@jacebenson/jsn 4.0.2 → 4.0.4
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 +29 -7
- package/package.json +3 -4
- package/src/app.js +25 -3
- package/src/cli.js +3 -1
- package/src/commands/auth.js +222 -46
- package/src/commands/dev/domains.js +292 -0
- 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 +4 -3
- package/src/commands/setup.js +53 -17
- package/src/context.js +52 -5
- package/src/help.js +18 -3
- package/src/mutations.js +10 -1
- package/src/sdk.js +4 -0
- package/suppress-warnings.cjs +0 -13
|
@@ -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 } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, assertSafeExactMatch } 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
|
|
@@ -208,7 +208,8 @@ export function recordsCmd(wrap) {
|
|
|
208
208
|
})
|
|
209
209
|
|
|
210
210
|
},
|
|
211
|
-
handler: () => {
|
|
211
|
+
handler: (argv) => {
|
|
212
|
+
if (argv._[1]) return; // a subcommand ran — its own handler handled it
|
|
212
213
|
console.log('Query and manage records in any ServiceNow table.');
|
|
213
214
|
console.log('');
|
|
214
215
|
console.log('Available subcommands:');
|
package/src/commands/setup.js
CHANGED
|
@@ -1,25 +1,61 @@
|
|
|
1
|
-
import { loginWizard } from './auth.js';
|
|
1
|
+
import { loginWizard, modifyProfile, removeProfile, pickProfile } from './auth.js';
|
|
2
|
+
import { setActiveProfile } from '../config.js';
|
|
3
|
+
import { errUsage } from '../errors.js';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** Interactive hub: what do you want to do? Returns 'add' | 'switch' | 'remove' | 'modify'. */
|
|
6
|
+
export async function authHubMenu() {
|
|
7
|
+
const { select } = await import('@inquirer/prompts');
|
|
8
|
+
return select({
|
|
9
|
+
message: 'What would you like to do?',
|
|
10
|
+
choices: [
|
|
11
|
+
{ name: 'Add a new instance', value: 'add' },
|
|
12
|
+
{ name: 'Switch to a different instance', value: 'switch' },
|
|
13
|
+
{ name: 'Remove an instance', value: 'remove' },
|
|
14
|
+
{ name: 'Modify an instance', value: 'modify' },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dispatch a hub action to the shared operation. Kept separate from the
|
|
21
|
+
* handler so routing is testable without invoking inquirer prompts.
|
|
22
|
+
*/
|
|
23
|
+
export async function dispatchSetupAction(app, argv, action) {
|
|
24
|
+
if (action === 'add') {
|
|
25
|
+
const result = await loginWizard(app, argv);
|
|
26
|
+
app.ok({ setup: true, ...result }, { summary: 'Setup complete' });
|
|
27
|
+
} else if (action === 'switch') {
|
|
28
|
+
const name = await pickProfile(app, 'Switch to which instance?');
|
|
29
|
+
await setActiveProfile(app.config, name);
|
|
30
|
+
app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
|
|
31
|
+
} else if (action === 'remove') {
|
|
32
|
+
const name = await pickProfile(app, 'Remove which instance?');
|
|
33
|
+
await removeProfile(app, name);
|
|
34
|
+
} else { // modify
|
|
35
|
+
await modifyProfile(app, argv);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `jsn setup` is the interactive front door for managing instances.
|
|
40
|
+
// The granular, scriptable surface lives under `jsn auth` — this command
|
|
41
|
+
// dispatches to the SAME functions (loginWizard, setActiveProfile,
|
|
42
|
+
// removeProfile, modifyProfile) so the two surfaces can never drift.
|
|
6
43
|
export function setupCmd(wrap) {
|
|
7
44
|
return {
|
|
8
45
|
command: 'setup',
|
|
9
|
-
describe: '
|
|
10
|
-
hidden: true, // legacy alias for `jsn auth login` — works but hidden from help
|
|
11
|
-
builder: (yargs) => {
|
|
12
|
-
return yargs
|
|
13
|
-
.option('read-only', {
|
|
14
|
-
describe: 'Mark profile as read-only (blocks mutation commands)',
|
|
15
|
-
type: 'boolean',
|
|
16
|
-
default: false,
|
|
17
|
-
});
|
|
18
|
-
},
|
|
46
|
+
describe: 'Set up and manage your ServiceNow instances (interactive)',
|
|
19
47
|
handler: wrap(async (argv, app) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
if (!app.isInteractive()) {
|
|
49
|
+
throw errUsage('"jsn setup" is interactive. Use the specific commands:\n jsn auth login <instance>\n jsn auth refresh\n jsn auth status');
|
|
50
|
+
}
|
|
51
|
+
const profileNames = Object.keys(app.config.profiles || {});
|
|
52
|
+
if (profileNames.length === 0) {
|
|
53
|
+
const result = await loginWizard(app, argv);
|
|
54
|
+
app.ok({ setup: true, ...result }, { summary: 'Setup complete' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const action = await authHubMenu();
|
|
58
|
+
await dispatchSetupAction(app, argv, action);
|
|
23
59
|
}),
|
|
24
60
|
};
|
|
25
61
|
}
|