@jacebenson/jsn 4.3.7 → 4.3.8-dev.47

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 +266 -1
  2. package/package.json +1 -1
  3. package/skills/servicenow/SKILL.md +16 -0
  4. package/src/app.js +8 -1
  5. package/src/auth.js +14 -12
  6. package/src/cli-adapter.js +162 -11
  7. package/src/cli.js +121 -13
  8. package/src/commands/_generic.js +3 -2
  9. package/src/commands/_simple.js +1 -1
  10. package/src/commands/actions.js +156 -0
  11. package/src/commands/auth.js +21 -11
  12. package/src/commands/cmdb.js +18 -0
  13. package/src/commands/diff.js +205 -0
  14. package/src/commands/docs/docs.js +18 -16
  15. package/src/commands/flows.js +149 -47
  16. package/src/commands/graphql.js +95 -0
  17. package/src/commands/groupmembers.js +19 -4
  18. package/src/commands/grouproles.js +15 -4
  19. package/src/commands/midservers.js +708 -0
  20. package/src/commands/perf.js +36 -9
  21. package/src/commands/platform.js +202 -0
  22. package/src/commands/records.js +19 -15
  23. package/src/commands/skill.js +5 -2
  24. package/src/commands/transactions.js +78 -0
  25. package/src/commands/updatesets.js +77 -11
  26. package/src/errors.js +71 -6
  27. package/src/flow-create.js +884 -0
  28. package/src/flow-designer-interactive.js +417 -0
  29. package/src/flow-inspection.js +15 -5
  30. package/src/flow-publish.js +32 -0
  31. package/src/flow-test.js +122 -0
  32. package/src/flow-triggers.js +448 -0
  33. package/src/flow-versioning.js +53 -0
  34. package/src/help.js +93 -28
  35. package/src/helpers.js +124 -9
  36. package/src/midserver-direct.js +312 -0
  37. package/src/midserver-install.js +170 -0
  38. package/src/midserver-zip.js +107 -0
  39. package/src/output.js +7 -0
  40. package/src/processflow-actions.js +366 -0
  41. package/src/sdk.js +8 -0
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import { declareCapabilities } from '../capabilities.js';
3
+
4
+ // GraphQL documents may contain mutations. Gate the whole command so normal
5
+ // instance/auth and read-only protections apply without attempting AST parsing.
6
+ declareCapabilities('graphql', { mutationSubcommands: [''] });
7
+
8
+ function readStdin() {
9
+ return new Promise((resolve, reject) => {
10
+ let data = '';
11
+ process.stdin.setEncoding('utf-8');
12
+ process.stdin.on('data', (chunk) => { data += chunk; });
13
+ process.stdin.on('end', () => resolve(data));
14
+ process.stdin.on('error', reject);
15
+ });
16
+ }
17
+
18
+ export async function resolveGraphQLInput(argv) {
19
+ const sources = [
20
+ argv.document !== undefined,
21
+ argv.query !== undefined,
22
+ argv.queryFile !== undefined,
23
+ argv.stdin === true,
24
+ ].filter(Boolean).length;
25
+ if (sources !== 1) {
26
+ throw new Error('Provide exactly one GraphQL document via positional document, --query, --query-file, or --stdin');
27
+ }
28
+
29
+ let query;
30
+ if (argv.document !== undefined) query = argv.document;
31
+ else if (argv.query !== undefined) query = argv.query;
32
+ else if (argv.queryFile !== undefined) query = fs.readFileSync(argv.queryFile, 'utf-8');
33
+ else query = await readStdin();
34
+
35
+ query = String(query || '').trim();
36
+ if (!query) throw new Error('GraphQL document must not be empty');
37
+ return query;
38
+ }
39
+
40
+ export function parseGraphQLVariables(value) {
41
+ if (value === undefined) return {};
42
+ let variables;
43
+ try {
44
+ variables = JSON.parse(value);
45
+ } catch {
46
+ throw new Error('--variables must be valid JSON');
47
+ }
48
+ if (!variables || typeof variables !== 'object' || Array.isArray(variables)) {
49
+ throw new Error('--variables must be a JSON object');
50
+ }
51
+ return variables;
52
+ }
53
+
54
+ export function graphqlCmd(wrap) {
55
+ return {
56
+ command: 'graphql [document]',
57
+ describe: 'Execute a GraphQL document against ServiceNow Now GraphQL',
58
+ builder: (yargs) => yargs
59
+ .positional('document', {
60
+ describe: 'GraphQL document string',
61
+ type: 'string',
62
+ })
63
+ .option('query', {
64
+ type: 'string',
65
+ describe: 'GraphQL document string (alternative to positional)',
66
+ })
67
+ .option('query-file', {
68
+ type: 'string',
69
+ describe: 'Read the GraphQL document from a file',
70
+ })
71
+ .option('variables', {
72
+ type: 'string',
73
+ describe: 'GraphQL variables as a JSON object',
74
+ })
75
+ .option('stdin', {
76
+ type: 'boolean',
77
+ describe: 'Read the GraphQL document from stdin',
78
+ }),
79
+ handler: wrap(async (argv, app) => {
80
+ const query = await resolveGraphQLInput(argv);
81
+ const variables = parseGraphQLVariables(argv.variables);
82
+ const endpoint = `${app.getEffectiveInstance()}/api/now/graphql`;
83
+ const result = await app.sdk.request(endpoint, {
84
+ method: 'POST',
85
+ body: JSON.stringify({ query, variables }),
86
+ });
87
+ const errorCount = Array.isArray(result?.errors) ? result.errors.length : 0;
88
+ app.ok(result, {
89
+ summary: errorCount
90
+ ? `GraphQL returned ${errorCount} error(s)`
91
+ : 'GraphQL request succeeded',
92
+ });
93
+ }),
94
+ };
95
+ }
@@ -1,4 +1,9 @@
1
- import { formatRecordForDisplay, getStringField, resolveFieldsParam } from '../helpers.js';
1
+ import { formatRecordForDisplay, getStringField, resolveFieldsParam, assertSafeExactMatch, confirmDelete, quoteShellArg } from '../helpers.js';
2
+ import { declareCapabilities } from '../capabilities.js';
3
+
4
+ // add/remove write sys_user_grmember; the mutation guard (instance +
5
+ // read-only profile) and the delete confirmation both key off this.
6
+ declareCapabilities('groupmembers', { mutationSubcommands: ['add', 'remove'] });
2
7
 
