@jacebenson/jsn 4.1.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/package.json +4 -1
- package/src/app.js +49 -65
- package/src/auth.js +46 -11
- package/src/capabilities.js +128 -0
- package/src/cli.js +129 -79
- package/src/commands/_ticket.js +36 -92
- package/src/commands/approvals.js +350 -0
- package/src/commands/atf.js +635 -0
- package/src/commands/auth.js +93 -13
- package/src/commands/catalog/enrich.js +139 -0
- package/src/commands/catalog.js +15 -133
- package/src/commands/cmdb.js +613 -0
- package/src/commands/codesearch.js +103 -0
- package/src/commands/dev/_generic.js +62 -110
- package/src/commands/dev/_simple.js +16 -12
- package/src/commands/dev/b4rules.js +1 -1
- package/src/commands/dev/domains.js +4 -0
- package/src/commands/dev/eval.js +40 -4
- package/src/commands/dev/flows.js +3 -0
- package/src/commands/dev/logs.js +97 -0
- package/src/commands/dev/rest.js +6 -0
- package/src/commands/dev/restmessage.js +1 -0
- package/src/commands/dev/scopes.js +49 -25
- package/src/commands/dev/scrapi.js +5 -0
- package/src/commands/dev/soapmessages.js +1 -0
- package/src/commands/dev/updatesets.js +145 -15
- package/src/commands/dev.js +3 -2
- package/src/commands/docs/db.js +14 -0
- package/src/commands/docs/docs.js +9 -21
- package/src/commands/docs/search.js +33 -10
- package/src/commands/groups.js +3 -0
- package/src/commands/records.js +254 -9
- package/src/commands/setup.js +5 -0
- package/src/commands/skill.js +3 -0
- package/src/commands/snippets.js +155 -0
- package/src/commands/tickets.js +9 -29
- package/src/commands/users.js +9 -35
- package/src/commands/version.js +3 -0
- package/src/errors.js +92 -1
- package/src/help.js +1 -0
- package/src/helpers.js +22 -11
- package/src/mutations.js +36 -91
- package/src/output.js +187 -61
- package/src/resolve-record.js +71 -0
- package/src/sdk.js +108 -8
- package/src/session.js +214 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Works standalone or with AI agents (Claude Code, OpenCode, Hermes, Cursor, Copil
|
|
|
10
10
|
npm install -g @jacebenson/jsn
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Node.js
|
|
13
|
+
Node.js (see `engines` in package.json). macOS, Linux, Windows.
|
|
14
14
|
|
|
15
15
|
The install also copies an AI agent skill file to `~/.agents/skills/servicenow/SKILL.md`.
|
|
16
16
|
|
|
@@ -59,9 +59,23 @@ jsn updatesets set "My Feature"
|
|
|
59
59
|
# Generic table access
|
|
60
60
|
jsn records list --table incident --limit 50 --json | jq '.[].number'
|
|
61
61
|
jsn records create --table incident --data '{"short_description":"test"}'
|
|
62
|
+
jsn records list --table incident --limit 50 --get "data.records.0.number" # no jq needed
|
|
63
|
+
jsn records list --table incident --query "active=true" # totals included by default
|
|
64
|
+
jsn records list --table incident --limit 10 --no-count # opt out of the total query
|
|
65
|
+
jsn records bulk --table incident --query "priority=1" --set '{"state":"3"}' # dry-run by default
|
|
66
|
+
jsn records get --table incident --sys-id 8a1234abcd5678 --attachments # record + its files
|
|
67
|
+
jsn records list --table incident --limit 10 --csv # output CSV
|
|
62
68
|
|
|
63
69
|
# Script execution
|
|
64
70
|
jsn eval "gs.info('Hello World')"
|
|
71
|
+
cat script.js | jsn eval --stdin # pipe a script in (no shell escaping)
|
|
72
|
+
|
|
73
|
+
# Saved query snippets (stored locally, run against the active profile)
|
|
74
|
+
jsn snippets save open-inc --table incident --query "active=true"
|
|
75
|
+
jsn snippets run open-inc
|
|
76
|
+
|
|
77
|
+
# Live log tailing (Ctrl+C to stop)
|
|
78
|
+
jsn logs follow --level error --tail 10
|
|
65
79
|
```
|
|
66
80
|
|
|
67
81
|
## Commands
|
|
@@ -76,7 +90,7 @@ Run `jsn` for the full grouped list. Commands are organized by ServiceNow domain
|
|
|
76
90
|
|
|
77
91
|
**User Experience** — `forms`, `lists`, `clientscripts`, `uipolicies`, `uiactions`
|
|
78
92
|
|
|
79
|
-
**Data** — `records
|
|
93
|
+
**Data** — `records` (list/get/create/update/delete/count/bulk/attachments), `tables`, `columns`, `includes`, `import`, `logs` (list/show/follow), `snippets` (save/run), `users`, `groups`
|
|
80
94
|
|
|
81
95
|
Every command supports `--json`, `--query`, and `--help`.
|
|
82
96
|
|
|
@@ -106,6 +120,8 @@ jsn auth modify dev12345 # Toggle read-only / skip conf
|
|
|
106
120
|
| `--json` | Pipelines, scripts |
|
|
107
121
|
| `--markdown` | Documentation |
|
|
108
122
|
| `--quiet` / `-q` | Data only, no envelope |
|
|
123
|
+
| `--csv` | Spreadsheets (opens straight into Excel) |
|
|
124
|
+
| `--get <path>` | Pull one value out of the JSON envelope — no jq (e.g. `--get "data.records.0.number"`) |
|
|
109
125
|
|
|
110
126
|
## Authentication
|
|
111
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"test": "node --experimental-test-module-mocks --test $(find test -name '*.test.js')",
|
|
12
12
|
"lint": "npx eslint src/ bin/ test/",
|
|
13
13
|
"start": "node bin/jsn.js",
|
|
14
|
+
"docs": "npm --prefix docs run build",
|
|
15
|
+
"docs:serve": "npm --prefix docs run serve",
|
|
14
16
|
"tag-reminder": "node scripts/tag-reminder.js",
|
|
15
17
|
"release": "node scripts/release.js"
|
|
16
18
|
},
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"yargs": "^17.7.2"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
39
|
+
"@11ty/eleventy": "^3.1.6",
|
|
37
40
|
"@eslint/js": "^10.0.1",
|
|
38
41
|
"@inquirer/search": "^4.2.1",
|
|
39
42
|
"globals": "^17.6.0"
|
package/src/app.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
// App context: bundles config, auth, SDK, output, and runtime context
|
|
2
|
+
//
|
|
3
|
+
// Identity ("which instance, as whom, with what profile flags") is owned by
|
|
4
|
+
// the SESSION (src/session.js): the constructor resolves one from the
|
|
5
|
+
// config, the cli.js middleware re-applies it once --instance/--profile are
|
|
6
|
+
// known (applySession), and getEffectiveInstance()/requireInstance()/
|
|
7
|
+
// setEffectiveInstance() are thin session-backed shims kept for the many
|
|
8
|
+
// command handlers that already call them.
|
|
2
9
|
|
|
3
10
|
import { AuthManager } from './auth.js';
|
|
4
11
|
import { SDKClient } from './sdk.js';
|
|
5
|
-
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
12
|
+
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled, hyperlink } from './output.js';
|
|
13
|
+
import { saveConfig } from './config.js';
|
|
14
|
+
import { resolveSession, applySession, contextFromSession, requireSessionInstance } from './session.js';
|
|
8
15
|
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
9
16
|
import { errUsage, errAuth } from './errors.js';
|
|
10
17
|
import { isMutationCommand } from './mutations.js';
|
|
@@ -14,52 +21,47 @@ import process from 'node:process';
|
|
|
14
21
|
export class App {
|
|
15
22
|
constructor(cfg) {
|
|
16
23
|
this.config = cfg;
|
|
17
|
-
this.auth = new AuthManager(this);
|
|
18
24
|
this.output = new OutputWriter({ format: resolveFormat(cfg.format) });
|
|
19
25
|
this.sdk = null;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// Back-compat field: flag-provided instance override (--instance /
|
|
27
|
+
// --profile). Written by applySession only; mirrored on argv no longer.
|
|
28
|
+
this._overrideInstance = null;
|
|
29
|
+
// Pinned argv flags for session re-resolution after config edits
|
|
30
|
+
// (auth switch / login / remove change the config under a pinned
|
|
31
|
+
// --profile). Set by applySession; {} means "no flags seen".
|
|
32
|
+
this._sessionArgv = {};
|
|
33
|
+
|
|
34
|
+
// Resolve the initial session from config alone (no argv yet — the
|
|
35
|
+
// middleware re-applies with argv). AuthManager is TOLD the identity:
|
|
36
|
+
// it reads username/instance through this provider over the session
|
|
37
|
+
// instead of reaching into config.profiles itself.
|
|
38
|
+
this.session = resolveSession({}, cfg);
|
|
39
|
+
this.auth = new AuthManager({
|
|
40
|
+
getUsername: () => this.session?.username || null,
|
|
41
|
+
getEffectiveInstance: () => this.session?.instance || '',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (this.session.instance) {
|
|
45
|
+
this._buildSdk(this.session.instance, this.session);
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
this.context = {
|
|
29
|
-
profileName: '',
|
|
30
|
-
username: '',
|
|
31
|
-
scope: '',
|
|
32
|
-
updateSet: '',
|
|
33
|
-
};
|
|
34
|
-
|
|
48
|
+
this.context = { profileName: '', username: '', scope: '', updateSet: '' };
|
|
35
49
|
this.loadContext();
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
52
|
+
/** Build (or rebuild) the SDK client for an instance + session. */
|
|
53
|
+
_buildSdk(instance, session = this.session) {
|
|
54
|
+
this.sdk = new SDKClient(instance, this.auth, {
|
|
55
|
+
domain: session?.domain || '',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!instance) return;
|
|
49
|
-
this.context.profileName = extractProfileName(instance);
|
|
50
|
-
for (const [name, profile] of Object.entries(this.config.profiles || {})) {
|
|
51
|
-
if (profile.instance_url === instance) {
|
|
52
|
-
this.context.profileName = name;
|
|
53
|
-
this.context.username = profile.username || '';
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
59
|
+
loadContext() {
|
|
60
|
+
this.context = contextFromSession(this.session, this.config);
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
getEffectiveInstance() {
|
|
60
|
-
|
|
61
|
-
if (this._overrideInstance) return this._overrideInstance;
|
|
62
|
-
return getEffectiveInstance(this.config);
|
|
64
|
+
return this.session?.instance || '';
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
async printContextHeader(argv = {}) {
|
|
@@ -124,16 +126,15 @@ export class App {
|
|
|
124
126
|
|
|
125
127
|
// Read-only profiles get a lock badge on every command run, so the
|
|
126
128
|
// armed safety is always visible (not just in `auth status`).
|
|
127
|
-
const
|
|
128
|
-
const roBadge = activeProfile?.read_only === true ? ' 🔒' : '';
|
|
129
|
+
const roBadge = this.session?.readOnly === true ? ' 🔒' : '';
|
|
129
130
|
|
|
130
131
|
process.stderr.write('# Use `jsn scopes` to change scope, `jsn updatesets set` to change updateset\n');
|
|
131
132
|
process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
|
|
132
133
|
|
|
133
|
-
const profileStr =
|
|
134
|
-
const userStr =
|
|
135
|
-
const scopeStr =
|
|
136
|
-
const updateSetStr =
|
|
134
|
+
const profileStr = hyperlink(`${String(this.context.profileName).padEnd(9)}${roBadge}`, instanceLink);
|
|
135
|
+
const userStr = hyperlink(String(displayUserName).padEnd(9), userLink);
|
|
136
|
+
const scopeStr = hyperlink(String(scopeFormatted).padEnd(17), scopeLink);
|
|
137
|
+
const updateSetStr = hyperlink(updateSet, updateSetLink);
|
|
137
138
|
|
|
138
139
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
139
140
|
|
|
@@ -185,9 +186,7 @@ export class App {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
requireInstance() {
|
|
188
|
-
|
|
189
|
-
throw errUsage('Instance URL required. Set via --instance flag, SERVICENOW_INSTANCE_URL env, or config file.');
|
|
190
|
-
}
|
|
189
|
+
requireSessionInstance(this.session);
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
requireAuth() {
|
|
@@ -197,26 +196,11 @@ export class App {
|
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
199
|
+
* Back-compat shim: override the effective instance for this command run.
|
|
200
|
+
* Routes through the session resolver (rebuilds the SDK client).
|
|
202
201
|
*/
|
|
203
202
|
setEffectiveInstance(url) {
|
|
204
|
-
|
|
205
|
-
if (normalizedUrl) {
|
|
206
|
-
this._overrideInstance = normalizedUrl;
|
|
207
|
-
this.sdk = new SDKClient(normalizedUrl, this.auth, {
|
|
208
|
-
domain: this._profileDomain(this.config),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Read the configured domain for the active profile (empty = no scoping). */
|
|
214
|
-
_profileDomain(cfg) {
|
|
215
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
216
|
-
if (name && cfg.profiles[name]) {
|
|
217
|
-
return cfg.profiles[name].domain || '';
|
|
218
|
-
}
|
|
219
|
-
return '';
|
|
203
|
+
applySession(this, { instance: url });
|
|
220
204
|
}
|
|
221
205
|
|
|
222
206
|
/** Set (or clear with '') the domain for the active profile. */
|
package/src/auth.js
CHANGED
|
@@ -272,23 +272,40 @@ function askHidden(promptText) {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
export class AuthManager {
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
/**
|
|
276
|
+
* @param {object} identity — the resolved-identity provider. Production
|
|
277
|
+
* wiring (src/app.js) hands an adapter over app.session:
|
|
278
|
+
* { getUsername(), getEffectiveInstance() }. AuthManager no longer
|
|
279
|
+
* reaches into config.profiles itself — the session owns "who am I".
|
|
280
|
+
*
|
|
281
|
+
* Transitional back-compat: a legacy configProvider ({ config, ...
|
|
282
|
+
* getEffectiveInstance() }) is adapted to the identity surface, so the
|
|
283
|
+
* existing test fixtures (which predate the session) keep working.
|
|
284
|
+
*/
|
|
285
|
+
constructor(identity) {
|
|
286
|
+
if (identity && identity.config && typeof identity.getUsername !== 'function') {
|
|
287
|
+
const provider = identity;
|
|
288
|
+
this.identity = {
|
|
289
|
+
getUsername: () => {
|
|
290
|
+
const cfg = provider.config;
|
|
291
|
+
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
292
|
+
return (name && cfg.profiles?.[name]?.username) || null;
|
|
293
|
+
},
|
|
294
|
+
getEffectiveInstance: () => provider.getEffectiveInstance(),
|
|
295
|
+
};
|
|
296
|
+
} else {
|
|
297
|
+
this.identity = identity;
|
|
298
|
+
}
|
|
277
299
|
this.httpClient = { timeout: 30000 };
|
|
278
300
|
}
|
|
279
301
|
|
|
280
302
|
/**
|
|
281
|
-
*
|
|
303
|
+
* The username for credential keying, from the resolved session identity.
|
|
282
304
|
* Returns null when no profile is configured — callers fall back to bare
|
|
283
305
|
* instance key for backward compatibility with legacy / no-profile usage.
|
|
284
306
|
*/
|
|
285
307
|
_activeUsername() {
|
|
286
|
-
|
|
287
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
288
|
-
if (name && cfg.profiles[name] && cfg.profiles[name].username) {
|
|
289
|
-
return cfg.profiles[name].username;
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
308
|
+
return this.identity.getUsername() || null;
|
|
292
309
|
}
|
|
293
310
|
|
|
294
311
|
getLastSeen(instance) {
|
|
@@ -334,7 +351,7 @@ export class AuthManager {
|
|
|
334
351
|
isAuthenticated() {
|
|
335
352
|
if (process.env.SERVICENOW_OAUTH_TOKEN) return true;
|
|
336
353
|
if (getBasicAuthFromEnv()) return true;
|
|
337
|
-
const instance = this.
|
|
354
|
+
const instance = this.identity.getEffectiveInstance();
|
|
338
355
|
if (!instance) return false;
|
|
339
356
|
try {
|
|
340
357
|
this.getCredentialsFor(instance);
|
|
@@ -357,7 +374,7 @@ export class AuthManager {
|
|
|
357
374
|
if (process.env.SERVICENOW_OAUTH_TOKEN) {
|
|
358
375
|
return { auth_method: 'oauth', access_token: process.env.SERVICENOW_OAUTH_TOKEN, auth_source: 'env_token' };
|
|
359
376
|
}
|
|
360
|
-
const instance = this.
|
|
377
|
+
const instance = this.identity.getEffectiveInstance();
|
|
361
378
|
if (!instance) {
|
|
362
379
|
throw errAuth('No instance configured');
|
|
363
380
|
}
|
|
@@ -581,6 +598,24 @@ export class AuthManager {
|
|
|
581
598
|
}
|
|
582
599
|
deleteCredentials(instance, this._activeUsername());
|
|
583
600
|
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Migrate legacy bare-instance-keyed credentials to <user>@<instance>
|
|
604
|
+
* keying after an OAuth login verifies the username. Re-saves under the
|
|
605
|
+
* compound key and removes the bare key. Returns true when a migration
|
|
606
|
+
* happened. Credential-store internals (key shapes, legacy bare keys)
|
|
607
|
+
* live behind this seam — command handlers must not import
|
|
608
|
+
* loadCredentials/saveCredentials/deleteCredentials to do this themselves.
|
|
609
|
+
*/
|
|
610
|
+
migrateLegacyCredential(instance, username) {
|
|
611
|
+
if (!instance || !username) return false;
|
|
612
|
+
const stored = loadCredentials(instance);
|
|
613
|
+
if (!stored || !stored.access_token) return false;
|
|
614
|
+
stored.username = username;
|
|
615
|
+
saveCredentials(instance, stored, username);
|
|
616
|
+
deleteCredentials(instance); // remove bare-instance legacy key
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
584
619
|
}
|
|
585
620
|
|
|
586
621
|
export { saveCredentials, loadCredentials, deleteCredentials, askHidden };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Command capability registry — the single source of truth for command
|
|
2
|
+
// metadata that middleware cares about.
|
|
3
|
+
//
|
|
4
|
+
// Each command module declares its capabilities ONCE at the definition site
|
|
5
|
+
// via declareCapabilities(name, caps):
|
|
6
|
+
//
|
|
7
|
+
// {
|
|
8
|
+
// // Subcommand verbs that mutate instance state. The mutation guard
|
|
9
|
+
// // (src/mutations.js) derives its command-path list from these.
|
|
10
|
+
// mutationSubcommands: ['create', 'update', 'delete'],
|
|
11
|
+
// // Also registered under `jsn dev <name>` — derive both spellings.
|
|
12
|
+
// devAlias: true,
|
|
13
|
+
// // Command runs without a configured instance (joins the middleware
|
|
14
|
+
// // skip-list for the instance guard).
|
|
15
|
+
// noInstance: true,
|
|
16
|
+
// // Skip the once-per-day npm-version / skill freshness checks and the
|
|
17
|
+
// // interactive context header.
|
|
18
|
+
// skipDailyChecks: true,
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
// cli.js derives every skip-list and the mutation guard's path list from
|
|
22
|
+
// this registry. Hand-maintained string lists of command names are a bug
|
|
23
|
+
// farm — see the catalogitems create read-only bypass that shipped because
|
|
24
|
+
// mutations.js listed `catalog create-item` while the real command is
|
|
25
|
+
// `catalogitems create`.
|
|
26
|
+
//
|
|
27
|
+
// Note: yargs strips custom properties off command modules when it
|
|
28
|
+
// registers them (only original/description/handler/builder survive in
|
|
29
|
+
// getCommandHandlers()), so declarations live in this module-level map,
|
|
30
|
+
// keyed by root command name. Declaration happens when the command module
|
|
31
|
+
// factory runs — i.e. during cli.js's registration chain — so the registry
|
|
32
|
+
// is complete once buildCLI() returns.
|
|
33
|
+
|
|
34
|
+
const REGISTRY = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Declare a command's capabilities. Called by command factories and
|
|
38
|
+
* hand-written command modules at definition time.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name — root command name (as typed: `jsn <name>`)
|
|
41
|
+
* @param {object} caps — capability declaration (see module header)
|
|
42
|
+
* @returns {object} the caps object (for convenient inline use)
|
|
43
|
+
*/
|
|
44
|
+
export function declareCapabilities(name, caps) {
|
|
45
|
+
REGISTRY.set(name, { devAlias: false, ...caps });
|
|
46
|
+
return caps;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The collected capability registry: root command name → capabilities.
|
|
51
|
+
* Includes the implicit declarations for yargs built-ins ('help') and the
|
|
52
|
+
* shell plumbing ('completion') which have no factory to declare from.
|
|
53
|
+
*
|
|
54
|
+
* @returns {Map<string, object>}
|
|
55
|
+
*/
|
|
56
|
+
export function collectCapabilities() {
|
|
57
|
+
const caps = new Map(REGISTRY);
|
|
58
|
+
if (!caps.has('help')) caps.set('help', { noInstance: true, skipDailyChecks: true });
|
|
59
|
+
if (!caps.has('version')) caps.set('version', { noInstance: true, skipDailyChecks: true });
|
|
60
|
+
if (!caps.has('completion')) caps.set('completion', { noInstance: true, skipDailyChecks: true });
|
|
61
|
+
return caps;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Derive the set of root command names that run without an instance.
|
|
66
|
+
* Preserves the legacy skip-list semantics exactly: help, version,
|
|
67
|
+
* completion, setup, auth, skill, docs.
|
|
68
|
+
*
|
|
69
|
+
* @param {Map<string, object>} [caps] — from collectCapabilities()
|
|
70
|
+
* @returns {Set<string>}
|
|
71
|
+
*/
|
|
72
|
+
export function noInstanceCommands(caps = collectCapabilities()) {
|
|
73
|
+
const out = new Set();
|
|
74
|
+
for (const [name, c] of caps) {
|
|
75
|
+
if (c.noInstance) out.add(name);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Derive the set of root command names that skip the daily npm-version /
|
|
82
|
+
* skill checks and the interactive context header. Legacy semantics:
|
|
83
|
+
* help, version, completion, skill, docs (a subset of noInstanceCommands).
|
|
84
|
+
*
|
|
85
|
+
* @param {Map<string, object>} [caps] — from collectCapabilities()
|
|
86
|
+
* @returns {Set<string>}
|
|
87
|
+
*/
|
|
88
|
+
export function dailyCheckSkipCommands(caps = collectCapabilities()) {
|
|
89
|
+
const out = new Set();
|
|
90
|
+
for (const [name, c] of caps) {
|
|
91
|
+
if (c.skipDailyChecks) out.add(name);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive the mutation command-path list (argv._ token arrays) from the
|
|
98
|
+
* capability registry. Each entry is a prefix pattern: an argv._ path that
|
|
99
|
+
* begins with the pattern IS that mutation (trailing positionals like a
|
|
100
|
+
* sys_id don't change intent).
|
|
101
|
+
*
|
|
102
|
+
* Commands with `devAlias: true` are also registered under `jsn dev <name>`,
|
|
103
|
+
* so both spellings are emitted.
|
|
104
|
+
*
|
|
105
|
+
* A whole-command mutation surface (e.g. `jsn eval`, `jsn rest` — the
|
|
106
|
+
* command itself is the mutation, there is no mutating sub-verb) declares
|
|
107
|
+
* `mutationSubcommands: ['']`.
|
|
108
|
+
*
|
|
109
|
+
* @param {Map<string, object>} [caps] — from collectCapabilities()
|
|
110
|
+
* @returns {Array<string[]>}
|
|
111
|
+
*/
|
|
112
|
+
export function mutationPaths(caps = collectCapabilities()) {
|
|
113
|
+
const paths = [];
|
|
114
|
+
for (const [name, c] of caps) {
|
|
115
|
+
if (!c.mutationSubcommands) continue;
|
|
116
|
+
for (const sub of c.mutationSubcommands) {
|
|
117
|
+
const parts = sub === '' ? [] : sub.split(' ');
|
|
118
|
+
paths.push([name, ...parts]);
|
|
119
|
+
if (c.devAlias) paths.push(['dev', name, ...parts]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return paths;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Test hook: clear the registry between isolated buildCLI() runs. */
|
|
126
|
+
export function _resetCapabilities() {
|
|
127
|
+
REGISTRY.clear();
|
|
128
|
+
}
|