@jacebenson/jsn 0.0.10 → 1.0.2

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.
Files changed (41) hide show
  1. package/README.md +7 -49
  2. package/bin/jsn.js +57 -2
  3. package/package.json +28 -32
  4. package/scripts/install.sh +227 -0
  5. package/scripts/npm-install.js +235 -0
  6. package/scripts/pre-commit-check.sh +61 -0
  7. package/src/app.js +0 -157
  8. package/src/auth.js +0 -283
  9. package/src/cli.js +0 -144
  10. package/src/commands/_ticket.js +0 -256
  11. package/src/commands/auth.js +0 -62
  12. package/src/commands/changes.js +0 -7
  13. package/src/commands/dev/_generic.js +0 -223
  14. package/src/commands/dev/_simple.js +0 -89
  15. package/src/commands/dev/eval.js +0 -17
  16. package/src/commands/dev/flows.js +0 -528
  17. package/src/commands/dev/forms.js +0 -313
  18. package/src/commands/dev/lists.js +0 -233
  19. package/src/commands/dev/logs.js +0 -51
  20. package/src/commands/dev/rest.js +0 -64
  21. package/src/commands/dev/scopes.js +0 -96
  22. package/src/commands/dev/updatesets.js +0 -97
  23. package/src/commands/dev.js +0 -53
  24. package/src/commands/groupmembers.js +0 -39
  25. package/src/commands/grouproles.js +0 -39
  26. package/src/commands/groups.js +0 -57
  27. package/src/commands/incidents.js +0 -7
  28. package/src/commands/profiles.js +0 -79
  29. package/src/commands/records.js +0 -137
  30. package/src/commands/requests.js +0 -7
  31. package/src/commands/setup.js +0 -39
  32. package/src/commands/tasks.js +0 -7
  33. package/src/commands/tickets.js +0 -121
  34. package/src/commands/users.js +0 -57
  35. package/src/commands/version.js +0 -25
  36. package/src/config.js +0 -154
  37. package/src/context.js +0 -62
  38. package/src/errors.js +0 -101
  39. package/src/helpers.js +0 -60
  40. package/src/output.js +0 -410
  41. package/src/sdk.js +0 -357