3
8
  export function groupMembersCmd(wrap) {
4
9
  return {
@@ -42,6 +47,8 @@ export function groupMembersCmd(wrap) {
42
47
  handler: wrap(async (argv, app) => {
43
48
  const groupName = argv.group;
44
49
  const userName = argv.user;
50
+ assertSafeExactMatch(groupName);
51
+ assertSafeExactMatch(userName);
45
52
 
46
53
  // Resolve group sys_id
47
54
  const isGroupSysID = groupName.length === 32 && /^[0-9a-fA-F]+$/.test(groupName);
@@ -86,7 +93,7 @@ export function groupMembersCmd(wrap) {
86
93
  summary: `Added ${userName} to ${groupName}`,
87
94
  breadcrumbs: [
88
95
  { action: 'list', cmd: `jsn groupmembers list --query "group.name=${groupName}"`, description: 'List group members' },
89
- { action: 'remove', cmd: `jsn groupmembers remove --group "${groupName}" --user "${userName}"`, description: 'Remove this member' },
96
+ { action: 'remove', cmd: `jsn groupmembers remove --group ${quoteShellArg(groupName)} --user ${quoteShellArg(userName)}`, description: 'Remove this member' },
90
97
  ],
91
98
  });
92
99
  }),
@@ -96,10 +103,18 @@ export function groupMembersCmd(wrap) {
96
103
  describe: 'Remove a user from a group',
97
104
  builder: (y) => y
98
105
  .option('group', { alias: 'g', type: 'string', demandOption: true, describe: 'Group name or sys_id (required)' })
99
- .option('user', { alias: 'u', type: 'string', demandOption: true, describe: 'Username or sys_id (required)' }),
106
+ .option('user', { alias: 'u', type: 'string', demandOption: true, describe: 'Username or sys_id (required)' })
107
+ .option('force', { type: 'boolean', describe: 'Skip the confirmation prompt (removal)' }),
100
108
  handler: wrap(async (argv, app) => {
101
109
  const groupName = argv.group;
102
110
  const userName = argv.user;
111
+ assertSafeExactMatch(groupName);
112
+ assertSafeExactMatch(userName);
113
+ // Confirm before any lookup: a refused confirmation must not
114
+ // touch the instance at all.
115
+ await confirmDelete(app, argv, `Remove ${userName} from group ${groupName}`, {
116
+ rerun: ['groupmembers', 'remove', '--group', groupName, '--user', userName],
117
+ });
103
118
 
104
119
  // Resolve group sys_id
105
120
  const isGroupSysID = groupName.length === 32 && /^[0-9a-fA-F]+$/.test(groupName);
@@ -142,7 +157,7 @@ export function groupMembersCmd(wrap) {
142
157
  summary: `Removed ${userName} from ${groupName}`,
143
158
  breadcrumbs: [
144
159
  { action: 'list', cmd: `jsn groupmembers list --query "group.name=${groupName}"`, description: 'List group members' },
145
- { action: 'add', cmd: `jsn groupmembers add --group "${groupName}" --user "${userName}"`, description: 'Add user back to group' },
160
+ { action: 'add', cmd: `jsn groupmembers add --group ${quoteShellArg(groupName)} --user ${quoteShellArg(userName)}`, description: 'Add user back to group' },
146
161
  ],
147
162
  });
148
163
  }),
@@ -1,4 +1,9 @@
1
- import { formatRecordForDisplay, getStringField, resolveFieldsParam, assertSafeExactMatch } from '../helpers.js';
1
+ import { formatRecordForDisplay, getStringField, resolveFieldsParam, assertSafeExactMatch, confirmDelete, quoteShellArg } from '../helpers.js';
2
+ import { declareCapabilities } from '../capabilities.js';
3
+
4
+ // add/remove write sys_group_has_role; the mutation guard (instance +
5
+ // read-only profile) and the delete confirmation both key off this.
6
+ declareCapabilities('grouproles', { mutationSubcommands: ['add', 'remove'] });
2
7
 
3
8
  export function groupRolesCmd(wrap) {
4
9
  return {
@@ -88,7 +93,7 @@ export function groupRolesCmd(wrap) {
88
93
  summary: `Added role ${roleName} to ${groupName}`,
89
94
  breadcrumbs: [
90
95
  { action: 'list', cmd: `jsn grouproles list --query "group.name=${groupName}"`, description: 'List group roles' },
91
- { action: 'remove', cmd: `jsn grouproles remove --group "${groupName}" --role "${roleName}"`, description: 'Remove this role' },
96
+ { action: 'remove', cmd: `jsn grouproles remove --group ${quoteShellArg(groupName)} --role ${quoteShellArg(roleName)}`, description: 'Remove this role' },
92
97
  ],
93
98
  });
94
99
  }),
@@ -98,12 +103,18 @@ export function groupRolesCmd(wrap) {
98
103
  describe: 'Remove a role from a group',
99
104
  builder: (y) => y
100
105
  .option('group', { alias: 'g', type: 'string', demandOption: true, describe: 'Group name or sys_id (required)' })
101
- .option('role', { alias: 'r', type: 'string', demandOption: true, describe: 'Role name or sys_id (required)' }),
106
+ .option('role', { alias: 'r', type: 'string', demandOption: true, describe: 'Role name or sys_id (required)' })
107
+ .option('force', { type: 'boolean', describe: 'Skip the confirmation prompt (removal)' }),
102
108
  handler: wrap(async (argv, app) => {
103
109
  const groupName = argv.group;
104
110
  const roleName = argv.role;
105
111
  assertSafeExactMatch(groupName);
106
112
  assertSafeExactMatch(roleName);
113
+ // Confirm before any lookup: a refused confirmation must not
114
+ // touch the instance at all.
115
+ await confirmDelete(app, argv, `Remove role ${roleName} from group ${groupName}`, {
116
+ rerun: ['grouproles', 'remove', '--group', groupName, '--role', roleName],
117
+ });
107
118
 
108
119
  // Resolve group sys_id
109
120
  const isGroupSysID = groupName.length === 32 && /^[0-9a-fA-F]+$/.test(groupName);
@@ -146,7 +157,7 @@ export function groupRolesCmd(wrap) {
146
157
  summary: `Removed role ${roleName} from ${groupName}`,
147
158
  breadcrumbs: [
148
159
  { action: 'list', cmd: `jsn grouproles list --query "group.name=${groupName}"`, description: 'List group roles' },
149
- { action: 'add', cmd: `jsn grouproles add --group "${groupName}" --role "${roleName}"`, description: 'Add role back to group' },
160
+ { action: 'add', cmd: `jsn grouproles add --group ${quoteShellArg(groupName)} --role ${quoteShellArg(roleName)}`, description: 'Add role back to group' },
150
161
  ],
151
162
  });
152
163
  }),