@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.45",
3
+ "version": "4.3.8-dev.46",
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": {
@@ -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 { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch } from '../helpers.js';
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: 'Mark the current update set as complete',
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 userSysID = await requireCurrentUserSysId(app.sdk);
638
- const current = await getCurrentUpdateSet(app.sdk, userSysID);
639
- if (!current?.sys_id) {
640
- throw errUsage('No current update set. Set one first:\n jsn updatesets set');
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
- await app.sdk.update('sys_update_set', current.sys_id, { state: 'complete' });
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 Mark the current update set as 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.');