@jacebenson/jsn 4.3.7-dev.45 → 4.3.8-dev.46
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 +20 -0
- package/package.json +1 -1
- package/skills/servicenow/SKILL.md +16 -0
- package/src/commands/updatesets.js +77 -11
package/README.md
CHANGED
|
@@ -107,6 +107,26 @@ jsn snippets run open-inc
|
|
|
107
107
|
jsn logs follow --level error --tail 10
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Update set completion
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
jsn updatesets complete # Current set, opens a notes editor in a human terminal
|
|
114
|
+
jsn updatesets complete "My Feature" --notes "Fixed validation" --force --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`complete [name]` saves concise bullet-list notes in the selected set's Description
|
|
118
|
+
and marks it complete. Omit the name to use the current set. Explicit names
|
|
119
|
+
must be unique; for duplicate names, select the intended set with `updatesets set`
|
|
120
|
+
and complete it without a name. ServiceNow's normal batch completion rules still
|
|
121
|
+
apply: completing a parent can complete its children. Notes are not copied to children.
|
|
122
|
+
|
|
123
|
+
Existing Description text is preserved exactly. The command shows it and asks
|
|
124
|
+
before appending notes, unless `--force` or the profile's `skip_confirmations`
|
|
125
|
+
policy explicitly allows the append. Scripts and JSON output must supply
|
|
126
|
+
nonempty `--notes`; they never open prompts. Separate multiple notes with real
|
|
127
|
+
newlines. Success requires readback of both Description and state. If verification
|
|
128
|
+
fails, inspect the set before retrying because the write may have partially succeeded.
|
|
129
|
+
|
|
110
130
|
### Flow Designer actions
|
|
111
131
|
|
|
112
132
|
`actions` and `action` are the same command. `list`, `show`, and explicit
|
package/package.json
CHANGED
|
@@ -73,6 +73,22 @@ Pick the most specific tool for the job. **Never default to eval** — it's the
|
|
|
73
73
|
| **Docs** | `docs sync`, `docs status`, `docs search`, `docs serve` |
|
|
74
74
|
| **Developer** | `eval`, `rest`, `skill`, `version` |
|
|
75
75
|
|
|
76
|
+
### Complete an update set
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jsn updatesets complete "My Feature" --notes "Fixed validation" --force --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`complete [name]` defaults to the current set when the name is omitted. Explicit
|
|
83
|
+
names must be unique. Supply nonempty `--notes` in automation; real newlines
|
|
84
|
+
separate bullets. A human terminal opens a notes editor when `--notes` is absent.
|
|
85
|
+
Existing Description text is preserved and appended after confirmation. Use
|
|
86
|
+
`--force` only after approval, or the profile's `skip_confirmations` policy.
|
|
87
|
+
Notes apply only to the selected set. ServiceNow's normal batch rules can complete
|
|
88
|
+
child sets when the parent completes; their descriptions are not replaced.
|
|
89
|
+
Both selected-set Description and state are read back before success. On
|
|
90
|
+
verification failure, inspect the set before retrying.
|
|
91
|
+
|
|
76
92
|
### Table and raw REST examples
|
|
77
93
|
|
|
78
94
|
```bash
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { editor, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch, canPrompt, buildRerunCommand, verifyWriteBack } from '../helpers.js';
|
|
4
|
+
import { getActiveProfile } from '../config.js';
|
|
5
|
+
import { FormatAuto } from '../output.js';
|
|
3
6
|
import { getCurrentApplication, getCurrentUpdateSet, requireCurrentUserSysId, setCurrentApplication, setCurrentUpdateSet } from '../context.js';
|
|
4
|
-
import { errAPI, errUsage } from '../errors.js';
|
|
7
|
+
import { errAPI, errUsage, errConfirmationRequired } from '../errors.js';
|
|
5
8
|
import { declareCapabilities } from '../capabilities.js';
|
|
6
9
|
|
|
7
10
|
declareCapabilities('updatesets', {
|
|
@@ -133,6 +136,28 @@ async function fetchUpdateSetById(app, sysID) {
|
|
|
133
136
|
return Array.isArray(records) && records.length > 0 ? records[0] : null;
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
/** Completion refuses duplicate names rather than guessing a mutation target. */
|
|
140
|
+
async function resolveCompletionTarget(app, name) {
|
|
141
|
+
if (name !== undefined) {
|
|
142
|
+
if (!String(name).trim()) throw errUsage('Update set name must not be empty');
|
|
143
|
+
assertSafeExactMatch(name);
|
|
144
|
+
const params = new URLSearchParams({
|
|
145
|
+
sysparm_query: `name=${name}`, sysparm_limit: '2',
|
|
146
|
+
sysparm_display_value: 'all', sysparm_fields: 'sys_id,name',
|
|
147
|
+
});
|
|
148
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
149
|
+
if (!records.length) throw errUsage(`Update set not found: ${name}`);
|
|
150
|
+
if (records.length !== 1) {
|
|
151
|
+
throw errUsage(`Multiple update sets named "${name}". Select the intended set with "jsn updatesets set", then run "jsn updatesets complete" without a name.`);
|
|
152
|
+
}
|
|
153
|
+
return { sys_id: rawReferenceId(records[0].sys_id), name: getStringField(records[0], 'name') };
|
|
154
|
+
}
|
|
155
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
156
|
+
const current = await getCurrentUpdateSet(app.sdk, userSysID);
|
|
157
|
+
if (!current?.sys_id) throw errUsage('No current update set. Set one first:\n jsn updatesets set');
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
/** Build a safe, flat preorder hierarchy with ancestors before the selected set. */
|
|
137
162
|
export async function buildUpdateSetHierarchy(app, selectedRecord) {
|
|
138
163
|
const selectedID = getStringField(selectedRecord, 'sys_id');
|
|
@@ -631,16 +656,57 @@ export function updateSetsCmd(wrap) {
|
|
|
631
656
|
}),
|
|
632
657
|
})
|
|
633
658
|
.command({
|
|
634
|
-
command: 'complete',
|
|
635
|
-
describe: '
|
|
659
|
+
command: 'complete [name]',
|
|
660
|
+
describe: 'Complete a named or current update set with completion notes',
|
|
661
|
+
builder: (y) => y
|
|
662
|
+
.positional('name', { type: 'string', describe: 'Unique update set name, defaults to the current set' })
|
|
663
|
+
.option('notes', { type: 'string', describe: 'Completion notes, one bullet per line; opens an editor when omitted interactively' }),
|
|
636
664
|
handler: wrap(async (argv, app) => {
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
if (!current
|
|
640
|
-
|
|
665
|
+
const current = await resolveCompletionTarget(app, argv.name);
|
|
666
|
+
const record = await fetchUpdateSetById(app, current.sys_id);
|
|
667
|
+
if (!record) throw errUsage(`Update set not found or unreadable: ${current.sys_id}`);
|
|
668
|
+
if (!Object.hasOwn(record, 'description')
|
|
669
|
+
|| (record.description && typeof record.description === 'object' && !Object.hasOwn(record.description, 'value'))) {
|
|
670
|
+
throw errUsage(`Description is unreadable for update set "${current.name}" (${current.sys_id}); refusing to replace unknown text.`);
|
|
671
|
+
}
|
|
672
|
+
const existing = String(record.description && typeof record.description === 'object'
|
|
673
|
+
? record.description.value ?? '' : record.description ?? '');
|
|
674
|
+
const interactive = !argv.json && !argv.get && (!argv.format || argv.format === FormatAuto)
|
|
675
|
+
&& app.output?.getFormat() === FormatAuto && canPrompt();
|
|
676
|
+
let notes = argv.notes;
|
|
677
|
+
let description;
|
|
678
|
+
try {
|
|
679
|
+
if (notes === undefined) {
|
|
680
|
+
if (!interactive) throw errUsage('Provide nonempty completion notes with --notes in noninteractive mode');
|
|
681
|
+
notes = await editor({ message: `Completion notes for "${current.name}". Write concise bullets, one per line` });
|
|
682
|
+
}
|
|
683
|
+
const bullets = String(notes).split(/\r?\n/)
|
|
684
|
+
.map(line => line.trim().replace(/^[-*•]\s*/, '').trim()).filter(Boolean)
|
|
685
|
+
.map(line => `- ${line}`).join('\n');
|
|
686
|
+
if (!bullets) throw errUsage('Provide nonempty completion notes with --notes');
|
|
687
|
+
description = existing ? `${existing}\n\n${bullets}` : bullets;
|
|
688
|
+
if (existing && !argv.force && getActiveProfile(app.config)?.skip_confirmations !== true) {
|
|
689
|
+
const question = `Append notes and complete "${current.name}"?\nExisting Description:\n${existing}\n\nNew notes:\n${bullets}`;
|
|
690
|
+
if (!interactive) throw errConfirmationRequired(question, buildRerunCommand(argv, {
|
|
691
|
+
rerun: ['updatesets', 'complete', ...(argv.name !== undefined ? [argv.name] : []), '--notes', String(notes)],
|
|
692
|
+
}));
|
|
693
|
+
if (!await confirm({ message: question, default: false })) return;
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (['ExitPromptError', 'AbortPromptError'].includes(error.name)) return;
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
const data = { state: 'complete', description };
|
|
700
|
+
try {
|
|
701
|
+
await app.sdk.update('sys_update_set', current.sys_id, data);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
throw errAPI(error.status || 0, `Could not complete update set "${current.name}" (${current.sys_id}): ${error.message}. Check the record before retrying.`);
|
|
704
|
+
}
|
|
705
|
+
const mismatches = await verifyWriteBack(app, 'sys_update_set', current.sys_id, data);
|
|
706
|
+
if (mismatches.length) {
|
|
707
|
+
throw errAPI(200, `Could not verify completion of update set "${current.name}" (${current.sys_id}): ${mismatches.map(item => item.field).join(', ')} did not persist or could not be read. The write may have partially succeeded; inspect the set before retrying.`);
|
|
641
708
|
}
|
|
642
|
-
|
|
643
|
-
app.ok({ update_set: current.name, state: 'complete' }, { summary: `Update set marked complete: ${current.name}` });
|
|
709
|
+
app.ok({ update_set: current.name, sys_id: current.sys_id, state: 'complete', description }, { summary: `Update set marked complete: ${current.name}` });
|
|
644
710
|
}),
|
|
645
711
|
})
|
|
646
712
|
.command({
|
|
@@ -667,7 +733,7 @@ export function updateSetsCmd(wrap) {
|
|
|
667
733
|
console.log(' export <name> Export an update set to XML (stdout or --out)');
|
|
668
734
|
console.log(' set [name] Set the current update set (picker when run bare)');
|
|
669
735
|
console.log(' create Create a new update set (auto-sets as current)');
|
|
670
|
-
console.log(' complete
|
|
736
|
+
console.log(' complete [name] Complete a named or current set with --notes (editor when omitted)');
|
|
671
737
|
console.log(' ignore Ignore the current update set');
|
|
672
738
|
console.log(' parent <name> Set the parent of an update set');
|
|
673
739
|
console.log('\nRun "jsn updatesets <command> --help" for details.');
|