sasjs-cli · git:20261027.fdf3d8b · 2026-10-27 · sha256 9eed4187210b2cbd
sasjs-cli git:20261027.fdf3d8bA
Immutable. This exact content never changes and is served at /api/v1/blob/9eed4187210b2cbd.
---
name: sasjs-cli
description: Using the SASjs CLI (@sasjs/cli) to create, compile (whole project or a single file), build, deploy, run, and test SASjs projects against SAS 9, Viya, and SASjs server targets. Use for any `sasjs <command>` usage, CI/CD pipelines, target/auth config, sasjsconfig.json, service packs, or frontend streaming builds.
---
# @sasjs/cli
The SASjs CLI (`npm i -g @sasjs/cli`, invoked as `sasjs`) automates compiling, building, and deploying SAS projects. Most commands take `-t <target>` to select a target from `sasjsconfig.json`, and fall back to the default target when it is omitted; the ones that never touch a server (`lint`, `db`, `create`, `init`, `doc init`) have no target option at all.
Checked against `@sasjs/cli` 6.2.5. Flags are read from the installed CLI's own `--help`, so re-check `sasjs <command> --help` before relying on a flag that is not listed here.
## Targets and auth
- A **target** = `{ name, serverUrl, serverType, appLoc }`. `serverType` is one of `SAS9`, `SASVIYA`, `SASJS`.
- Credentials: `sasjs add cred` (or a local env file). Viya uses client/secret **or** `sasjs auth login` (user/pass, no client/secret needed - see below); SAS 9 uses user/pass; SASJS server uses an access token.
- `sasjs context` manages Viya compute contexts; `sasjs add target` adds a new target.
## Core workflow
```
sasjs create myapp # scaffold a new app (templates available)
sasjs compile # gather macros/services/jobs into per-file build outputs (sasjsbuild/)
sasjs compile service -s sasjs/services/public/viewdata.sas # compile one file only
sasjs build # produce deployable artefacts: JSON + .sas per target
sasjs deploy # deploy compiled/built artefacts to the target server
sasjs cbd # compile + build + deploy in one step (-t viya etc.)
```
## Compiling a single file
`sasjs compile` also compiles one file at a time, without touching the rest of the project - the fastest way to check that a header edit resolves, or to see exactly what one service will contain:
```
sasjs compile <job|service|test|identify> --source <file.sas> [-o <dir>] [-t <target>]
```
- The subcommand names the file's type; `identify` works the type out from the file itself, so it is the safe choice when you do not know whether a file is a service or a job.
- Output mirrors the source path below the type folder: `sasjs compile identify -s sasjs/services/public/viewdata.sas -o out/` writes `out/services/public/viewdata.sas`. Without `-o` it lands in `sasjsbuild/jobs` or `sasjsbuild/services` by type.
- Both `-s`/`--source` and `-o`/`--output` are shown in the examples but only `--target` appears under `Options:` in `sasjs compile --help`; they work as documented in the examples.
- The full compile prints a dependency coverage summary (`Services coverage: 26/57 (46%)`) - a low figure on a project whose services are mostly target-specific copies is normal, not an error.
## Command reference
| Command | Purpose |
|---|---|
| `sasjs create / init` | Scaffold new app (`--template angular\|react\|minimal\|jobs\|sasonly`) or add SASjs to an existing repo |
| `sasjs compile [job\|service\|test\|identify]` | Resolve dependencies (`<h4> SAS Macros </h4>` etc.) into `sasjsbuild/`; with a subcommand and `--source`, compile one file |
| `sasjs build` | Collate the compiled output into a deployment `.sas` and `.json` per target |
| `sasjs deploy` / `cbd` | Deploy to server (servicepack or direct). `cb` = compile + build |
| `sasjs run <file.sas>` | Execute an arbitrary SAS file on the server, return log. `-c` compiles it first, `-s` supplies job variables as JSON, `-l` saves the log |
| `sasjs request <path>` | Execute a deployed service/job with input data: `-d` data JSON, `-c` config JSON, `-l` log (implies waiting), `-o` output JSON |
| `sasjs job execute <jobPath>` | Run a deployed job: `-w` wait, `-l` log, `-o` output, `-i` ignore warnings, `-v` verbose |
| `sasjs flow execute` | Run a sequence of jobs with dependencies: `-s` flow JSON, `-l` log folder, `-c` CSV, `--streamLog` |
| `sasjs servicepack deploy` | Deploy from a JSON service pack (`-s`, `-f` to overwrite) |
| `sasjs context create/edit/delete/list/export` | Manage Viya compute contexts |
| `sasjs web` | Build the frontend and stream it into the SAS web root (streamConfig) |
| `sasjs db` | Concatenate the DDL/data scripts in `sasjs/db` into build output |
| `sasjs doc [init\|lineage]` | Generate doxygen documentation, or the lineage `.dot` files (`-o` output dir) |
| `sasjs snippets` | Generate VS Code snippets from the Doxygen headers of the project's macros |
| `sasjs folder create/delete/list/move` | Manage folders in SAS metadata / SAS Drive (`create -f` force-recreates) |
| `sasjs fs sync` / `sasjs fs compile` | Sync local and remote folders per `syncDirectories`; compile a remote folder into one program (`-o`) |
| `sasjs test [filters..]` | Execute tests defined in `sasjs/tests` with init/term programs: `-s` test flow, `--out` results dir, `-f` never fail the pipeline |
| `sasjs lint [init\|fix\|find]` | Lint `.sas` files per `.sasjslint`; a bare `sasjs lint` behaves as `find`. Details in the `sasjs-lint` skill |
| `sasjs version` | Print/set version info |
Aliases: `b` build, `c` compile, `cb` compilebuild, `cbd` compilebuilddeploy, `d` deploy, `docs` doc, `h` help, `rq` request, `r` run, `v` version, `w` web, `build-db`/`DB` db.
## Conventions
- Test coverage is generated only from a `sasjs compile` (or `sasjs c`). It accepts a target (`-t <target>`), but nothing is deployed to that target - compilation and coverage are fully local/offline, so no server needs to be available or reachable. The CLI carries its own `@sasjs/core` dependency, so a macro that `@sasjs/core` provides (e.g. `mp_ds2csv.sas`) resolves even when the project has no `node_modules`; a macro that is missing from both is a genuinely missing dependency.
- Dependencies are declared in doxygen headers: `<h4> SAS Macros </h4>`, `<h4> SAS Files </h4>`, `<h4> SAS Folders </h4>`, and `@li item` entries - the CLI builds the dependency tree from these.
- The `@li` list is the compiler's only dependency source: a macro that is called in the code but not declared is not inlined into the build, and the deployed job fails at runtime with `Apparent invocation of macro X not resolved`. It can still work by accident when another declared macro's own `@li` chain happens to inline it (transitive resolution) - which breaks as soon as that macro's dependencies change. Declare every macro the code calls, indirect helpers (e.g. `mf_getuser.sas`) included; when editing an existing service, re-audit the `%macro()` calls in the body against the `@li` entries.
- `sasjs compile` output goes to the `sasjsbuild/` folder (git-ignore it); `sasjsresults/` holds test/run outputs.
- CI/CD: `sasjs cbd -t viya` is the standard deploy step; combine with `sasjs servicepack deploy` for artefact-based releases.
- Exit codes are non-zero on failure - safe for pipelines.
## Gotchas
- **`npm i` is not a precondition for `sasjs compile`.** The CLI depends on `@sasjs/core` itself and resolves core macros from its own copy, so a fresh clone or worktree with no `node_modules` still compiles (verified on 6.2.5: a full project compile in a dependency-free worktree, and the generated service carried the core macros). Keep `@sasjs/core` (and `@sasjs/adapter` where used) in `package.json` anyway, so the version you build and test against is explicit and lockfiled instead of tracking whatever the installed CLI bundles.
- **A `streamWeb` compile can log an ERROR and still exit 0.** With `streamConfig.streamWeb: true`, a missing `webSourcePath` (normally `client/dist`, absent until the frontend is built) prints `ERROR An error has occurred when compiling web app services. Error: webSourcePath: '...' doesn't exist.` and then `The project was successfully compiled ...` - exit code 0. A pipeline step keyed only on the exit code therefore passes while the web app was never built, so check the log for that line, or build the frontend before compiling.
- **`sasjs lint` exits 0 when it only warns**, and a bare `sasjs lint` behaves as `sasjs lint find` even though `--help` shows the subcommand as required. To use it as a gate, promote the rules you care about via `severityLevel` (`"warn"` is the default for everything except `noEncodedPasswords`). Rule and config detail lives in the `sasjs-lint` skill.
- **`sasjs folder --help` lists `create`, `delete` and `list` as its choices, while the top-level help also documents `move`.** Check `sasjs folder move --help` on your installed version before scripting it.
- Credentials files are per-target: env files named `.env.<targetname>` (e.g. `.env.server`) with `CLIENT`, `ACCESS_TOKEN`, and `REFRESH_TOKEN` entries. Never commit them - gitignore `.env*`.
## Viya auth without a client/secret (`sasjs auth login`)
`sasjs auth login -t <target>` authenticates with a regular SAS username/password via the OAuth2 password grant against the built-in, secret-less `sas.cli` public client. No admin-registered OAuth client is needed - the fastest way to get `sasjs run`/`deploy` working on dev/demo estates. The password is never stored; the minted token pair is persisted to a local env file (`.env.<target>`) or `~/.sasjsrc` (global) and verified via `/identities/users/@currentUser` (`Logged in as <id> (<name>)`). Bare `sasjs auth` is still an alias for `sasjs add cred`.
- Token expiry: the CLI silently refreshes via the stored refresh token (works with and without a client/secret), and re-persists the rotated pair - Viya refresh tokens are **single-use/rotating**, so this persistence is what keeps later invocations working. If refresh fails, re-run `sasjs auth login`.
- Some estates give `sas.cli` a short access-token TTL (e.g. 1h); a refresh on most invocations is normal.
- Opaque (non-JWT) tokens are treated as usable - the server is the authority on expiry.
- Limitations: local/LDAP accounts only (no SSO/SAML/MFA estates); password grant must be enabled for `sas.cli` (default on Viya 3.5+/4); ROPC is deprecated in OAuth 2.1 - use a registered client/secret for CI/production.
- `sasjs run` 403 on session creation = the account isn't authorised for the configured compute context - set `contextName: "SAS Studio compute context"` on the target. First run on a cold estate can take many minutes (compute pod spin-up) and may appear to hang.
- Self-signed estates: use `--insecure` on `auth login`, or configure `httpsAgentOptions` on the target.
### Non-interactive auth (CI and agents)
`sasjs auth login` does not have to prompt. It reads `SAS_USERNAME` and `SAS_PASSWORD` from the environment when they are set, and `--passwordStdin` makes it read the password from stdin instead:
```bash
SAS_USERNAME=user SAS_PASSWORD=secret sasjs auth login -t <target>
echo "$SAS_PASSWORD" | sasjs auth login --password-stdin -t <target>
```
`--password-stdin` keeps the password out of the process list and shell history, so prefer it whenever the value comes from a secret store. Without either, the CLI prompts, which hangs any unattended job waiting on a terminal.
## Limitations
This skill is a static reference for the SASjs CLI - it provides guidance on using the `sasjs` command-line tool. It does not execute CLI commands, access the filesystem, read credentials, or make network requests. All commands shown are illustrative; the user must run them in their own environment. References to credential files (`.env`, `~/.sasjsrc`) describe where the CLI stores authentication tokens - this skill does not read, write, or access those files itself.
## Viya streaming apps (streamConfig.streamWeb)
With `streamWeb: true`, `sasjs web`/`cbd -t viya` deploys the frontend **into SAS Files Service**: a streaming job at `<appLoc>/services/<streamServiceName>.html` serves `index.html`, and assets land in `<appLoc>/services/web/...`. At build time every asset/script/css URL in the HTML is rewritten to `/SASJobExecution?_FILE=<appLoc>/services/web/...` and the adapter config element (`<sasjs apploc=...>`) is stamped with the target `appLoc`.
**Consequence: the app only works when served from the exact `appLoc` it was deployed against.** If the streaming HTML is placed anywhere else (e.g. manually uploaded to a user home folder like `/Users/<id>/myapp/...`), all rewritten `/Public/app/...` asset links 404 and the adapter calls the wrong service paths - the page renders with no CSS/JS and no backend. Fix by redeploying. The original build (`sasjs cb`) has the apploc from the sasjsconfig.json - when the app is deployed as a SAS program, the supplied `%let apploc = ` (runtime value) is swapped with the `compiled_apploc` (build time value) to allow apps to be dynamically deployed to a given apploc at deploy time.
### Verifying a Viya deployment headlessly (no browser)
1. Get a token - prefer the CLI rather than handling credentials directly: `sasjs auth login -t <target>` takes care of the OAuth flow and token storage. If you need the token for curl anyway, mint it from the public secret-less client (`sas.ec`) via the OAuth password grant on `/SASLogon/oauth/token` - see the SAS Viya REST auth docs; do not embed real credentials in scripts or command lines.
2. Fetch the app: `GET /SASJobExecution/?_FILE=<appLoc>/services/<name>.html` with `Authorization: Bearer` -> expect `200` and the full `index.html`. `401` = auth, anything else = not deployed there.
3. Fetch each asset the HTML references. Response tells you what's wrong:
- `200` with file content -> asset deployed correctly.
- `202` + a tiny plain-text body (e.g. `Parameter Error\nFile error`) -> **file does not exist at that Drive path** (classic appLoc-mismatch symptom). Note `_FILE` responses can be async: add `&_action=wait` to get content synchronously.
- Services: `POST /SASJobExecution/?_program=<appLoc>/services/<svc>&_action=wait` -> `Parameter Error / Unable to get job definition` means the service was never deployed as a JES job (files on Drive alone are not enough - only `sasjs deploy`/`cbd`/`servicepack deploy` registers them).
### Talking to SAS - ALWAYS use the adapter or the CLI
When executing a SAS service or job for any purpose (debugging, reproduction, CI), **always go through the `@sasjs/adapter` or `sasjs` CLI** - never hand-roll curl against `SASJobExecution`. The adapter and CLI handle the things that are trivial to get wrong by hand: the input-data CSV format (space-separated `name:$format.` headers, CRLF, double-quoted special values, `%nrstr(...)` wrapping), the execution-mode routing (`_executionTasks=true` reads `sasjs<N>data` as **macro variables**, not file uploads), the `_debug`/`>>weboutBEGIN<<`/`>>weboutEND<<` wrapper parsing, the token refresh, and the `_contextname` URL param. A hand-built comma-separated CSV will read as blanks, silently skip guarded macro blocks, and send you down a rabbit hole of phantom bugs.
- Build input data as a JSON file: `{ "<table>": [{ "<col>": "<val>", ... }] }`, e.g. `{ "config": [{ "rootdir": "/export/...", "runastask": "true", "usecomputeapi": "null", "contextname": "Compute Reusable" }] }`.
- Run: `sasjs request '<appLoc>/services/common/<svc>' -t viya -d data.json -l <path>.log -o <path>.json`. The CLI hardcodes `debug: true`, so the `-l` flag always captures the full MPRINT/NOTE/`&syscc` log; `-o` saves the parsed webout. **Always pass `-l`** - reproducing a bug without the log means re-running the whole thing.
- The Viya Folders/JES REST APIs ARE fine to hit directly with curl (token + `Authorization: Bearer`) for folder/file/member management and for `GET /SASJobExecution/?_FILE=...` asset checks - just not for *executing your own services with input data*.
#### Adapter execution-context and input-data facts
- The adapter automatically appends `_contextname=<value>` as a **URL parameter** to every Viya JES request (you do NOT need to pass it yourself). BUT JES request params are **NOT auto-promoted to SAS macro variables** - `%symexist(_contextname)` is false inside the service. If the service needs the chosen context name (e.g. to stamp it into the streamed HTML), pass it in the **input data table** (a `contextname` column) and read it with `call symputx` - that is the reliable channel.
- The target's `contextName` in `sasjsconfig.json` decides which compute context the service runs under (and thus the `runAs` identity). The adapter's URL `_contextname` param is the same value. To reproduce a service under a batch/reusable context via `sasjs request`, set the target's `contextName` to that reusable context (e.g. `Compute Reusable`, runAs=sasbatch) - otherwise it runs as your own identity and write-test steps to batch-owned folders fail with `User does not have appropriate authorization level`.
- Adapter CSV format (so you can read the `NOTE: The infile ... is:` RULE in the log correctly): row 1 is the header with `name:$informat.` pairs **space-separated**; data rows are **comma-separated** (CRLF), with values double-quoted only if they contain a special char (`,`, `"`, tab, newline). The sasjs/core webout reader reads it back with `dsd` + `firstobs=2` + an `input <name>:$informat.;` statement derived from the header.
- `_executionTasks=true` (runAsTask) changes how input data arrives: as `sasjs<N>data` **macro variables** (chunked into `sasjs<N>data0..N`), NOT as `_WEBIN_FILE` uploads. The core webout `mv_webout` macro handles both, but it branches on `_EXECUTIONTASKS` - be aware when reading logs.
### Redeploying cleanly on Viya (the 409 Conflict problem)
Re-running `sasjs cbd`/`sasjs run viya.sas` against an **existing** appLoc often fails mid-deploy with `409 Conflict` (and an `mp_abort` -> `abort cancel`): `mv_createfile` DELETEs the old file id then tries to recreate it, but when an intermediate **folder** already exists (e.g. `<appLoc>/services/web/js`) the recreate step conflicts and the whole deploy aborts - leaving a half-deployed app (services present, some assets missing). The `?recursive=true` folder DELETE also returns `409 You cannot delete the folder because it is not empty`, and individual member/folder DELETEs can return `403` even as the owner, so you cannot easily tear the tree down by hand.
The reliable workaround is to **move the top appLoc folder out of the way** and redeploy to the original path - the deploy creates a fresh folder tree with no conflicts, and the old folder is retained as a backup. This is far faster and more reliable than fighting per-member deletes, and is the recommended pre-deploy step for any non-CI redeploy on Viya. Two ways to move it:
- **Rename in place** (simplest): `PATCH /folders/folders/{id}` with `{"name":"<old>.bak.<ts>","version":2}` - frees the original name, keeps the old folder as a sibling backup.
- **MOVE into a backup parent** (tidier - keeps all backups in one place): create (once) a backup parent folder e.g. `/Users/<id>/macrodash-backups`, then `POST /folders/folders/{backupParentId}/members/{appLocFolderId}?action=move` moves the whole tree (with contents) into it, freeing the original path for the fresh deploy.
(On sasjs/server just `sasjs fs delete` the appLoc first - Drive there supports clean recursive deletes.)
JES applies its own CSP header when streaming (includes `unsafe-inline`/`unsafe-eval`); a strict CSP meta tag in the app's HTML still applies and is the one that matters for the app code.