@jacebenson/jsn 0.0.3

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.
@@ -0,0 +1,96 @@
1
+ import { formatRecordForDisplay, getStringField } from '../../helpers.js';
2
+
3
+ export function scopesCmd(wrap) {
4
+ return {
5
+ command: 'scopes [subcommand]',
6
+ aliases: ['scope', 'sc'],
7
+ describe: 'Manage application scopes',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ aliases: ['ls'],
13
+ describe: 'List application scopes',
14
+ builder: (y) => y
15
+ .option('query', { type: 'string', describe: 'Encoded query string' })
16
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
17
+ .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ handler: wrap(async (argv, app) => {
19
+ const columns = argv.columns ? argv.columns.split(',') : ['name', 'scope', 'short_description', 'active'];
20
+ const params = new URLSearchParams();
21
+ params.set('sysparm_limit', String(argv.limit));
22
+ params.set('sysparm_display_value', 'all');
23
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
24
+ const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
25
+ params.set('sysparm_query', q);
26
+ const records = await app.sdk.list('sys_scope', params);
27
+ app.ok({
28
+ table: 'sys_scope',
29
+ count: records.length,
30
+ columns,
31
+ records: records.map(r => formatRecordForDisplay(r, columns)),
32
+ context: { instance_url: app.getEffectiveInstance() },
33
+ }, { summary: `${records.length} scope(s)` });
34
+ }),
35
+ })
36
+ .command({
37
+ command: 'show <scope>',
38
+ aliases: ['get'],
39
+ describe: 'Show a scope',
40
+ handler: wrap(async (argv, app) => {
41
+ const params = new URLSearchParams();
42
+ params.set('sysparm_query', `scope=${argv.scope}`);
43
+ params.set('sysparm_limit', '1');
44
+ params.set('sysparm_display_value', 'all');
45
+ const records = await app.sdk.list('sys_scope', params);
46
+ if (records.length === 0) {
47
+ throw new Error(`Scope not found: ${argv.scope}`);
48
+ }
49
+ app.ok(records[0], { summary: `Scope ${argv.scope}` });
50
+ }),
51
+ })
52
+ .command({
53
+ command: 'set <scope>',
54
+ describe: 'Set the current application scope',
55
+ handler: wrap(async (argv, app) => {
56
+ const params = new URLSearchParams();
57
+ params.set('sysparm_query', `scope=${argv.scope}`);
58
+ params.set('sysparm_limit', '1');
59
+ params.set('sysparm_fields', 'sys_id,scope');
60
+ const records = await app.sdk.list('sys_scope', params);
61
+ if (records.length === 0) {
62
+ throw new Error(`Scope not found: ${argv.scope}`);
63
+ }
64
+ const scopeSysID = getStringField(records[0], 'sys_id');
65
+ // Update user preference
66
+ const user = await app.sdk.list('sys_user', new URLSearchParams({
67
+ sysparm_query: 'user_name=javascript:gs.getUserName()',
68
+ sysparm_limit: '1',
69
+ sysparm_fields: 'sys_id',
70
+ }));
71
+ if (user.length === 0) {
72
+ throw new Error('Could not determine current user');
73
+ }
74
+ const userSysID = getStringField(user[0], 'sys_id');
75
+ const prefParams = new URLSearchParams();
76
+ prefParams.set('sysparm_query', `user=${userSysID}^name=apps.current_app`);
77
+ prefParams.set('sysparm_limit', '1');
78
+ const prefs = await app.sdk.list('sys_user_preference', prefParams);
79
+ if (prefs.length > 0) {
80
+ await app.sdk.update('sys_user_preference', getStringField(prefs[0], 'sys_id'), { value: scopeSysID });
81
+ } else {
82
+ await app.sdk.create('sys_user_preference', {
83
+ user: userSysID,
84
+ name: 'apps.current_app',
85
+ value: scopeSysID,
86
+ type: 'string',
87
+ });
88
+ }
89
+ app.ok({ scope: argv.scope, sys_id: scopeSysID }, { summary: `Current scope: ${argv.scope}` });
90
+ }),
91
+ })
92
+
93
+ },
94
+ handler: () => {},
95
+ };
96
+ }
@@ -0,0 +1,97 @@
1
+ import { formatRecordForDisplay, getStringField } from '../../helpers.js';
2
+
3
+ export function updateSetsCmd(wrap) {
4
+ return {
5
+ command: 'updatesets [subcommand]',
6
+ aliases: ['updateset', 'us'],
7
+ describe: 'Manage update sets',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ aliases: ['ls'],
13
+ describe: 'List update sets',
14
+ builder: (y) => y
15
+ .option('query', { type: 'string', describe: 'Encoded query string' })
16
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
17
+ .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ handler: wrap(async (argv, app) => {
19
+ const columns = argv.columns ? argv.columns.split(',') : ['name', 'state', 'application'];
20
+ const params = new URLSearchParams();
21
+ params.set('sysparm_limit', String(argv.limit));
22
+ params.set('sysparm_display_value', 'all');
23
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
24
+ const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
25
+ params.set('sysparm_query', q);
26
+ const records = await app.sdk.list('sys_update_set', params);
27
+ app.ok({
28
+ table: 'sys_update_set',
29
+ count: records.length,
30
+ columns,
31
+ records: records.map(r => formatRecordForDisplay(r, columns)),
32
+ context: { instance_url: app.getEffectiveInstance() },
33
+ }, { summary: `${records.length} update set(s)` });
34
+ }),
35
+ })
36
+ .command({
37
+ command: 'show <name>',
38
+ aliases: ['get'],
39
+ describe: 'Show an update set',
40
+ handler: wrap(async (argv, app) => {
41
+ const params = new URLSearchParams();
42
+ params.set('sysparm_query', `name=${argv.name}`);
43
+ params.set('sysparm_limit', '1');
44
+ params.set('sysparm_display_value', 'all');
45
+ const records = await app.sdk.list('sys_update_set', params);
46
+ if (records.length === 0) {
47
+ throw new Error(`Update set not found: ${argv.name}`);
48
+ }
49
+ app.ok(records[0], { summary: `Update set ${argv.name}` });
50
+ }),
51
+ })
52
+ .command({
53
+ command: 'set <name>',
54
+ describe: 'Set the current update set',
55
+ handler: wrap(async (argv, app) => {
56
+ const params = new URLSearchParams();
57
+ params.set('sysparm_query', `name=${argv.name}`);
58
+ params.set('sysparm_limit', '1');
59
+ params.set('sysparm_fields', 'sys_id,name');
60
+ const records = await app.sdk.list('sys_update_set', params);
61
+ if (records.length === 0) {
62
+ throw new Error(`Update set not found: ${argv.name}`);
63
+ }
64
+ const sysID = getStringField(records[0], 'sys_id');
65
+ // Update user preference
66
+ const user = await app.sdk.list('sys_user', new URLSearchParams({
67
+ sysparm_query: 'user_name=javascript:gs.getUserName()',
68
+ sysparm_limit: '1',
69
+ sysparm_fields: 'sys_id',
70
+ }));
71
+ if (user.length === 0) {
72
+ throw new Error('Could not determine current user');
73
+ }
74
+ const userSysID = getStringField(user[0], 'sys_id');
75
+ // Find or create preference
76
+ const prefParams = new URLSearchParams();
77
+ prefParams.set('sysparm_query', `user=${userSysID}^name=sys_update_set`);
78
+ prefParams.set('sysparm_limit', '1');
79
+ const prefs = await app.sdk.list('sys_user_preference', prefParams);
80
+ if (prefs.length > 0) {
81
+ await app.sdk.update('sys_user_preference', getStringField(prefs[0], 'sys_id'), { value: sysID });
82
+ } else {
83
+ await app.sdk.create('sys_user_preference', {
84
+ user: userSysID,
85
+ name: 'sys_update_set',
86
+ value: sysID,
87
+ type: 'string',
88
+ });
89
+ }
90
+ app.ok({ update_set: argv.name, sys_id: sysID }, { summary: `Current update set: ${argv.name}` });
91
+ }),
92
+ })
93
+
94
+ },
95
+ handler: () => {},
96
+ };
97
+ }
@@ -0,0 +1,53 @@
1
+ // Parent dev command
2
+
3
+ import {
4
+ actionsCmd, includesCmd, rulesCmd,
5
+ clientScriptsCmd, uiActionsCmd, uiPoliciesCmd,
6
+ tablesCmd, columnsCmd, importCmd,
7
+ spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd, scRAPICmd,
8
+ aclsCmd, rolesCmd, propertiesCmd,
9
+ } from './dev/_simple.js';
10
+ import { flowsCmd } from './dev/flows.js';
11
+ import { formsCmd } from './dev/forms.js';
12
+ import { listsCmd } from './dev/lists.js';
13
+ import { updateSetsCmd } from './dev/updatesets.js';
14
+ import { scopesCmd } from './dev/scopes.js';
15
+ import { evalCmd } from './dev/eval.js';
16
+ import { restCmd } from './dev/rest.js';
17
+ import { logsCmd } from './dev/logs.js';
18
+
19
+ export function devCmd(wrap) {
20
+ return {
21
+ command: 'dev [subcommand]',
22
+ describe: 'Manage ServiceNow development artifacts',
23
+ builder: (yargs) => {
24
+ return yargs
25
+ .command(flowsCmd(wrap))
26
+ .command(actionsCmd(wrap))
27
+ .command(includesCmd(wrap))
28
+ .command(rulesCmd(wrap))
29
+ .command(clientScriptsCmd(wrap))
30
+ .command(uiActionsCmd(wrap))
31
+ .command(uiPoliciesCmd(wrap))
32
+ .command(tablesCmd(wrap))
33
+ .command(columnsCmd(wrap))
34
+ .command(formsCmd(wrap))
35
+ .command(listsCmd(wrap))
36
+ .command(importCmd(wrap))
37
+ .command(spPagesCmd(wrap))
38
+ .command(spWidgetsCmd(wrap))
39
+ .command(uiPagesCmd(wrap))
40
+ .command(appMenuCmd(wrap))
41
+ .command(scRAPICmd(wrap))
42
+ .command(aclsCmd(wrap))
43
+ .command(rolesCmd(wrap))
44
+ .command(updateSetsCmd(wrap))
45
+ .command(scopesCmd(wrap))
46
+ .command(propertiesCmd(wrap))
47
+ .command(logsCmd(wrap))
48
+ .command(evalCmd(wrap))
49
+ .command(restCmd(wrap));
50
+ },
51
+ handler: () => {},
52
+ };
53
+ }
@@ -0,0 +1,39 @@
1
+ import { formatRecordForDisplay } from '../helpers.js';
2
+
3
+ export function groupMembersCmd(wrap) {
4
+ return {
5
+ command: 'groupmembers [subcommand]',
6
+ aliases: ['groupmember', 'gmember', 'gm'],
7
+ describe: 'Manage group memberships',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ aliases: ['ls'],
13
+ describe: 'List group members',
14
+ builder: (y) => y
15
+ .option('query', { type: 'string', describe: 'Encoded query string' })
16
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
17
+ .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ handler: wrap(async (argv, app) => {
19
+ const columns = argv.columns ? argv.columns.split(',') : ['user.name', 'group.name'];
20
+ const params = new URLSearchParams();
21
+ params.set('sysparm_limit', String(argv.limit));
22
+ params.set('sysparm_display_value', 'all');
23
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
24
+ if (argv.query) params.set('sysparm_query', argv.query);
25
+ const records = await app.sdk.list('sys_user_grmember', params);
26
+ app.ok({
27
+ table: 'sys_user_grmember',
28
+ count: records.length,
29
+ columns,
30
+ records: records.map(r => formatRecordForDisplay(r, columns)),
31
+ context: { instance_url: app.getEffectiveInstance() },
32
+ }, { summary: `${records.length} group member(s)` });
33
+ }),
34
+ })
35
+
36
+ },
37
+ handler: () => {},
38
+ };
39
+ }
@@ -0,0 +1,39 @@
1
+ import { formatRecordForDisplay } from '../helpers.js';
2
+
3
+ export function groupRolesCmd(wrap) {
4
+ return {
5
+ command: 'grouproles [subcommand]',
6
+ aliases: ['grouprole', 'grole', 'gr'],
7
+ describe: 'Manage group roles',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ aliases: ['ls'],
13
+ describe: 'List group roles',
14
+ builder: (y) => y
15
+ .option('query', { type: 'string', describe: 'Encoded query string' })
16
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
17
+ .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ handler: wrap(async (argv, app) => {
19
+ const columns = argv.columns ? argv.columns.split(',') : ['group.name', 'role.name'];
20
+ const params = new URLSearchParams();
21
+ params.set('sysparm_limit', String(argv.limit));
22
+ params.set('sysparm_display_value', 'all');
23
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
24
+ if (argv.query) params.set('sysparm_query', argv.query);
25
+ const records = await app.sdk.list('sys_group_has_role', params);
26
+ app.ok({
27
+ table: 'sys_group_has_role',
28
+ count: records.length,
29
+ columns,
30
+ records: records.map(r => formatRecordForDisplay(r, columns)),
31
+ context: { instance_url: app.getEffectiveInstance() },
32
+ }, { summary: `${records.length} group role(s)` });
33
+ }),
34
+ })
35
+
36
+ },
37
+ handler: () => {},
38
+ };
39
+ }
@@ -0,0 +1,57 @@
1
+ import { formatRecordForDisplay } from '../helpers.js';
2
+
3
+ export function groupsCmd(wrap) {
4
+ return {
5
+ command: 'groups [subcommand]',
6
+ aliases: ['group'],
7
+ describe: 'Search and display groups',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ aliases: ['ls'],
13
+ describe: 'List groups',
14
+ builder: (y) => y
15
+ .option('query', { type: 'string', describe: 'Encoded query string' })
16
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
17
+ .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ handler: wrap(async (argv, app) => {
19
+ const columns = argv.columns ? argv.columns.split(',') : ['name', 'manager', 'email'];
20
+ const params = new URLSearchParams();
21
+ params.set('sysparm_limit', String(argv.limit));
22
+ params.set('sysparm_display_value', 'all');
23
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
24
+ if (argv.query) params.set('sysparm_query', argv.query);
25
+ const records = await app.sdk.list('sys_user_group', params);
26
+ app.ok({
27
+ table: 'sys_user_group',
28
+ count: records.length,
29
+ columns,
30
+ records: records.map(r => formatRecordForDisplay(r, columns)),
31
+ context: { instance_url: app.getEffectiveInstance() },
32
+ }, { summary: `${records.length} group(s)` });
33
+ }),
34
+ })
35
+ .command({
36
+ command: 'show <name>',
37
+ aliases: ['get'],
38
+ describe: 'Show a group by name or sys_id',
39
+ handler: wrap(async (argv, app) => {
40
+ const id = argv.name;
41
+ const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
42
+ const params = new URLSearchParams();
43
+ params.set('sysparm_query', isSysID ? `sys_id=${id}` : `name=${id}`);
44
+ params.set('sysparm_limit', '1');
45
+ params.set('sysparm_display_value', 'all');
46
+ const records = await app.sdk.list('sys_user_group', params);
47
+ if (records.length === 0) {
48
+ throw new Error(`Group not found: ${id}`);
49
+ }
50
+ app.ok(records[0], { summary: `Group ${id}` });
51
+ }),
52
+ })
53
+
54
+ },
55
+ handler: () => {},
56
+ };
57
+ }
@@ -0,0 +1,7 @@
1
+ import { buildTicketCommands } from './_ticket.js';
2
+
3
+ const incidentDefaultColumns = ['number', 'short_description', 'priority', 'state', 'assigned_to'];
4
+
5
+ export function incidentsCmd(wrap) {
6
+ return buildTicketCommands('incident', 'incidents', 'inc', incidentDefaultColumns, {}, null, wrap);
7
+ }
@@ -0,0 +1,79 @@
1
+ import { saveConfig } from '../config.js';
2
+
3
+ export function profilesCmd(wrap) {
4
+ return {
5
+ command: 'profiles <subcommand>',
6
+ aliases: ['profile'],
7
+ describe: 'Manage configuration profiles',
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .command({
11
+ command: 'list',
12
+ describe: 'List all profiles',
13
+ handler: wrap(async (_argv, app) => {
14
+ const profiles = [];
15
+ for (const [name, profile] of Object.entries(app.config.profiles || {})) {
16
+ const instance = profile.instance_url || '';
17
+ const isAuth = app.auth.isAuthenticatedFor(instance);
18
+ const isDefault = name === app.config.defaultProfile;
19
+ profiles.push({
20
+ name,
21
+ instance,
22
+ username: profile.username || '',
23
+ authenticated: isAuth,
24
+ default: isDefault,
25
+ });
26
+ }
27
+ app.ok({ profiles }, { summary: `${profiles.length} profile(s)` });
28
+ }),
29
+ })
30
+ .command({
31
+ command: 'use <name>',
32
+ describe: 'Set active profile',
33
+ handler: wrap(async (argv, app) => {
34
+ const name = argv.name;
35
+ if (!app.config.profiles[name]) {
36
+ throw new Error(`Profile not found: ${name}`);
37
+ }
38
+ app.config.defaultProfile = name;
39
+ app.config.activeProfile = name;
40
+ saveConfig(app.config);
41
+ app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
42
+ }),
43
+ })
44
+ .command({
45
+ command: 'show [name]',
46
+ describe: 'Show profile details',
47
+ handler: wrap(async (argv, app) => {
48
+ const name = argv.name || app.config.defaultProfile || Object.keys(app.config.profiles || {})[0];
49
+ if (!name || !app.config.profiles[name]) {
50
+ throw new Error('No profiles configured');
51
+ }
52
+ const profile = app.config.profiles[name];
53
+ app.ok({ name, ...profile }, { summary: `Profile: ${name}` });
54
+ }),
55
+ })
56
+ .command({
57
+ command: 'remove <name>',
58
+ describe: 'Remove a profile',
59
+ handler: wrap(async (argv, app) => {
60
+ const name = argv.name;
61
+ if (!app.config.profiles[name]) {
62
+ throw new Error(`Profile not found: ${name}`);
63
+ }
64
+ delete app.config.profiles[name];
65
+ if (app.config.defaultProfile === name) {
66
+ app.config.defaultProfile = '';
67
+ }
68
+ if (app.config.activeProfile === name) {
69
+ app.config.activeProfile = '';
70
+ }
71
+ saveConfig(app.config);
72
+ app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
73
+ }),
74
+ })
75
+
76
+ },
77
+ handler: () => {},
78
+ };
79
+ }
@@ -0,0 +1,137 @@
1
+ import { formatRecordForDisplay, buildQuerySuffix } from '../helpers.js';
2
+
3
+ const tableDefaultColumns = {
4
+ incident: ['number', 'short_description', 'priority', 'state', 'assigned_to'],
5
+ change_request: ['number', 'short_description', 'risk', 'state', 'assigned_to'],
6
+ change_task: ['number', 'short_description', 'state', 'assigned_to'],
7
+ problem: ['number', 'short_description', 'priority', 'state', 'assigned_to'],
8
+ sc_request: ['number', 'short_description', 'request_state', 'requested_for'],
9
+ sc_req_item: ['number', 'short_description', 'stage', 'assigned_to'],
10
+ sc_task: ['number', 'short_description', 'state', 'assigned_to'],
11
+ sys_user: ['user_name', 'name', 'email', 'active'],
12
+ sys_user_group: ['name', 'manager', 'email'],
13
+ cmdb_ci: ['name', 'operational_status', 'ip_address'],
14
+ cmdb_ci_server: ['name', 'operational_status', 'ip_address'],
15
+ kb_knowledge: ['number', 'short_description', 'workflow_state', 'author'],
16
+ };
17
+
18
+ function getDefaultColumns(table) {
19
+ return tableDefaultColumns[table] || ['sys_id'];
20
+ }
21
+
22
+ export function recordsCmd(wrap) {
23
+ return {
24
+ command: 'records <subcommand>',
25
+ describe: 'Query and manage records in any table',
26
+ builder: (yargs) => {
27
+ return yargs
28
+ .command({
29
+ command: 'list',
30
+ describe: 'List records from a table',
31
+ builder: (y) => y
32
+ .option('table', { type: 'string', demandOption: true, describe: 'Table name' })
33
+ .option('query', { type: 'string', describe: 'Encoded query string' })
34
+ .option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns' })
35
+ .option('limit', { type: 'number', default: 20, describe: 'Max records' })
36
+ .option('offset', { type: 'number', default: 0, describe: 'Offset' }),
37
+ handler: wrap(async (argv, app) => {
38
+ const table = argv.table;
39
+ const columns = argv.columns ? argv.columns.split(',') : getDefaultColumns(table);
40
+ const params = new URLSearchParams();
41
+ params.set('sysparm_limit', String(argv.limit));
42
+ params.set('sysparm_offset', String(argv.offset));
43
+ params.set('sysparm_display_value', 'all');
44
+ params.set('sysparm_fields', ['sys_id', ...columns].join(','));
45
+ if (argv.query) params.set('sysparm_query', argv.query);
46
+ const records = await app.sdk.list(table, params);
47
+ const displayRecords = records.map(r => formatRecordForDisplay(r, columns));
48
+ const breadcrumbs = [
49
+ { action: 'create', cmd: `jsn records create --table ${table} --data '{...}'`, description: 'Create a new record' },
50
+ { action: 'filter', cmd: `jsn records list --table ${table} --query "priority=1"`, description: 'Filter: priority 1 only' },
51
+ { action: 'columns', cmd: `jsn dev columns --table ${table}`, description: 'View available columns' },
52
+ ];
53
+ if (records.length === argv.limit) {
54
+ breadcrumbs.push({
55
+ action: 'next',
56
+ cmd: `jsn records list --table ${table} --limit ${argv.limit} --offset ${argv.offset + argv.limit}${buildQuerySuffix(argv.query)}`,
57
+ description: `Next page`,
58
+ });
59
+ }
60
+ if (argv.offset > 0) {
61
+ breadcrumbs.push({
62
+ action: 'prev',
63
+ cmd: `jsn records list --table ${table} --limit ${argv.limit} --offset ${Math.max(0, argv.offset - argv.limit)}${buildQuerySuffix(argv.query)}`,
64
+ description: 'Previous page',
65
+ });
66
+ }
67
+ app.ok({
68
+ table,
69
+ count: records.length,
70
+ columns,
71
+ records: displayRecords,
72
+ pagination: { limit: argv.limit, offset: argv.offset },
73
+ context: { instance_url: app.getEffectiveInstance() },
74
+ }, { summary: `${records.length} record(s) from ${table}`, breadcrumbs });
75
+ }),
76
+ })
77
+ .command({
78
+ command: 'get',
79
+ describe: 'Get a single record by sys_id',
80
+ builder: (y) => y
81
+ .option('table', { type: 'string', demandOption: true, describe: 'Table name' })
82
+ .option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' })
83
+ .option('columns', { type: 'string', describe: 'Comma-separated columns' }),
84
+ handler: wrap(async (argv, app) => {
85
+ const params = new URLSearchParams();
86
+ params.set('sysparm_query', `sys_id=${argv['sys-id']}`);
87
+ params.set('sysparm_limit', '1');
88
+ params.set('sysparm_display_value', 'true');
89
+ if (argv.columns) params.set('sysparm_fields', argv.columns);
90
+ const records = await app.sdk.list(argv.table, params);
91
+ if (records.length === 0) {
92
+ throw new Error(`Record not found: ${argv['sys-id']}`);
93
+ }
94
+ app.ok(records[0], { summary: `Record from ${argv.table}` });
95
+ }),
96
+ })
97
+ .command({
98
+ command: 'create',
99
+ describe: 'Create a new record',
100
+ builder: (y) => y
101
+ .option('table', { type: 'string', demandOption: true, describe: 'Table name' })
102
+ .option('data', { type: 'string', demandOption: true, describe: 'JSON data' }),
103
+ handler: wrap(async (argv, app) => {
104
+ const recordData = JSON.parse(argv.data);
105
+ const record = await app.sdk.create(argv.table, recordData);
106
+ app.ok(record, { summary: `Created record in ${argv.table}` });
107
+ }),
108
+ })
109
+ .command({
110
+ command: 'update',
111
+ describe: 'Update an existing record',
112
+ builder: (y) => y
113
+ .option('table', { type: 'string', demandOption: true, describe: 'Table name' })
114
+ .option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' })
115
+ .option('data', { type: 'string', demandOption: true, describe: 'JSON data' }),
116
+ handler: wrap(async (argv, app) => {
117
+ const recordData = JSON.parse(argv.data);
118
+ const record = await app.sdk.update(argv.table, argv['sys-id'], recordData);
119
+ app.ok(record, { summary: `Updated record in ${argv.table}` });
120
+ }),
121
+ })
122
+ .command({
123
+ command: 'delete',
124
+ describe: 'Delete a record',
125
+ builder: (y) => y
126
+ .option('table', { type: 'string', demandOption: true, describe: 'Table name' })
127
+ .option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' }),
128
+ handler: wrap(async (argv, app) => {
129
+ await app.sdk.delete(argv.table, argv['sys-id']);
130
+ app.ok({ message: 'Record deleted', table: argv.table, sys_id: argv['sys-id'] }, { summary: `Deleted record from ${argv.table}` });
131
+ }),
132
+ })
133
+
134
+ },
135
+ handler: () => {},
136
+ };
137
+ }
@@ -0,0 +1,7 @@
1
+ import { buildTicketCommands } from './_ticket.js';
2
+
3
+ const requestDefaultColumns = ['number', 'short_description', 'stage', 'assigned_to'];
4
+
5
+ export function requestsCmd(wrap) {
6
+ return buildTicketCommands('sc_req_item', 'requests', 'req', requestDefaultColumns, {}, null, wrap);
7
+ }
@@ -0,0 +1,35 @@
1
+ import readline from 'node:readline';
2
+ import { getEffectiveInstance, normalizeInstanceURL, setProfile } from '../config.js';
3
+
4
+ export function setupCmd(wrap) {
5
+ return {
6
+ command: 'setup',
7
+ describe: 'Interactive first-time setup',
8
+ handler: wrap(async (_argv, app) => {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
11
+
12
+ console.log('Welcome to JSN - ServiceNow CLI');
13
+ console.log();
14
+
15
+ let instance = getEffectiveInstance(app.config);
16
+ if (!instance) {
17
+ instance = await ask('ServiceNow instance URL (e.g., dev12345.service-now.com): ');
18
+ instance = normalizeInstanceURL(instance);
19
+ }
20
+ console.log(`Instance: ${instance}`);
21
+
22
+ const profileName = await ask('Profile name (default): ') || 'default';
23
+ await setProfile(app.config, profileName, { instance_url: instance });
24
+
25
+ const loginNow = await ask('Login now? [Y/n]: ');
26
+ if (!loginNow || loginNow.toLowerCase() !== 'n') {
27
+ await app.auth.login(instance);
28
+ console.log('Login successful!');
29
+ }
30
+
31
+ rl.close();
32
+ app.ok({ setup: true, instance, profile: profileName }, { summary: 'Setup complete' });
33
+ }),
34
+ };
35
+ }
@@ -0,0 +1,7 @@
1
+ import { buildTicketCommands } from './_ticket.js';
2
+
3
+ const taskDefaultColumns = ['number', 'short_description', 'state', 'assigned_to'];
4
+
5
+ export function tasksCmd(wrap) {
6
+ return buildTicketCommands('sc_task', 'tasks', 'task', taskDefaultColumns, {}, null, wrap);
7
+ }