@@ -1,256 +0,0 @@
1
- // Generic command builder for CRUD operations on a ServiceNow table
2
- // Used by incidents, changes, requests, tasks, and most dev subcommands
3
-
4
- import { getStringField, formatRecordForDisplay, buildQuerySuffix } from '../helpers.js';
5
- import { select } from '@inquirer/prompts';
6
- import { isTTY, FormatAuto } from '../output.js';
7
-
8
- export function buildTicketCommands(table, displayName, alias, defaultColumns, stateMap, iconFn, wrap) {
9
- return {
10
- command: `${displayName} [subcommand]`,
11
- aliases: [table, alias],
12
- describe: `Manage ${displayName} (e.g. "${displayName} list --query priority=1")`,
13
- builder: (yargs) => {
14
- return yargs
15
- .command({
16
- command: 'list',
17
- aliases: ['ls'],
18
- describe: `List ${table}`,
19
- builder: (y) => y
20
- .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
21
- .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
22
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
23
- .option('offset', { alias: 'o', type: 'number', default: 0, describe: 'Offset for pagination' }),
24
- handler: wrap(async (argv, app) => {
25
- const query = argv.query || '';
26
- const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
27
- const limit = argv.limit;
28
- const offset = argv.offset;
29
-
30
- // Interactive picker in TTY with auto format and no explicit query/offset
31
- const isInteractive = isTTY(process.stdout) && isTTY(process.stdin);
32
- if (isInteractive && app.output.getFormat() === FormatAuto && !query && offset === 0) {
33
- const pickerColumns = ['sys_id', 'number', 'short_description', 'state', 'assigned_to'];
34
- const pickerParams = new URLSearchParams();
35
- pickerParams.set('sysparm_limit', String(limit));
36
- pickerParams.set('sysparm_offset', '0');
37
- pickerParams.set('sysparm_display_value', 'all');
38
- pickerParams.set('sysparm_fields', pickerColumns.join(','));
39
- pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
40
-
41
- const pickerRecords = await app.sdk.list(table, pickerParams);
42
- if (pickerRecords.length === 0) {
43
- app.ok({
44
- table,
45
- count: 0,
46
- columns: pickerColumns,
47
- records: [],
48
- pagination: { limit, offset: 0 },
49
- context: { instance_url: app.getEffectiveInstance() },
50
- }, {
51
- summary: `0 ${table}(s)`,
52
- breadcrumbs: [
53
- { action: 'create', cmd: `jsn ${alias} create --description "..."`, description: `Create a new ${displayName}` },
54
- ],
55
- });
56
- return;
57
- }
58
-
59
- const choices = pickerRecords.map((record) => {
60
- const number = getStringField(record, 'number');
61
- const desc = getStringField(record, 'short_description');
62
- const state = getStringField(record, 'state');
63
- const assigned = getStringField(record, 'assigned_to');
64
- let label = `${number} ${desc} | ${state}`;
65
- if (assigned) {
66
- label += ` → ${assigned}`;
67
- }
68
- return { name: label, value: number };
69
- });
70
-
71
- let selectedNumber;
72
- try {
73
- selectedNumber = await select({
74
- message: `Select a ${displayName.slice(0, -1)}:`,
75
- choices,
76
- });
77
- } catch (err) {
78
- if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
79
- return;
80
- }
81
- throw err;
82
- }
83
-
84
- if (selectedNumber) {
85
- const showParams = new URLSearchParams();
86
- showParams.set('sysparm_query', `number=${selectedNumber}`);
87
- showParams.set('sysparm_display_value', 'all');
88
- showParams.set('sysparm_limit', '1');
89
- const showRecords = await app.sdk.list(table, showParams);
90
- if (showRecords.length === 0) {
91
- throw new Error(`${displayName} not found: ${selectedNumber}`);
92
- }
93
- const record = showRecords[0];
94
- record._context = {
95
- instance_url: app.getEffectiveInstance(),
96
- table,
97
- };
98
- app.ok(record, {
99
- summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${selectedNumber}`,
100
- breadcrumbs: [
101
- { action: 'update', cmd: `jsn ${alias} update ${selectedNumber} --data '{...}'`, description: `Update this ${displayName}` },
102
- { action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
103
- ],
104
- });
105
- }
106
- return;
107
- }
108
-
109
- const params = new URLSearchParams();
110
- params.set('sysparm_limit', String(limit));
111
- params.set('sysparm_offset', String(offset));
112
- params.set('sysparm_display_value', 'all');
113
- const fetchColumns = ['sys_id', ...columns];
114
- params.set('sysparm_fields', fetchColumns.join(','));
115
- const q = query ? query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
116
- params.set('sysparm_query', q);
117
-
118
- const records = await app.sdk.list(table, params);
119
- const displayRecords = records.map(r => formatRecordForDisplay(r, columns));
120
-
121
- const breadcrumbs = [
122
- { action: 'create', cmd: `jsn ${alias} create --description "..."`, description: `Create a new ${displayName}` },
123
- { action: 'filter', cmd: `jsn ${alias} list --query "priority=1"`, description: 'Filter: critical only' },
124
- ];
125
-
126
- if (records.length === limit) {
127
- breadcrumbs.push({
128
- action: 'next',
129
- cmd: `jsn ${alias} list --limit ${limit} --offset ${offset + limit}${buildQuerySuffix(query)}`,
130
- description: `Next page (offset ${offset + limit})`,
131
- });
132
- }
133
- if (offset > 0) {
134
- breadcrumbs.push({
135
- action: 'prev',
136
- cmd: `jsn ${alias} list --limit ${limit} --offset ${Math.max(0, offset - limit)}${buildQuerySuffix(query)}`,
137
- description: 'Previous page',
138
- });
139
- }
140
-
141
- app.ok({
142
- table,
143
- count: records.length,
144
- columns,
145
- records: displayRecords,
146
- pagination: { limit, offset },
147
- context: { instance_url: app.getEffectiveInstance() },
148
- }, {
149
- summary: `${records.length} ${table}(s)`,
150
- breadcrumbs,
151
- });
152
- }),
153
- })
154
- .command({
155
- command: 'show <number>',
156
- aliases: ['get'],
157
- describe: `Show a specific ${displayName}`,
158
- handler: wrap(async (argv, app) => {
159
- const number = argv.number;
160
- const params = new URLSearchParams();
161
- params.set('sysparm_query', `number=${number}`);
162
- params.set('sysparm_display_value', 'all');
163
- params.set('sysparm_limit', '1');
164
- const records = await app.sdk.list(table, params);
165
- if (records.length === 0) {
166
- throw new Error(`${displayName} not found: ${number}`);
167
- }
168
- const record = records[0];
169
- record._context = {
170
- instance_url: app.getEffectiveInstance(),
171
- table,
172
- };
173
- app.ok(record, {
174
- summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${number}`,
175
- breadcrumbs: [
176
- { action: 'update', cmd: `jsn ${alias} update ${number} --data '{...}'`, description: `Update this ${displayName}` },
177
- { action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
178
- ],
179
- });
180
- }),
181
- })
182
- .command({
183
- command: 'create',
184
- describe: `Create a new ${displayName}`,
185
- builder: (y) => y
186
- .option('description', { alias: 'd', type: 'string', describe: 'Short description' })
187
- .option('priority', { type: 'string', describe: 'Priority (1-5)' })
188
- .option('data', { type: 'string', describe: 'JSON data for additional fields' }),
189
- handler: wrap(async (argv, app) => {
190
- const recordData = {};
191
- if (argv.data) {
192
- Object.assign(recordData, JSON.parse(argv.data));
193
- }
194
- if (argv.description) recordData.short_description = argv.description;
195
- if (argv.priority) recordData.priority = argv.priority;
196
- if (!recordData.short_description) {
197
- throw new Error('short_description is required (use --description or --data)');
198
- }
199
- const record = await app.sdk.create(table, recordData);
200
- app.ok(record, {
201
- summary: `Created ${displayName} ${getStringField(record, 'number')}`,
202
- breadcrumbs: [
203
- { action: 'view', cmd: `jsn ${alias} show ${getStringField(record, 'number')}`, description: `View the new ${displayName}` },
204
- ],
205
- });
206
- }),
207
- })
208
- .command({
209
- command: 'update <number>',
210
- describe: `Update a ${displayName}`,
211
- builder: (y) => y
212
- .option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
213
- handler: wrap(async (argv, app) => {
214
- const number = argv.number;
215
- const recordData = JSON.parse(argv.data);
216
- const findParams = new URLSearchParams();
217
- findParams.set('sysparm_query', `number=${number}`);
218
- findParams.set('sysparm_limit', '1');
219
- const records = await app.sdk.list(table, findParams);
220
- if (records.length === 0) {
221
- throw new Error(`${displayName} not found: ${number}`);
222
- }
223
- const sysID = getStringField(records[0], 'sys_id');
224
- const updated = await app.sdk.update(table, sysID, recordData);
225
- app.ok(updated, {
226
- summary: `Updated ${displayName} ${number}`,
227
- breadcrumbs: [
228
- { action: 'view', cmd: `jsn ${alias} show ${number}`, description: `View the updated ${displayName}` },
229
- ],
230
- });
231
- }),
232
- })
233
- .command({
234
- command: 'delete <number>',
235
- describe: `Delete a ${displayName}`,
236
- handler: wrap(async (argv, app) => {
237
- const number = argv.number;
238
- const findParams = new URLSearchParams();
239
- findParams.set('sysparm_query', `number=${number}`);
240
- findParams.set('sysparm_limit', '1');
241
- const records = await app.sdk.list(table, findParams);
242
- if (records.length === 0) {
243
- throw new Error(`${displayName} not found: ${number}`);
244
- }
245
- const sysID = getStringField(records[0], 'sys_id');
246
- await app.sdk.delete(table, sysID);
247
- app.ok({ number, message: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} deleted` }, {
248
- summary: `Deleted ${displayName} ${number}`,
249
- });
250
- }),
251
- })
252
-
253
- },
254
- handler: () => {}, // Handled by subcommands
255
- };
256
- }
@@ -1,62 +0,0 @@
1
- import { getEffectiveInstance, normalizeInstanceURL } from '../config.js';
2
-
3
- export function authCmd(wrap) {
4
- return {
5
- command: 'auth <subcommand>',
6
- describe: 'Manage authentication',
7
- builder: (yargs) => {
8
- return yargs
9
- .command({
10
- command: 'login [instance]',
11
- describe: 'Authenticate with a ServiceNow instance',
12
- handler: wrap(async (argv, app) => {
13
- const instance = argv.instance ? normalizeInstanceURL(argv.instance) : getEffectiveInstance(app.config);
14
- if (!instance) {
15
- throw new Error('No instance configured. Set via --instance or run: jsn setup');
16
- }
17
- await app.auth.login(instance);
18
- app.ok({ authenticated: true, instance }, { summary: `Authenticated with ${instance}` });
19
- }),
20
- })
21
- .command({
22
- command: 'logout',
23
- describe: 'Remove stored credentials',
24
- handler: wrap(async (_argv, app) => {
25
- const instance = getEffectiveInstance(app.config);
26
- if (!instance) {
27
- throw new Error('No instance configured');
28
- }
29
- app.auth.logout(instance);
30
- app.ok({ logged_out: true, instance }, { summary: `Logged out from ${instance}` });
31
- }),
32
- })
33
- .command({
34
- command: 'status',
35
- describe: 'Show authentication status',
36
- handler: wrap(async (_argv, app) => {
37
- const instance = getEffectiveInstance(app.config);
38
- if (!instance) {
39
- app.ok({ authenticated: false, instance: null }, { summary: 'No instance configured' });
40
- return;
41
- }
42
- const isAuth = app.auth.isAuthenticatedFor(instance);
43
- app.ok({ authenticated: isAuth, instance }, { summary: isAuth ? `Authenticated with ${instance}` : `Not authenticated with ${instance}` });
44
- }),
45
- })
46
- .command({
47
- command: 'refresh',
48
- describe: 'Refresh OAuth token',
49
- handler: wrap(async (_argv, app) => {
50
- const instance = getEffectiveInstance(app.config);
51
- if (!instance) {
52
- throw new Error('No instance configured');
53
- }
54
- await app.auth.refreshToken(instance, await app.auth.getCredentialsFor(instance));
55
- app.ok({ refreshed: true, instance }, { summary: `Token refreshed for ${instance}` });
56
- }),
57
- })
58
-
59
- },
60
- handler: () => {},
61
- };
62
- }
@@ -1,7 +0,0 @@
1
- import { buildTicketCommands } from './_ticket.js';
2
-
3
- const changeDefaultColumns = ['number', 'short_description', 'risk', 'state', 'assigned_to'];
4
-
5
- export function changesCmd(wrap) {
6
- return buildTicketCommands('change_request', 'changes', 'chg', changeDefaultColumns, {}, null, wrap);
7
- }
@@ -1,223 +0,0 @@
1
- // Generic dev subcommand builder for table-based CRUD
2
-
3
- import { formatRecordForDisplay, getStringField, isHexString } from '../../helpers.js';
4
- import { getCurrentUser, getCurrentApplication } from '../../context.js';
5
- import readline from 'node:readline';
6
-
7
- function vowelArticle(word) {
8
- const first = word.charAt(0).toLowerCase();
9
- return first === 'a' || first === 'e' || first === 'i' || first === 'o' || first === 'u' ? 'an' : 'a';
10
- }
11
-
12
- function toSingular(name, explicitSingular) {
13
- if (explicitSingular) return explicitSingular;
14
- if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
15
- if (name.endsWith('es') && !name.endsWith('ses')) return name.slice(0, -2);
16
- if (name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1);
17
- return name;
18
- }
19
-
20
- async function getCurrentScope(sdk) {
21
- try {
22
- const user = await getCurrentUser(sdk);
23
- if (!user) return 'global';
24
- const app = await getCurrentApplication(sdk, user.sys_id);
25
- return app?.scope || 'global';
26
- } catch {
27
- return 'global';
28
- }
29
- }
30
-
31
- async function checkScope(sdk, recordScope) {
32
- const currentScope = await getCurrentScope(sdk);
33
- if (currentScope === 'global') return null;
34
- if (currentScope === recordScope) return null;
35
- return { currentScope, recordScope };
36
- }
37
-
38
- export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {}) {
39
- const showFields = opts.showFields !== undefined ? opts.showFields : null;
40
- const singular = toSingular(name, opts.singular);
41
- const readOnly = opts.readOnly || false;
42
- const scopeValidation = opts.scopeValidation || false;
43
- const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
44
- const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
45
- const crumbs = [
46
- { action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
47
- ];
48
- if (!readOnly) {
49
- crumbs.unshift(
50
- { action: 'delete', cmd: `jsn dev ${name} delete ${id}`, description: `Delete this ${singular}` },
51
- { action: 'update', cmd: `jsn dev ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
52
- );
53
- }
54
- return crumbs;
55
- });
56
-
57
- const builder = (yargs) => {
58
- let y = yargs
59
- .command({
60
- command: 'list',
61
- aliases: ['ls'],
62
- describe: `List ${name}`,
63
- builder: (y) => y
64
- .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
65
- .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
66
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
67
- handler: wrap(async (argv, app) => {
68
- const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
69
- const params = new URLSearchParams();
70
- params.set('sysparm_limit', String(argv.limit));
71
- params.set('sysparm_display_value', 'all');
72
- params.set('sysparm_fields', ['sys_id', ...columns].join(','));
73
- const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
74
- params.set('sysparm_query', q);
75
- const records = await app.sdk.list(table, params);
76
- app.ok({
77
- table,
78
- count: records.length,
79
- columns,
80
- records: records.map(r => formatRecordForDisplay(r, columns)),
81
- context: { instance_url: app.getEffectiveInstance() },
82
- }, { summary: `${records.length} ${name}(s)` });
83
- }),
84
- })
85
- .command({
86
- command: 'show <identifier>',
87
- aliases: ['get'],
88
- describe: `Show ${vowelArticle(singular)} ${singular} by name or sys_id`,
89
- handler: wrap(async (argv, app) => {
90
- const id = argv.identifier;
91
- const queryField = isHexString(id) && id.length === 32 ? 'sys_id' : 'name';
92
- const params = new URLSearchParams();
93
- params.set('sysparm_query', `${queryField}=${id}`);
94
- params.set('sysparm_limit', '1');
95
- params.set('sysparm_display_value', 'all');
96
- // Only restrict sysparm_fields if showFields is explicitly set.
97
- // Go version fetches all fields for show unless explicitly restricted.
98
- if (showFields && showFields.length > 0) {
99
- params.set('sysparm_fields', [...new Set(['sys_id', ...showFields])].join(','));
100
- }
101
- const records = await app.sdk.list(table, params);
102
- if (records.length === 0) {
103
- throw new Error(`${singular} not found: ${id}`);
104
- }
105
- records[0]._context = {
106
- instance_url: app.getEffectiveInstance(),
107
- table,
108
- };
109
-
110
- if (opts.onShow) {
111
- await opts.onShow(records[0], app);
112
- }
113
-
114
- const summary = typeof showSummary === 'function' ? showSummary(records[0], id) : showSummary;
115
- const breadcrumbs = typeof showBreadcrumbs === 'function' ? showBreadcrumbs(records[0], id) : showBreadcrumbs;
116
-
117
- app.ok(records[0], { summary, breadcrumbs });
118
- }),
119
- });
120
-
121
- if (!readOnly) {
122
- y = y
123
- .command({
124
- command: 'create',
125
- describe: `Create a new ${singular}`,
126
- builder: (y) => y.option('data', { type: 'string', demandOption: true, describe: 'JSON fields (e.g. \'{"state":"2"}\')' }),
127
- handler: wrap(async (argv, app) => {
128
- const recordData = JSON.parse(argv.data);
129
- const record = await app.sdk.create(table, recordData);
130
- app.ok(record, {
131
- summary: `Created ${singular}`,
132
- breadcrumbs: [
133
- { action: 'show', cmd: `jsn dev ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
134
- ],
135
- });
136
- }),
137
- })
138
- .command({
139
- command: 'update <identifier>',
140
- describe: `Update ${vowelArticle(singular)} ${singular}`,
141
- builder: (y) => y.option('data', { type: 'string', demandOption: true, describe: 'JSON fields (e.g. \'{"state":"2"}\')' }),
142
- handler: wrap(async (argv, app) => {
143
- const id = argv.identifier;
144
- const queryField = isHexString(id) && id.length === 32 ? 'sys_id' : 'name';
145
- const findParams = new URLSearchParams();
146
- findParams.set('sysparm_query', `${queryField}=${id}`);
147
- findParams.set('sysparm_limit', '1');
148
- const records = await app.sdk.list(table, findParams);
149
- if (records.length === 0) {
150
- throw new Error(`${singular} not found: ${id}`);
151
- }
152
- const sysID = getStringField(records[0], 'sys_id');
153
- const recordScope = getStringField(records[0], 'sys_scope');
154
-
155
- if (scopeValidation) {
156
- const scopeErr = await checkScope(app.sdk, recordScope);
157
- if (scopeErr) {
158
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
159
- }
160
- }
161
-
162
- const recordData = JSON.parse(argv.data);
163
- const updated = await app.sdk.update(table, sysID, recordData);
164
- app.ok(updated, { summary: `Updated ${singular} ${id}` });
165
- }),
166
- })
167
- .command({
168
- command: 'delete <identifier>',
169
- describe: `Delete ${vowelArticle(singular)} ${singular}`,
170
- builder: (y) => y.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
171
- handler: wrap(async (argv, app) => {
172
- const id = argv.identifier;
173
- const queryField = isHexString(id) && id.length === 32 ? 'sys_id' : 'name';
174
- const findParams = new URLSearchParams();
175
- findParams.set('sysparm_query', `${queryField}=${id}`);
176
- findParams.set('sysparm_limit', '1');
177
- const records = await app.sdk.list(table, findParams);
178
- if (records.length === 0) {
179
- throw new Error(`${singular} not found: ${id}`);
180
- }
181
- const sysID = getStringField(records[0], 'sys_id');
182
- const recordName = getStringField(records[0], 'name') || id;
183
- const recordScope = getStringField(records[0], 'sys_scope');
184
-
185
- if (scopeValidation) {
186
- const scopeErr = await checkScope(app.sdk, recordScope);
187
- if (scopeErr) {
188
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
189
- }
190
- }
191
-
192
- if (!argv.force && process.stdout.isTTY && process.stdin.isTTY) {
193
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
194
- const answer = await new Promise((resolve) => {
195
- rl.question(`Delete ${singular} '${recordName}'? (y/N): `, resolve);
196
- });
197
- rl.close();
198
- const response = answer.trim().toLowerCase();
199
- if (response !== 'y' && response !== 'yes') {
200
- throw new Error('Deletion cancelled');
201
- }
202
- }
203
-
204
- await app.sdk.delete(table, sysID);
205
- app.ok({ name: recordName, sys_id: sysID, deleted: true }, { summary: `Deleted ${singular} '${recordName}'` });
206
- }),
207
- });
208
- }
209
-
210
- return y;
211
- };
212
-
213
- return {
214
- command: `${name} [subcommand]`,
215
- aliases: aliases || [],
216
- describe: `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
217
- builder,
218
- handler: (argv) => {
219
- if (argv.help) return;
220
- argv.showHelp?.();
221
- },
222
- };
223
- }
@@ -1,89 +0,0 @@
1
- import { buildDevCmd } from './_generic.js';
2
- import { getStringField } from '../../helpers.js';
3
-
4
- // Simple table-based dev commands — singular names passed for grammar
5
- // Default columns and aliases match the Go version exactly
6
-
7
- async function fetchExtensionChain(sdk, record) {
8
- const chain = [];
9
- let current = record;
10
- let depth = 0;
11
- const maxDepth = 10;
12
-
13
- while (current && depth < maxDepth) {
14
- const name = getStringField(current, 'name');
15
- if (name) {
16
- chain.push(name);
17
- }
18
-
19
- const superClass = current.super_class;
20
- if (!superClass) break;
21
-
22
- let superClassSysId;
23
- if (typeof superClass === 'object' && superClass != null) {
24
- superClassSysId = superClass.value;
25
- } else if (typeof superClass === 'string') {
26
- superClassSysId = superClass;
27
- } else {
28
- break;
29
- }
30
-
31
- if (!superClassSysId) break;
32
-
33
- const params = new URLSearchParams();
34
- params.set('sysparm_query', `sys_id=${superClassSysId}`);
35
- params.set('sysparm_limit', '1');
36
- params.set('sysparm_fields', 'name,super_class');
37
- params.set('sysparm_display_value', 'all');
38
-
39
- const records = await sdk.list('sys_db_object', params);
40
- if (records.length === 0) break;
41
-
42
- current = records[0];
43
- depth++;
44
- }
45
-
46
- return chain;
47
- }
48
-
49
- export const actionsCmd = (wrap) => buildDevCmd('actions', 'sys_hub_action_type_definition', ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, { singular: 'action', scopeValidation: true });
50
-
51
- export const includesCmd = (wrap) => buildDevCmd('includes', 'sys_script_include', ['include', 'si'], ['name', 'api_name', 'active', 'sys_scope'], wrap, { singular: 'script include', scopeValidation: true });
52
-
53
- export const rulesCmd = (wrap) => buildDevCmd('rules', 'sys_script', ['rule', 'br'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'business rule', scopeValidation: true });
54
-
55
- export const clientScriptsCmd = (wrap) => buildDevCmd('clientscripts', 'sys_script_client', ['clientscript', 'cs'], ['name', 'table', 'active', 'type', 'sys_scope'], wrap, { singular: 'client script', scopeValidation: true });
56
-
57
- export const uiActionsCmd = (wrap) => buildDevCmd('uiactions', 'sys_ui_action', ['uiaction', 'ua'], ['name', 'table', 'active', 'order', 'sys_scope'], wrap, { singular: 'UI action', scopeValidation: true });
58
-
59
- export const uiPoliciesCmd = (wrap) => buildDevCmd('uipolicies', 'sys_ui_policy', ['uipolicy', 'up'], ['short_description', 'table', 'active', 'order', 'sys_scope'], wrap, { singular: 'UI policy', scopeValidation: true });
60
-
61
- export const tablesCmd = (wrap) => buildDevCmd('tables', 'sys_db_object', ['table', 't'], ['name', 'label', 'super_class', 'create_access_controls'], wrap, {
62
- singular: 'table',
63
- scopeValidation: true,
64
- showFields: ['name', 'label', 'super_class', 'create_access_controls', 'sys_scope', 'sys_created_on', 'sys_updated_on', 'sys_created_by', 'sys_updated_by', 'is_extendable'],
65
- async onShow(record, app) {
66
- const tableName = getStringField(record, 'name');
67
- const [count, extChain] = await Promise.all([
68
- app.sdk.aggregateCount('sys_dictionary', 'name=' + tableName),
69
- fetchExtensionChain(app.sdk, record),
70
- ]);
71
- record._column_count = count;
72
- record._extension_info = { chain: extChain };
73
- },
74
- });
75
-
76
- export const columnsCmd = (wrap) => buildDevCmd('columns', 'sys_dictionary', ['column', 'col'], ['element', 'column_label', 'internal_type', 'mandatory', 'max_length', 'active'], wrap, { singular: 'column', scopeValidation: true });
77
-
78
- // Read-only commands (Go only has list/show)
79
- export const importCmd = (wrap) => buildDevCmd('import', 'sys_import_set', ['imports', 'imp'], ['sys_import_set', 'sys_import_row', 'sys_target_table', 'sys_target_sys_id'], wrap, { singular: 'import set', readOnly: true });
80
- export const spPagesCmd = (wrap) => buildDevCmd('sppages', 'sp_page', ['sp-pages', 'pages'], ['id', 'title', 'sys_scope'], wrap, { singular: 'Service Portal page', readOnly: true });
81
- export const spWidgetsCmd = (wrap) => buildDevCmd('spwidgets', 'sp_widget', ['sp-widget', 'widgets'], ['id', 'name', 'sys_scope'], wrap, { singular: 'Service Portal widget', readOnly: true });
82
- export const uiPagesCmd = (wrap) => buildDevCmd('uipages', 'sys_ui_page', ['ui-page', 'pages'], ['name', 'sys_scope'], wrap, { singular: 'UI page', readOnly: true });
83
- export const appMenuCmd = (wrap) => buildDevCmd('appmenu', 'sys_app_application', ['app-menu', 'menu'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application menu', readOnly: true });
84
- export const scRAPICmd = (wrap) => buildDevCmd('scrapi', 'sys_ws_operation', ['scripted-rest', 'rest-api'], ['name', 'sys_ws_definition', 'sys_scope'], wrap, { singular: 'scripted REST API', readOnly: true });
85
-
86
- // Commands with full CRUD
87
- export const aclsCmd = (wrap) => buildDevCmd('acls', 'sys_security_acl', ['acl'], ['name', 'operation', 'type', 'active', 'sys_scope'], wrap, { singular: 'ACL', readOnly: true });
88
- export const rolesCmd = (wrap) => buildDevCmd('roles', 'sys_user_role', ['role', 'r'], ['name', 'description', 'elevated_privilege', 'sys_scope'], wrap, { singular: 'role', scopeValidation: true });
89
- export const propertiesCmd = (wrap) => buildDevCmd('properties', 'sys_properties', ['property', 'prop'], ['name', 'value', 'description', 'sys_scope'], wrap, { singular: 'property', readOnly: true });
@@ -1,17 +0,0 @@
1
- export function evalCmd(wrap) {
2
- return {
3
- command: 'eval',
4
- describe: 'Execute background scripts (eval)',
5
- builder: (yargs) => {
6
- return yargs
7
- .option('script', { alias: 's', type: 'string', describe: 'JavaScript code to execute (required)', demandOption: true });
8
- },
9
- handler: wrap(async (argv, app) => {
10
- app.ok({
11
- status: 'stub',
12
- message: 'Background script execution is not yet implemented',
13
- script: argv.script,
14
- }, { summary: 'Background script execution (stub)' });
15
- }),
16
- };
17
- }