@jacebenson/jsn 4.0.3 → 4.1.0
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/package.json +1 -3
- package/src/app.js +25 -3
- package/src/cli.js +17 -1
- package/src/commands/_ticket.js +3 -3
- package/src/commands/auth.js +116 -7
- package/src/commands/catalog.js +66 -4
- package/src/commands/dev/_generic.js +3 -3
- package/src/commands/dev/domains.js +292 -0
- package/src/commands/dev/eval.js +27 -5
- package/src/commands/dev/scopes.js +88 -36
- package/src/commands/dev/updatesets.js +297 -106
- package/src/commands/dev.js +16 -1
- package/src/commands/records.js +23 -5
- package/src/context.js +52 -5
- package/src/errors.js +15 -0
- package/src/help.js +16 -2
- package/src/helpers.js +110 -5
- package/src/mutations.js +10 -1
- package/src/output.js +8 -3
- package/src/sdk.js +4 -0
|
@@ -1,5 +1,184 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch } from '../../helpers.js';
|
|
2
|
+
import { getCurrentApplication, getCurrentUpdateSet, requireCurrentUserSysId, setCurrentApplication, setCurrentUpdateSet } from '../../context.js';
|
|
3
|
+
import { errUsage } from '../../errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scope-mismatch check for an update set vs the current app.
|
|
7
|
+
* The update set's `application` is a sys_app reference — its display is the
|
|
8
|
+
* app NAME ("test"), while the current app resolves to the scope VALUE
|
|
9
|
+
* ("x_8821_test"). Compare by app sys_id: same sys_id = same scope.
|
|
10
|
+
* @param {object} updateSetApp raw application field ({display_value, value} or string)
|
|
11
|
+
* @param {object} currentApp { scope, appSysId } from getCurrentApplication
|
|
12
|
+
* @returns {string} warning text, or '' when scopes match / can't determine
|
|
13
|
+
*/
|
|
14
|
+
export function scopeMismatchWarning(updateSetApp, currentApp) {
|
|
15
|
+
if (!updateSetApp || !currentApp) return '';
|
|
16
|
+
const updateSetAppId = updateSetApp?.value || (typeof updateSetApp === 'string' ? updateSetApp : '');
|
|
17
|
+
if (!updateSetAppId || !currentApp.appSysId || updateSetAppId === currentApp.appSysId) return '';
|
|
18
|
+
const name = updateSetApp?.display_value || updateSetApp;
|
|
19
|
+
return ` ⚠️ update set is in app "${name}", but current app is "${currentApp.scope}" — changes won't land here. Run: jsn scopes set ${name}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Format a picker label for an update set: "Name [State] (app/scope)". */
|
|
23
|
+
export function formatUpdateSetLabel(r) {
|
|
24
|
+
const appName = getStringField(r, 'application');
|
|
25
|
+
const scopeName = getStringField(r, 'application.scope');
|
|
26
|
+
const scope = appName && scopeName ? `${appName}/${scopeName}` : (appName || scopeName);
|
|
27
|
+
return `${getStringField(r, 'name')} [${getStringField(r, 'state') || '?'}] (${scope || '?'})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an update set by name, disambiguating duplicate names via the
|
|
32
|
+
* current application scope. Every scope has its own "Default" update set,
|
|
33
|
+
* so a bare name lookup must prefer the set in the current scope and only
|
|
34
|
+
* error when it's still ambiguous.
|
|
35
|
+
* @returns {object} resolved record ({sys_id,name,application,state,...})
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveUpdateSetByName(app, name) {
|
|
38
|
+
assertSafeExactMatch(name);
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
params.set('sysparm_query', `name=${name}`);
|
|
41
|
+
params.set('sysparm_limit', '50');
|
|
42
|
+
params.set('sysparm_display_value', 'all');
|
|
43
|
+
params.set('sysparm_fields', 'sys_id,name,application,state');
|
|
44
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
45
|
+
if (records.length === 0) {
|
|
46
|
+
throw new Error(`Update set not found: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
if (records.length === 1) return records[0];
|
|
49
|
+
|
|
50
|
+
// Multiple sets share the name — prefer the one in the current scope.
|
|
51
|
+
try {
|
|
52
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
53
|
+
const currentApp = await getCurrentApplication(app.sdk, userSysID);
|
|
54
|
+
const appIdOf = (r) => r.application?.value || (typeof r.application === 'string' ? r.application : '');
|
|
55
|
+
const inScope = records.find((r) => {
|
|
56
|
+
const appId = appIdOf(r);
|
|
57
|
+
return appId && currentApp.appSysId && appId === currentApp.appSysId;
|
|
58
|
+
});
|
|
59
|
+
if (inScope) return inScope;
|
|
60
|
+
} catch {
|
|
61
|
+
// fall through to ambiguity error
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scopes = records.map((r) => r.application?.display_value || r.application || '?').join(', ');
|
|
65
|
+
throw errUsage(`Multiple update sets named "${name}" (${records.length}): ${scopes}.\nRun bare "jsn updatesets set" and pick by scope.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rich display for an update set: header fields + child update filenames.
|
|
70
|
+
* Children (sys_update_xml) listed filename-only, sorted by sys_updated_on
|
|
71
|
+
* descending (newest first). Shared by `list` pick and `show`.
|
|
72
|
+
*
|
|
73
|
+
* Fetches an enriched record itself (sysparm_display_value=all so the parent
|
|
74
|
+
* reference renders as a name, not a sys_id) — callers can pass a thin
|
|
75
|
+
* picker record; the formatter completes it.
|
|
76
|
+
*/
|
|
77
|
+
export async function formatUpdateSetDetail(app, record) {
|
|
78
|
+
const sysID = getStringField(record, 'sys_id');
|
|
79
|
+
const name = getStringField(record, 'name');
|
|
80
|
+
const instance = app.getEffectiveInstance();
|
|
81
|
+
const link = `${instance}/sys_update_set.do?sys_id=${sysID}`;
|
|
82
|
+
|
|
83
|
+
let full = record;
|
|
84
|
+
try {
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
87
|
+
params.set('sysparm_limit', '1');
|
|
88
|
+
params.set('sysparm_display_value', 'all');
|
|
89
|
+
params.set('sysparm_fields', 'sys_id,name,state,application,application.scope,parent,sys_created_on,sys_updated_on');
|
|
90
|
+
const recs = await app.sdk.list('sys_update_set', params);
|
|
91
|
+
if (Array.isArray(recs) && recs.length > 0) full = recs[0];
|
|
92
|
+
} catch {
|
|
93
|
+
// fall back to the passed record
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const state = getStringField(full, 'state') || '?';
|
|
97
|
+
const appName = getStringField(full, 'application') || '';
|
|
98
|
+
const scopeName = getStringField(full, 'application.scope') || '';
|
|
99
|
+
const application = appName && scopeName ? `${appName}/${scopeName}` : (appName || scopeName);
|
|
100
|
+
const parent = getStringField(full, 'parent') || '';
|
|
101
|
+
const created = getStringField(full, 'sys_created_on') || '';
|
|
102
|
+
const updated = getStringField(full, 'sys_updated_on') || '';
|
|
103
|
+
|
|
104
|
+
// Child updates: filename only, newest first by sys_updated_on
|
|
105
|
+
let children = [];
|
|
106
|
+
try {
|
|
107
|
+
const params = new URLSearchParams();
|
|
108
|
+
params.set('sysparm_query', `update_set=${sysID}^ORDERBYDESCsys_updated_on`);
|
|
109
|
+
params.set('sysparm_limit', '500');
|
|
110
|
+
params.set('sysparm_fields', 'name,sys_updated_on');
|
|
111
|
+
const updates = await app.sdk.list('sys_update_xml', params);
|
|
112
|
+
if (Array.isArray(updates)) {
|
|
113
|
+
children = updates.map(u => getStringField(u, 'name')).filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// children are best-effort — a broken query shouldn't kill the display
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const lines = [];
|
|
120
|
+
lines.push(`Update set: ${name}`);
|
|
121
|
+
lines.push(` State: ${state}`);
|
|
122
|
+
if (application) lines.push(` Scope: ${application}`);
|
|
123
|
+
if (parent) lines.push(` Parent: ${parent}`);
|
|
124
|
+
if (created) lines.push(` Created: ${created}`);
|
|
125
|
+
if (updated) lines.push(` Updated: ${updated}`);
|
|
126
|
+
lines.push(` Updates: ${children.length}`);
|
|
127
|
+
for (const c of children) {
|
|
128
|
+
lines.push(` ${c}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push(` Link: ${link}`);
|
|
131
|
+
|
|
132
|
+
// State-aware action hints (rendered as breadcrumbs under the detail).
|
|
133
|
+
// Closed sets (complete/committed) cannot be set as current.
|
|
134
|
+
const stateRaw = String(full.state?.value ?? full.state ?? '').toLowerCase();
|
|
135
|
+
const isClosed = ['complete', 'committed'].includes(stateRaw);
|
|
136
|
+
const hints = [];
|
|
137
|
+
if (isClosed) {
|
|
138
|
+
hints.push({
|
|
139
|
+
action: 'set',
|
|
140
|
+
cmd: '',
|
|
141
|
+
description: 'Cannot set as current — update set is closed (complete/committed)',
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
hints.push({
|
|
145
|
+
action: 'set',
|
|
146
|
+
cmd: `jsn updatesets set "${name}"`,
|
|
147
|
+
description: 'Set as current update set',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
hints.push({
|
|
151
|
+
action: 'parent',
|
|
152
|
+
cmd: `jsn updatesets parent "${name}" --parent "<parent set name>"`,
|
|
153
|
+
description: 'Set the parent of this update set',
|
|
154
|
+
});
|
|
155
|
+
if (!isClosed) {
|
|
156
|
+
hints.push({
|
|
157
|
+
action: 'complete',
|
|
158
|
+
cmd: 'jsn updatesets complete',
|
|
159
|
+
description: 'Mark the current update set as complete (set it first with jsn updatesets set)',
|
|
160
|
+
});
|
|
161
|
+
hints.push({
|
|
162
|
+
action: 'ignore',
|
|
163
|
+
cmd: 'jsn updatesets ignore',
|
|
164
|
+
description: 'Ignore the current update set (won\'t be installed)',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
sys_id: sysID,
|
|
170
|
+
name,
|
|
171
|
+
state,
|
|
172
|
+
application,
|
|
173
|
+
parent,
|
|
174
|
+
sys_created_on: created,
|
|
175
|
+
sys_updated_on: updated,
|
|
176
|
+
children,
|
|
177
|
+
link,
|
|
178
|
+
_formatted: lines.join('\n'),
|
|
179
|
+
hints,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
3
182
|
|
|
4
183
|
export function updateSetsCmd(wrap) {
|
|
5
184
|
return {
|
|
@@ -11,23 +190,23 @@ export function updateSetsCmd(wrap) {
|
|
|
11
190
|
.command({
|
|
12
191
|
command: 'list',
|
|
13
192
|
aliases: ['ls'],
|
|
14
|
-
describe: 'List update sets',
|
|
193
|
+
describe: 'List update sets (shows the application scope each belongs to)',
|
|
15
194
|
builder: (y) => y
|
|
16
195
|
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
17
196
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
18
197
|
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
19
198
|
handler: wrap(async (argv, app) => {
|
|
20
|
-
const columns = argv.columns ? argv.columns.split(',') : ['name', 'state', 'application'];
|
|
199
|
+
const columns = argv.columns ? argv.columns.split(',') : ['name', 'state', 'application', 'application.scope'];
|
|
21
200
|
const query = argv.query || '';
|
|
22
201
|
|
|
23
202
|
const picked = await interactiveList({
|
|
24
203
|
app, table: 'sys_update_set', singular: 'update set', columns, limit: argv.limit, query, labelField: 'name',
|
|
25
|
-
formatLabel:
|
|
204
|
+
formatLabel: formatUpdateSetLabel,
|
|
26
205
|
});
|
|
27
206
|
if (picked === undefined) return; // user cancelled
|
|
28
207
|
if (picked) {
|
|
29
|
-
|
|
30
|
-
return app.ok(
|
|
208
|
+
const detail = await formatUpdateSetDetail(app, picked);
|
|
209
|
+
return app.ok(detail, { summary: `Update set: ${getStringField(picked, 'name')}`, breadcrumbs: detail.hints });
|
|
31
210
|
}
|
|
32
211
|
|
|
33
212
|
const params = new URLSearchParams();
|
|
@@ -51,61 +230,102 @@ export function updateSetsCmd(wrap) {
|
|
|
51
230
|
aliases: ['get'],
|
|
52
231
|
describe: 'Show an update set',
|
|
53
232
|
handler: wrap(async (argv, app) => {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
params.set('sysparm_display_value', 'all');
|
|
58
|
-
const records = await app.sdk.list('sys_update_set', params);
|
|
59
|
-
if (records.length === 0) {
|
|
60
|
-
throw new Error(`Update set not found: ${argv.name}`);
|
|
61
|
-
}
|
|
62
|
-
app.ok(records[0], { summary: `Update set ${argv.name}` });
|
|
233
|
+
const record = await resolveUpdateSetByName(app, argv.name);
|
|
234
|
+
const detail = await formatUpdateSetDetail(app, record);
|
|
235
|
+
app.ok(detail, { summary: `Update set ${argv.name}`, breadcrumbs: detail.hints });
|
|
63
236
|
}),
|
|
64
237
|
})
|
|
65
238
|
.command({
|
|
66
|
-
command: 'set
|
|
67
|
-
describe: 'Set the current update set',
|
|
239
|
+
command: 'set [name]',
|
|
240
|
+
describe: 'Set the current update set (interactive picker with scope when run bare)',
|
|
68
241
|
handler: wrap(async (argv, app) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
242
|
+
let name = argv.name;
|
|
243
|
+
let sysID = null;
|
|
244
|
+
let updateSetApp = null; // raw application field ({display_value, value} or string)
|
|
245
|
+
let stateRaw = '';
|
|
246
|
+
if (!name) {
|
|
247
|
+
const picked = await interactiveList({
|
|
248
|
+
app, table: 'sys_update_set', singular: 'update set', columns: ['name', 'state', 'application', 'application.scope'], labelField: 'name',
|
|
249
|
+
formatLabel: formatUpdateSetLabel,
|
|
250
|
+
});
|
|
251
|
+
if (!picked) return; // cancelled or non-interactive
|
|
252
|
+
// Picker returns the full record — use its sys_id directly.
|
|
253
|
+
// Do NOT re-query by name: duplicate names (e.g. "Default")
|
|
254
|
+
// would resolve to the wrong record.
|
|
255
|
+
name = getStringField(picked, 'name');
|
|
256
|
+
sysID = getStringField(picked, 'sys_id');
|
|
257
|
+
updateSetApp = picked.application;
|
|
258
|
+
stateRaw = String(picked.state?.value ?? picked.state ?? '').toLowerCase();
|
|
76
259
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
sysparm_fields: 'sys_id',
|
|
83
|
-
}));
|
|
84
|
-
if (user.length === 0) {
|
|
85
|
-
throw new Error('Could not determine current user');
|
|
260
|
+
if (!sysID) {
|
|
261
|
+
const resolved = await resolveUpdateSetByName(app, name);
|
|
262
|
+
sysID = getStringField(resolved, 'sys_id');
|
|
263
|
+
updateSetApp = resolved.application;
|
|
264
|
+
stateRaw = String(resolved.state?.value ?? resolved.state ?? '').toLowerCase();
|
|
86
265
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
266
|
+
if (['complete', 'ignore'].includes(stateRaw)) {
|
|
267
|
+
throw errUsage(`Cannot set "${name}" as current — update set is ${stateRaw === 'ignore' ? 'ignored' : 'complete'}.\nView it instead: jsn updatesets show "${name}"`);
|
|
268
|
+
}
|
|
269
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
270
|
+
await setCurrentUpdateSet(app.sdk, userSysID, sysID);
|
|
271
|
+
|
|
272
|
+
// Like the platform update set picker: switching the set also
|
|
273
|
+
// switches the application scope into the set's scope. The
|
|
274
|
+
// application field is a sys_scope reference, so its raw value
|
|
275
|
+
// IS the scope sys_id the apps.current_app preference stores.
|
|
276
|
+
let scopeNote = '';
|
|
277
|
+
try {
|
|
278
|
+
const scopeSysId = updateSetApp?.value || (typeof updateSetApp === 'string' ? updateSetApp : '');
|
|
279
|
+
if (scopeSysId) {
|
|
280
|
+
await setCurrentApplication(app.sdk, userSysID, scopeSysId);
|
|
281
|
+
const scopeName = updateSetApp?.display_value || updateSetApp;
|
|
282
|
+
scopeNote = ` — scope switched to ${scopeName}`;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// scope switch failed — fall back to a warning
|
|
286
|
+
const currentApp = await getCurrentApplication(app.sdk, userSysID).catch(() => null);
|
|
287
|
+
scopeNote = scopeMismatchWarning(updateSetApp, currentApp);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
app.ok({ update_set: name, sys_id: sysID }, { summary: `Current update set: ${name}${scopeNote}` });
|
|
291
|
+
}),
|
|
292
|
+
})
|
|
293
|
+
.command({
|
|
294
|
+
command: 'parent <name>',
|
|
295
|
+
describe: 'Set the parent of an update set',
|
|
296
|
+
builder: (y) => y
|
|
297
|
+
.positional('name', { describe: 'Update set name', type: 'string' })
|
|
298
|
+
.option('parent', { type: 'string', describe: 'Parent update set name' }),
|
|
299
|
+
handler: wrap(async (argv, app) => {
|
|
300
|
+
const child = await resolveUpdateSetByName(app, argv.name);
|
|
301
|
+
const sysID = getStringField(child, 'sys_id');
|
|
302
|
+
const childScope = getStringField(child, 'application');
|
|
303
|
+
|
|
304
|
+
let parentName = argv.parent;
|
|
305
|
+
if (!parentName) {
|
|
306
|
+
const picked = await interactiveList({
|
|
307
|
+
app, table: 'sys_update_set', singular: 'parent update set', columns: ['name', 'state', 'application', 'application.scope'], labelField: 'name',
|
|
308
|
+
formatLabel: formatUpdateSetLabel,
|
|
101
309
|
});
|
|
310
|
+
if (!picked) return; // cancelled or non-interactive
|
|
311
|
+
parentName = getStringField(picked, 'name');
|
|
312
|
+
}
|
|
313
|
+
const parentRecord = await resolveUpdateSetByName(app, parentName);
|
|
314
|
+
const parentSysID = getStringField(parentRecord, 'sys_id');
|
|
315
|
+
const parentScope = getStringField(parentRecord, 'application');
|
|
316
|
+
|
|
317
|
+
let warn = '';
|
|
318
|
+
if (childScope && parentScope && childScope !== parentScope) {
|
|
319
|
+
warn = ` ⚠️ child is in scope "${childScope}", parent is in scope "${parentScope}" — allowed, but they won't commit as one unit`;
|
|
102
320
|
}
|
|
103
|
-
|
|
321
|
+
|
|
322
|
+
await app.sdk.update('sys_update_set', sysID, { parent: parentSysID });
|
|
323
|
+
app.ok({ update_set: argv.name, parent: parentName }, { summary: `Parent of "${argv.name}" is now "${parentName}"${warn}` });
|
|
104
324
|
}),
|
|
105
325
|
})
|
|
106
326
|
.command({
|
|
107
327
|
command: 'create',
|
|
108
|
-
describe: 'Create a new update set',
|
|
328
|
+
describe: 'Create a new update set (auto-sets as current)',
|
|
109
329
|
builder: (y) => y
|
|
110
330
|
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Update set name' })
|
|
111
331
|
.option('description', { type: 'string', describe: 'Description' }),
|
|
@@ -119,30 +339,8 @@ export function updateSetsCmd(wrap) {
|
|
|
119
339
|
// Auto-set as current update set
|
|
120
340
|
const sysID = record?.sys_id?.value || record?.sys_id;
|
|
121
341
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
sysparm_limit: '1',
|
|
125
|
-
sysparm_fields: 'sys_id',
|
|
126
|
-
}));
|
|
127
|
-
if (user.length > 0) {
|
|
128
|
-
const userSysID = user[0].sys_id?.value || user[0].sys_id;
|
|
129
|
-
// Check for existing preference
|
|
130
|
-
const prefParams = new URLSearchParams();
|
|
131
|
-
prefParams.set('sysparm_query', `user=${userSysID}^name=sys_update_set`);
|
|
132
|
-
prefParams.set('sysparm_limit', '1');
|
|
133
|
-
const prefs = await app.sdk.list('sys_user_preference', prefParams);
|
|
134
|
-
if (prefs.length > 0) {
|
|
135
|
-
const prefID = prefs[0].sys_id?.value || prefs[0].sys_id;
|
|
136
|
-
await app.sdk.update('sys_user_preference', prefID, { value: sysID });
|
|
137
|
-
} else {
|
|
138
|
-
await app.sdk.create('sys_user_preference', {
|
|
139
|
-
user: userSysID,
|
|
140
|
-
name: 'sys_update_set',
|
|
141
|
-
value: sysID,
|
|
142
|
-
type: 'string',
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
342
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
343
|
+
await setCurrentUpdateSet(app.sdk, userSysID, sysID);
|
|
146
344
|
} catch {
|
|
147
345
|
// Non-fatal — auto-set is a convenience, not mandatory
|
|
148
346
|
}
|
|
@@ -165,37 +363,29 @@ export function updateSetsCmd(wrap) {
|
|
|
165
363
|
}),
|
|
166
364
|
})
|
|
167
365
|
.command({
|
|
168
|
-
command: '
|
|
169
|
-
describe: '
|
|
170
|
-
builder: (y) => y
|
|
171
|
-
.positional('name', {
|
|
172
|
-
describe: 'Update set name',
|
|
173
|
-
type: 'string',
|
|
174
|
-
})
|
|
175
|
-
.option('output', {
|
|
176
|
-
alias: 'o',
|
|
177
|
-
type: 'string',
|
|
178
|
-
describe: 'Output file path (default: stdout)',
|
|
179
|
-
}),
|
|
366
|
+
command: 'complete',
|
|
367
|
+
describe: 'Mark the current update set as complete',
|
|
180
368
|
handler: wrap(async (argv, app) => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const records = await app.sdk.list('sys_update_set', params);
|
|
186
|
-
if (records.length === 0) {
|
|
187
|
-
throw new Error(`Update set not found: ${argv.name}`);
|
|
369
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
370
|
+
const current = await getCurrentUpdateSet(app.sdk, userSysID);
|
|
371
|
+
if (!current?.sys_id) {
|
|
372
|
+
throw errUsage('No current update set. Set one first:\n jsn updatesets set');
|
|
188
373
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
374
|
+
await app.sdk.update('sys_update_set', current.sys_id, { state: 'complete' });
|
|
375
|
+
app.ok({ update_set: current.name, state: 'complete' }, { summary: `Update set marked complete: ${current.name}` });
|
|
376
|
+
}),
|
|
377
|
+
})
|
|
378
|
+
.command({
|
|
379
|
+
command: 'ignore',
|
|
380
|
+
describe: 'Ignore the current update set (won\'t be installed)',
|
|
381
|
+
handler: wrap(async (argv, app) => {
|
|
382
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
383
|
+
const current = await getCurrentUpdateSet(app.sdk, userSysID);
|
|
384
|
+
if (!current?.sys_id) {
|
|
385
|
+
throw errUsage('No current update set. Set one first:\n jsn updatesets set');
|
|
198
386
|
}
|
|
387
|
+
await app.sdk.update('sys_update_set', current.sys_id, { state: 'ignore' });
|
|
388
|
+
app.ok({ update_set: current.name, state: 'ignore' }, { summary: `Update set ignored: ${current.name}` });
|
|
199
389
|
}),
|
|
200
390
|
})
|
|
201
391
|
|
|
@@ -204,12 +394,13 @@ export function updateSetsCmd(wrap) {
|
|
|
204
394
|
if (!argv._[1]) {
|
|
205
395
|
console.log('Manage ServiceNow update sets.\n');
|
|
206
396
|
console.log('Commands:');
|
|
207
|
-
console.log(' list List update sets');
|
|
397
|
+
console.log(' list List update sets (shows scope)');
|
|
208
398
|
console.log(' show <name> Show an update set');
|
|
209
|
-
console.log(' set
|
|
399
|
+
console.log(' set [name] Set the current update set (picker when run bare)');
|
|
210
400
|
console.log(' create Create a new update set (auto-sets as current)');
|
|
211
|
-
console.log('
|
|
212
|
-
console.log('
|
|
401
|
+
console.log(' complete Mark the current update set as complete');
|
|
402
|
+
console.log(' ignore Ignore the current update set');
|
|
403
|
+
console.log(' parent <name> Set the parent of an update set');
|
|
213
404
|
console.log('\nRun "jsn updatesets <command> --help" for details.');
|
|
214
405
|
console.log('\nTip: Create an update set first:');
|
|
215
406
|
console.log(' jsn updatesets create --name "My Feature"');
|
package/src/commands/dev.js
CHANGED
|
@@ -15,6 +15,7 @@ import { formsCmd } from './dev/forms.js';
|
|
|
15
15
|
import { listsCmd } from './dev/lists.js';
|
|
16
16
|
import { updateSetsCmd } from './dev/updatesets.js';
|
|
17
17
|
import { scopesCmd } from './dev/scopes.js';
|
|
18
|
+
import { domainsCmd } from './dev/domains.js';
|
|
18
19
|
import { evalCmd } from './dev/eval.js';
|
|
19
20
|
import { restCmd } from './dev/rest.js';
|
|
20
21
|
import { logsCmd } from './dev/logs.js';
|
|
@@ -47,6 +48,7 @@ export function devCmd(wrap) {
|
|
|
47
48
|
.command(rolesCmd(wrap))
|
|
48
49
|
.command(updateSetsCmd(wrap))
|
|
49
50
|
.command(scopesCmd(wrap))
|
|
51
|
+
.command(domainsCmd(wrap))
|
|
50
52
|
.command(propertiesCmd(wrap))
|
|
51
53
|
.command(relationshipsCmd(wrap))
|
|
52
54
|
.command(appmodulesCmd(wrap))
|
|
@@ -61,6 +63,19 @@ export function devCmd(wrap) {
|
|
|
61
63
|
.command(evalCmd(wrap))
|
|
62
64
|
.command(restCmd(wrap));
|
|
63
65
|
},
|
|
64
|
-
handler: () => {
|
|
66
|
+
handler: (argv) => {
|
|
67
|
+
if (argv._[1]) return; // a subcommand ran — its own handler handled it
|
|
68
|
+
console.log('Manage ServiceNow development artifacts.');
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Most dev commands are also available at the top level:');
|
|
71
|
+
console.log(' flows, actions, rules, clientscripts, uiactions, uipolicies,');
|
|
72
|
+
console.log(' tables, columns, forms, lists, import, sppages, spwidgets,');
|
|
73
|
+
console.log(' uipages, appmenu, scrapi, acls, roles, properties,');
|
|
74
|
+
console.log(' relationships, appmodules, listcontrols, views, privileges,');
|
|
75
|
+
console.log(' uxscripts, aliases, catalogscripts, cataloguipolicies, logs,');
|
|
76
|
+
console.log(' updatesets, scopes, eval, rest');
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log('Run "jsn dev <command> --help" or "jsn <command> --help" for details.');
|
|
79
|
+
},
|
|
65
80
|
};
|
|
66
81
|
}
|
package/src/commands/records.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, assertSafeExactMatch } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, assertSafeExactMatch, verifyWriteBack } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
const tableDefaultColumns = {
|
|
4
4
|
incident: ['number', 'short_description', 'priority', 'state', 'assigned_to'],
|
|
@@ -21,7 +21,7 @@ function getDefaultColumns(table) {
|
|
|
21
21
|
|
|
22
22
|
export function recordsCmd(wrap) {
|
|
23
23
|
return {
|
|
24
|
-
command: 'records
|
|
24
|
+
command: 'records [subcommand]',
|
|
25
25
|
describe: 'Query and manage records in any table (e.g. "records list --table incident --query priority=1")',
|
|
26
26
|
builder: (yargs) => {
|
|
27
27
|
return yargs
|
|
@@ -135,7 +135,8 @@ export function recordsCmd(wrap) {
|
|
|
135
135
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
136
136
|
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
137
137
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
138
|
-
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
138
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
139
|
+
.option('strict', { type: 'boolean', default: false, describe: 'Exit non-zero if any supplied field was not persisted (write-back check)' }),
|
|
139
140
|
handler: wrap(async (argv, app) => {
|
|
140
141
|
const recordData = parseDataArg(argv);
|
|
141
142
|
const warnings = checkDerivedFields(argv.table, recordData);
|
|
@@ -143,6 +144,14 @@ export function recordsCmd(wrap) {
|
|
|
143
144
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
144
145
|
}
|
|
145
146
|
const record = await app.sdk.create(argv.table, recordData);
|
|
147
|
+
const sysID = getStringField(record, 'sys_id');
|
|
148
|
+
const mismatches = await verifyWriteBack(app, argv.table, sysID, recordData);
|
|
149
|
+
for (const m of mismatches) {
|
|
150
|
+
process.stderr.write(`⚠ field "${m.field}" was not persisted (sent: "${m.sent}", got: "${m.got}")\n`);
|
|
151
|
+
}
|
|
152
|
+
if (mismatches.length > 0 && argv.strict) {
|
|
153
|
+
throw new Error(`Strict mode: ${mismatches.length} field(s) not persisted after create`);
|
|
154
|
+
}
|
|
146
155
|
app.ok(record, { summary: `Created record in ${argv.table}` });
|
|
147
156
|
}),
|
|
148
157
|
})
|
|
@@ -154,7 +163,8 @@ export function recordsCmd(wrap) {
|
|
|
154
163
|
.option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' })
|
|
155
164
|
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
156
165
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
157
|
-
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
166
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
167
|
+
.option('strict', { type: 'boolean', default: false, describe: 'Exit non-zero if any supplied field was not persisted (write-back check)' }),
|
|
158
168
|
handler: wrap(async (argv, app) => {
|
|
159
169
|
const recordData = parseDataArg(argv);
|
|
160
170
|
const warnings = checkDerivedFields(argv.table, recordData);
|
|
@@ -162,6 +172,13 @@ export function recordsCmd(wrap) {
|
|
|
162
172
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
163
173
|
}
|
|
164
174
|
const record = await app.sdk.update(argv.table, argv['sys-id'], recordData);
|
|
175
|
+
const mismatches = await verifyWriteBack(app, argv.table, argv['sys-id'], recordData);
|
|
176
|
+
for (const m of mismatches) {
|
|
177
|
+
process.stderr.write(`⚠ field "${m.field}" was not persisted (sent: "${m.sent}", got: "${m.got}")\n`);
|
|
178
|
+
}
|
|
179
|
+
if (mismatches.length > 0 && argv.strict) {
|
|
180
|
+
throw new Error(`Strict mode: ${mismatches.length} field(s) not persisted after update`);
|
|
181
|
+
}
|
|
165
182
|
app.ok(record, { summary: `Updated record in ${argv.table}` });
|
|
166
183
|
}),
|
|
167
184
|
})
|
|
@@ -208,7 +225,8 @@ export function recordsCmd(wrap) {
|
|
|
208
225
|
})
|
|
209
226
|
|
|
210
227
|
},
|
|
211
|
-
handler: () => {
|
|
228
|
+
handler: (argv) => {
|
|
229
|
+
if (argv._[1]) return; // a subcommand ran — its own handler handled it
|
|
212
230
|
console.log('Query and manage records in any ServiceNow table.');
|
|
213
231
|
console.log('');
|
|
214
232
|
console.log('Available subcommands:');
|