@jacebenson/jsn 4.3.9 → 4.3.10-dev.67
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 +11 -539
- package/package.json +1 -1
- package/src/auth.js +81 -38
- package/src/browser-auth.js +162 -0
- package/src/browser-launch.js +148 -0
- package/src/capabilities.js +10 -0
- package/src/cli.js +2 -2
- package/src/commands/auth.js +763 -650
- package/src/commands/flows.js +17 -7
- package/src/commands/midservers.js +1 -1
- package/src/commands/setup.js +25 -20
- package/src/commands/skill.js +1 -1
- package/src/commands/skipped-updates.js +1 -1
- package/src/commands/updatesets.js +14 -2
- package/src/errors.js +8 -0
- package/src/flow-create.js +329 -74
- package/src/flow-designer-interactive.js +11 -6
- package/src/flow-safe-edit.js +48 -0
- package/src/paginated-search.js +2 -2
- package/src/prompts.js +43 -0
package/src/commands/flows.js
CHANGED
|
@@ -21,7 +21,7 @@ const FLOW_COMMANDS = [
|
|
|
21
21
|
{ name: 'publish [identifier]', description: 'Publish (activate) a flow, or a custom action with --action' },
|
|
22
22
|
{ name: 'test [identifier]', description: 'Execute a record-triggered flow against one record' },
|
|
23
23
|
{ name: 'create', description: 'Create a draft flow: guided in a terminal, JSON manifest in scripts' },
|
|
24
|
-
{ name: 'update [identifier]', description: 'Update a draft flow (alias: edit)' },
|
|
24
|
+
{ name: 'update [identifier]', description: 'Update a draft, or a published flow whose definition reports write access (alias: edit)' },
|
|
25
25
|
{ name: 'doctor', description: 'Check whether this instance can publish flows at all' },
|
|
26
26
|
{ name: 'delete [identifier]', description: 'Delete a flow (not implemented: use the Flow Designer UI)' },
|
|
27
27
|
];
|
|
@@ -341,15 +341,19 @@ export function flowsCmd(wrap) {
|
|
|
341
341
|
.command({
|
|
342
342
|
command: 'update [identifier]',
|
|
343
343
|
aliases: ['edit'],
|
|
344
|
-
describe: '
|
|
344
|
+
describe: 'Save partial edits to a Draft, or to a published/active flow whose ProcessFlow definition reports security.can_write=true: record_create_or_update trigger table/condition and existing action inputs. Published flows are edited as their working definition; the published snapshot and runtime pointers must stay unchanged, and the command fails loudly if they do not. Flows without write access are refused before any lock or content write, naming the permission. Use --preview for before/after values without writes. Flow Logic, variables, catalog-trigger edits and stage placement require Flow Designer. Never publishes or executes. Use `jsn flows schemas` and `jsn actions action-type-schema --id <actionTypeId> --scope <scopeId>` for live schemas; omitted values are preserved.',
|
|
345
345
|
builder: (y) => y
|
|
346
346
|
.option('data', { type: 'string', describe: 'Update manifest as inline JSON' })
|
|
347
347
|
.option('data-file', { type: 'string', describe: 'Read the update manifest from a JSON file' })
|
|
348
|
+
.option('preview', { type: 'boolean', default: false, describe: 'Validate and show exact before/after input values without saving or versioning' })
|
|
348
349
|
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
349
350
|
handler: wrap(async (argv, app) => {
|
|
350
351
|
app.requireInstance();
|
|
351
352
|
const dataFile = argv.dataFile ?? argv['data-file'];
|
|
352
|
-
|
|
353
|
+
// No lifecycle filter: published flows are editable when their own
|
|
354
|
+
// definition reports write access, and a non-writable target is
|
|
355
|
+
// refused with a permission message rather than a lifecycle label.
|
|
356
|
+
const identifier = await pickFlow(argv, app);
|
|
353
357
|
if (identifier === undefined) return;
|
|
354
358
|
argv = { ...argv, identifier };
|
|
355
359
|
let manifest;
|
|
@@ -360,13 +364,19 @@ export function flowsCmd(wrap) {
|
|
|
360
364
|
} catch (error) {
|
|
361
365
|
throw new Error(`Invalid flow update manifest JSON: ${error.message}`, { cause: error });
|
|
362
366
|
}
|
|
363
|
-
await confirmDesignerMutation(argv, app, `Update
|
|
367
|
+
if (!argv.preview) await confirmDesignerMutation(argv, app, `Update flow ${identifier}; unspecified components and inputs are preserved. A published flow is edited as its working definition: the published snapshot must stay unchanged.`);
|
|
364
368
|
const result = await updateFlowFromManifest(app.sdk, app.getEffectiveInstance(), argv.identifier, manifest, {
|
|
369
|
+
preview: argv.preview,
|
|
365
370
|
readUpdateSet: () => readCurrentUpdateSet(app.sdk),
|
|
366
371
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
const diff = result.changes.map(change => `${change.component}${change.order ? ` ${change.order}` : ''}.${change.input}: ${JSON.stringify(change.before)} -> ${JSON.stringify(change.after)}`).join('\n');
|
|
373
|
+
const state = `${result.flow.status}${result.flow.active ? ', active' : ''}`;
|
|
374
|
+
const preservation = result.lifecycle?.published && !result.preview
|
|
375
|
+
? `; published snapshot ${result.runtime_preservation?.before?.published_definition?.snapshot_id ?? ''} unchanged`
|
|
376
|
+
: '';
|
|
377
|
+
return app.ok({ ...result, _formatted: diff }, {
|
|
378
|
+
summary: `${result.preview ? 'Preview for' : 'Updated'} flow "${result.flow.name}" (${result.flow.id}) in state ${state}${preservation} without activation`,
|
|
379
|
+
breadcrumbs: [{ action: 'show', cmd: `jsn flows show ${result.flow.id}`, description: 'Inspect the updated flow' }],
|
|
370
380
|
});
|
|
371
381
|
}),
|
|
372
382
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
-
import { input, password as passwordPrompt, select } from '
|
|
4
|
+
import { input, password as passwordPrompt, select } from '../prompts.js';
|
|
5
5
|
import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt, assertSafeExactMatch } from '../helpers.js';
|
|
6
6
|
import { FormatAuto } from '../output.js';
|
|
7
7
|
import { declareCapabilities } from '../capabilities.js';
|
package/src/commands/setup.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } from '@inquirer/core';
|
|
2
|
-
import { loginWizard, modifyProfile,
|
|
3
|
-
import { setActiveProfile } from '../config.js';
|
|
2
|
+
import { loginWizard, loginProfile, showAuthStatus, switchProfile, modifyProfile, logoutProfile, removeConnection } from './auth.js';
|
|
4
3
|
import { errUsage } from '../errors.js';
|
|
5
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
6
5
|
|
|
7
6
|
// Setup manages local profile config — runs without a configured instance.
|
|
8
7
|
// (Not in the daily-check skip-list: legacy behavior checks for updates here.)
|
|
9
|
-
declareCapabilities('setup', { noInstance: true });
|
|
8
|
+
declareCapabilities('setup', { noInstance: true, skipContextHeader: true });
|
|
10
9
|
|
|
11
10
|
const HUB_CHOICES = [
|
|
12
|
-
{ name: 'Add a
|
|
13
|
-
{ name: '
|
|
14
|
-
{ name: '
|
|
15
|
-
{ name: '
|
|
11
|
+
{ name: 'Add a connection', value: 'add' },
|
|
12
|
+
{ name: 'Sign in again', value: 'login' },
|
|
13
|
+
{ name: 'Check connections', value: 'status' },
|
|
14
|
+
{ name: 'Choose the default profile', value: 'switch' },
|
|
15
|
+
{ name: 'Change settings', value: 'modify' },
|
|
16
|
+
{ name: 'Sign out (keep the profile)', value: 'logout' },
|
|
17
|
+
{ name: 'Remove a connection', value: 'remove' },
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
/** The setup hub prompt; q and Escape resolve with no selected action. */
|
|
@@ -54,41 +56,44 @@ export async function authHubMenu({ promptFn = authHubPrompt, input, output } =
|
|
|
54
56
|
*/
|
|
55
57
|
export async function dispatchSetupAction(app, argv, action) {
|
|
56
58
|
if (action === 'add') {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
return loginWizard(app, argv);
|
|
60
|
+
} else if (action === 'login') {
|
|
61
|
+
return loginProfile(argv, app);
|
|
62
|
+
} else if (action === 'status') {
|
|
63
|
+
return showAuthStatus(argv, app);
|
|
59
64
|
} else if (action === 'switch') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
return switchProfile(argv, app);
|
|
66
|
+
} else if (action === 'logout') {
|
|
67
|
+
return logoutProfile(argv, app);
|
|
63
68
|
} else if (action === 'remove') {
|
|
64
|
-
|
|
65
|
-
await removeProfile(app, name);
|
|
69
|
+
return removeConnection(argv, app);
|
|
66
70
|
} else if (action === 'modify') {
|
|
67
|
-
|
|
71
|
+
return modifyProfile(app, argv);
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
// `jsn setup` is the interactive front door for managing instances.
|
|
72
76
|
// The granular, scriptable surface lives under `jsn auth` — this command
|
|
73
|
-
// dispatches to the
|
|
74
|
-
// removeProfile, modifyProfile) so the two surfaces can never drift.
|
|
77
|
+
// collects choices and dispatches to the same auth operations.
|
|
75
78
|
export function setupCmd(wrap) {
|
|
76
79
|
return {
|
|
77
80
|
command: 'setup',
|
|
78
81
|
describe: 'Set up and manage your ServiceNow instances (interactive)',
|
|
79
82
|
handler: wrap(async (argv, app) => {
|
|
80
|
-
|
|
83
|
+
const { canUsePicker } = await import('../helpers.js');
|
|
84
|
+
if (!app.isInteractive() || !canUsePicker(app, argv)) {
|
|
81
85
|
throw errUsage('"jsn setup" is interactive. Use the specific commands:\n jsn auth login <instance>\n jsn auth refresh\n jsn auth status');
|
|
82
86
|
}
|
|
83
87
|
const profileNames = Object.keys(app.config.profiles || {});
|
|
84
88
|
if (profileNames.length === 0) {
|
|
85
89
|
const result = await loginWizard(app, argv);
|
|
86
|
-
|
|
90
|
+
await showAuthStatus({ ...argv, profile: result.profileName }, app);
|
|
87
91
|
return;
|
|
88
92
|
}
|
|
89
93
|
const action = await authHubMenu();
|
|
90
94
|
if (action === undefined) return;
|
|
91
|
-
await dispatchSetupAction(app, argv, action);
|
|
95
|
+
const result = await dispatchSetupAction(app, argv, action);
|
|
96
|
+
if (action !== 'status') await showAuthStatus({ ...argv, profile: result?.profile || result?.profileName }, app);
|
|
92
97
|
}),
|
|
93
98
|
};
|
|
94
99
|
}
|
package/src/commands/skill.js
CHANGED
|
@@ -333,7 +333,7 @@ export function skillCmd(wrap) {
|
|
|
333
333
|
? `Where should the jsn skill be installed? (${outdated.length} outdated)`
|
|
334
334
|
: 'Where should the jsn skill be installed?';
|
|
335
335
|
|
|
336
|
-
const { checkbox } = await import('
|
|
336
|
+
const { checkbox } = await import('../prompts.js');
|
|
337
337
|
const choices = Object.entries(TARGET_NAMES).map(([key, label]) => {
|
|
338
338
|
const dir = AGENT_SKILL_DIRS[key];
|
|
339
339
|
const targetPath = key === 'cursor'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkbox, select, confirm } from '
|
|
1
|
+
import { checkbox, select, confirm } from '../prompts.js';
|
|
2
2
|
import { canUsePicker, interactiveList, getStringField } from '../helpers.js';
|
|
3
3
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
4
|
import { errUsage } from '../errors.js';
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { editor, confirm } from '
|
|
2
|
+
import { editor, confirm } from '../prompts.js';
|
|
3
3
|
import { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch, canPrompt, buildRerunCommand, verifyWriteBack } from '../helpers.js';
|
|
4
4
|
import { getActiveProfile } from '../config.js';
|
|
5
5
|
import { FormatAuto } from '../output.js';
|
|
6
6
|
import { getCurrentApplication, getCurrentUpdateSet, requireCurrentUserSysId, setCurrentApplication, setCurrentUpdateSet } from '../context.js';
|
|
7
7
|
import { errAPI, errUsage, errConfirmationRequired } from '../errors.js';
|
|
8
8
|
import { declareCapabilities } from '../capabilities.js';
|
|
9
|
+
import { resolveScope } from './scopes.js';
|
|
10
|
+
import { unwrapSysId } from '../resolve-record.js';
|
|
9
11
|
|
|
10
12
|
declareCapabilities('updatesets', {
|
|
11
13
|
mutationSubcommands: ['create', 'set', 'complete', 'ignore', 'parent'],
|
|
@@ -612,12 +614,22 @@ export function updateSetsCmd(wrap) {
|
|
|
612
614
|
describe: 'Create a new update set (auto-sets as current)',
|
|
613
615
|
builder: (y) => y
|
|
614
616
|
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Update set name' })
|
|
615
|
-
.option('description', { type: 'string', describe: 'Description' })
|
|
617
|
+
.option('description', { type: 'string', describe: 'Description' })
|
|
618
|
+
.option('scope', { type: 'string', describe: 'Switch to this application before creating (scope value, sys_id, or name)' }),
|
|
616
619
|
handler: wrap(async (argv, app) => {
|
|
620
|
+
let scopeSysID;
|
|
621
|
+
if (argv.scope !== undefined) {
|
|
622
|
+
if (!String(argv.scope).trim()) throw errUsage('Scope must not be empty');
|
|
623
|
+
const scope = await resolveScope(app.sdk, argv.scope);
|
|
624
|
+
scopeSysID = unwrapSysId(scope);
|
|
625
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
626
|
+
await setCurrentApplication(app.sdk, userSysID, scopeSysID);
|
|
627
|
+
}
|
|
617
628
|
const record = await app.sdk.create('sys_update_set', {
|
|
618
629
|
name: argv.name,
|
|
619
630
|
description: argv.description || argv.name,
|
|
620
631
|
state: 'in progress',
|
|
632
|
+
...(scopeSysID ? { application: scopeSysID } : {}),
|
|
621
633
|
});
|
|
622
634
|
|
|
623
635
|
// Auto-set as current update set
|
package/src/errors.js
CHANGED
|
@@ -219,8 +219,16 @@ function resolveJsonMode(app, argv) {
|
|
|
219
219
|
* text is null when the error was rendered through app.err (envelope).
|
|
220
220
|
*/
|
|
221
221
|
export function renderAppError(err, { app = null, argv = null, identifier = '' } = {}) {
|
|
222
|
+
if (err.name === 'PromptCancelled') return { stream: 'stderr', text: 'Cancelled.\n', exitCode: 0 };
|
|
222
223
|
const code = err.code || 'unknown';
|
|
223
224
|
|
|
225
|
+
if (code === CodeAuth) {
|
|
226
|
+
if (resolveJsonMode(app, argv) && app) return { stream: 'stdout', text: null, exitCode: 1, appErr: true };
|
|
227
|
+
let text = `Error (auth): ${err.message}\n`;
|
|
228
|
+
if (err.hint) text += `\nHint: ${err.hint}\n`;
|
|
229
|
+
return { stream: 'stderr', text, exitCode: 1 };
|
|
230
|
+
}
|
|
231
|
+
|
|
224
232
|
if (code === CodeConfirmationRequired) {
|
|
225
233
|
// Structured error for AI agents / piped consumers: the JSON envelope
|
|
226
234
|
// goes to stdout (where tooling reads) so the agent can surface the
|