@jacebenson/jsn 4.3.7-dev.22 → 4.3.7-dev.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.22",
3
+ "version": "4.3.7-dev.24",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@ import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList,
3
3
  import { FormatAuto } from '../output.js';
4
4
  import { declareCapabilities } from '../capabilities.js';
5
5
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
6
- import { dockerComposeAvailable, installDockerMidServer, validateMidCredentials } from '../midserver-install.js';
6
+ import { detectDocker, installDockerMidServer, validateMidCredentials } from '../midserver-install.js';
7
7
 
8
8
  const AGENT_TABLE = 'ecc_agent';
9
9
  const QUEUE_TABLE = 'ecc_queue';
@@ -235,23 +235,30 @@ export function midserversCmd(wrap) {
235
235
  message: 'MID Server name:',
236
236
  validate: value => value.trim() ? true : 'MID Server name is required',
237
237
  })).trim();
238
- const dockerAvailable = dockerComposeAvailable();
238
+ const dockerStatus = detectDocker();
239
+ const dockerAvailable = dockerStatus.available;
239
240
  const installMethod = argv['install-method'] || await select({
240
241
  message: dockerAvailable
241
- ? 'Docker and Docker Compose detected. How should the MID Server be installed?'
242
- : 'Docker was not detected. How should the MID Server be installed?',
242
+ ? 'Docker and Docker Compose are ready. How should the MID Server be installed?'
243
+ : dockerStatus.installed && dockerStatus.compose
244
+ ? 'Docker is installed, but this user cannot access the Docker daemon. How should the MID Server be installed?'
245
+ : 'Docker was not detected. How should the MID Server be installed?',
243
246
  choices: dockerAvailable
244
247
  ? [
245
248
  { name: 'Docker (recommended)', value: 'docker' },
246
249
  { name: 'Direct install (not available yet)', value: 'direct' },
247
250
  ]
251
+ : dockerStatus.installed && dockerStatus.compose
252
+ ? [
253
+ { name: 'Prepare files now, run Docker later', value: 'prepare' },
254
+ { name: 'Direct install (not available yet)', value: 'direct' },
255
+ ]
248
256
  : [
249
- { name: 'Install Docker first', value: 'install-docker' },
257
+ { name: 'Prepare files now, run Docker later', value: 'prepare' },
250
258
  { name: 'Direct install (not available yet)', value: 'direct' },
251
259
  ],
252
260
  });
253
261
  if (installMethod === 'direct') throw new Error('Direct MID Server installation is not implemented yet. Choose Docker.');
254
- if (installMethod === 'install-docker') throw new Error('Automatic Docker installation is not implemented yet. Install Docker and Compose, then run this command again.');
255
262
 
256
263
  const installation = await installDockerMidServer({
257
264
  instanceURL: app.getEffectiveInstance(),
@@ -260,7 +267,12 @@ export function midserversCmd(wrap) {
260
267
  name,
261
268
  releaseName: credentials.releaseName,
262
269
  directory: argv.directory,
270
+ runNow: installMethod !== 'prepare',
263
271
  });
272
+ if (installation.prepared) {
273
+ app.ok(installation, { summary: `Prepared MID Server files in ${installation.installDir}. Run: ${installation.runCommand}` });
274
+ return;
275
+ }
264
276
  let record;
265
277
  try {
266
278
  record = await waitForMidServerUp(app, installation.name, Math.max(1, argv['wait-timeout']), Math.max(500, argv['wait-interval']));
@@ -277,14 +289,36 @@ export function midserversCmd(wrap) {
277
289
  }),
278
290
  })
279
291
  .command({
280
- command: 'remove <identifier>',
292
+ command: 'remove [identifier]',
281
293
  aliases: ['delete'],
282
- describe: 'Remove a MID Server record',
294
+ describe: 'Remove a MID Server record (interactive when no identifier is given)',
283
295
  builder: (b) => b.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
284
296
  handler: wrap(async (argv, app) => {
285
- const record = await resolveMidServer(app.sdk, argv.identifier);
297
+ let record;
298
+ if (argv.identifier) {
299
+ record = await resolveMidServer(app.sdk, argv.identifier);
300
+ } else {
301
+ if (!canPrompt() || app.output.getFormat() !== FormatAuto) {
302
+ throw new Error('An identifier is required outside an interactive terminal');
303
+ }
304
+ record = await interactiveList({
305
+ app,
306
+ table: AGENT_TABLE,
307
+ singular: 'MID Server',
308
+ columns: LIST_FIELDS,
309
+ limit: 50,
310
+ labelField: 'name',
311
+ message: 'Select a MID Server to remove',
312
+ formatLabel: (candidate) => {
313
+ const name = getStringField(candidate, 'name') || getStringField(candidate, 'sys_id');
314
+ const status = getStringField(candidate, 'status');
315
+ return status ? `${name} [${status}]` : name;
316
+ },
317
+ });
318
+ if (!record) return;
319
+ }
286
320
  const sysId = unwrapSysId(record);
287
- const name = getStringField(record, 'name') || argv.identifier;
321
+ const name = getStringField(record, 'name') || argv.identifier || sysId;
288
322
  await confirmDelete(app, argv, `Remove MID Server '${name}'`);
289
323
  await app.sdk.delete(AGENT_TABLE, sysId);
290
324
  app.ok({ name, sys_id: sysId, deleted: true }, { summary: `Removed MID Server '${name}'` });
@@ -63,18 +63,30 @@ export async function validateMidCredentials(instanceURL, username, password, fe
63
63
  return { releaseName };
64
64
  }
65
65
 
66
+ export function detectDocker(run = defaultRun) {
67
+ const status = { installed: false, compose: false, daemon: false, available: false };
68
+ try { run('docker', ['--version'], { stdio: 'ignore' }); status.installed = true; } catch { return status; }
69
+ try { run('docker', ['compose', 'version'], { stdio: 'ignore' }); status.compose = true; } catch { return status; }
70
+ try { run('docker', ['info'], { stdio: 'ignore' }); status.daemon = true; } catch { return status; }
71
+ status.available = true;
72
+ return status;
73
+ }
74
+
66
75
  export function dockerComposeAvailable(run = defaultRun) {
67
- try {
68
- run('docker', ['info'], { stdio: 'ignore' });
69
- run('docker', ['compose', 'version'], { stdio: 'ignore' });
70
- return true;
71
- } catch {
72
- return false;
73
- }
76
+ return detectDocker(run).available;
74
77
  }
75
78
 
76
79
  function composeFile({ serviceName }) {
77
- return `services:\n ${serviceName}:\n container_name: ${serviceName}\n image: ${serviceName}:\${MID_RELEASE}\n restart: unless-stopped\n volumes:\n - ./export:/opt/snc_mid_server/agent/export\n environment:\n MID_INSTANCE_URL: \${MID_INSTANCE_URL}\n MID_INSTANCE_USERNAME: \${MID_INSTANCE_USERNAME}\n MID_INSTANCE_PASSWORD: \${MID_INSTANCE_PASSWORD}\n MID_SERVER_NAME: \${MID_SERVER_NAME}\n`;
80
+ return `services:\n ${serviceName}:\n container_name: ${serviceName}\n build: ./recipe\n image: ${serviceName}:\${MID_RELEASE}\n restart: unless-stopped\n volumes:\n - ./export:/opt/snc_mid_server/agent/export\n environment:\n MID_INSTANCE_URL: \${MID_INSTANCE_URL}\n MID_INSTANCE_USERNAME: \${MID_INSTANCE_USERNAME}\n MID_INSTANCE_PASSWORD: \${MID_INSTANCE_PASSWORD}\n MID_SERVER_NAME: \${MID_SERVER_NAME}\n`;
81
+ }
82
+
83
+ function envFile({ instanceURL, username, password, displayName, releaseName }) {
84
+ return `MID_RELEASE=${releaseName}\nMID_INSTANCE_URL=${String(instanceURL).replace(/\/$/, '')}/\nMID_INSTANCE_USERNAME=${username}\nMID_INSTANCE_PASSWORD=${password}\nMID_SERVER_NAME=${displayName}\n`;
85
+ }
86
+
87
+ function instanceName(instanceURL) {
88
+ const hostname = new URL(instanceURL).hostname;
89
+ return hostname.replace(/\.service-now\.com$|\.servicenowservices\.com$/i, '').replace(/[^A-Za-z0-9._-]/g, '_');
78
90
  }
79
91
 
80
92
  export async function installDockerMidServer({
@@ -84,17 +96,17 @@ export async function installDockerMidServer({
84
96
  name,
85
97
  directory,
86
98
  releaseName: suppliedReleaseName,
99
+ runNow = true,
87
100
  fetchImpl = fetch,
88
101
  run = defaultRun,
89
102
  }) {
90
- if (!dockerComposeAvailable(run)) {
91
- throw new Error('Docker and Docker Compose are required for Docker MID Server installation');
92
- }
103
+ const dockerAvailable = detectDocker(run).available;
93
104
  const shortName = normalizeMidName(name);
94
105
  const displayName = `mid.server.${shortName}`;
95
106
  const releaseName = suppliedReleaseName || (await validateMidCredentials(instanceURL, username, password, fetchImpl)).releaseName;
96
107
  const recipeURL = buildMidRecipeUrl(releaseName);
97
- const installDir = directory || path.join(os.homedir(), '.local', 'share', 'jsn', 'midservers', shortName);
108
+ const serviceNowInstance = instanceName(instanceURL);
109
+ const installDir = directory || path.join(os.homedir(), '.servicenow', 'mids', serviceNowInstance, shortName);
98
110
  fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
99
111
  const exportDir = path.join(installDir, 'export');
100
112
  fs.mkdirSync(exportDir, { recursive: true, mode: 0o700 });
@@ -107,9 +119,8 @@ export async function installDockerMidServer({
107
119
  fs.mkdirSync(recipeDir, { recursive: true, mode: 0o700 });
108
120
  run('unzip', ['-q', archive, '-d', recipeDir]);
109
121
  const composePath = path.join(installDir, 'docker-compose.yaml');
122
+ const persistedRecipeDir = path.join(installDir, 'recipe');
110
123
  const serviceName = `jsn_mid_${shortName.replace(/[^A-Za-z0-9_.-]/g, '_')}`;
111
- run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, recipeDir]);
112
- fs.writeFileSync(composePath, composeFile({ serviceName, instanceURL, displayName }), { mode: 0o600 });
113
124
  const env = {
114
125
  MID_RELEASE: releaseName,
115
126
  MID_INSTANCE_URL: `${String(instanceURL).replace(/\/$/, '')}/`,
@@ -117,6 +128,22 @@ export async function installDockerMidServer({
117
128
  MID_INSTANCE_PASSWORD: password,
118
129
  MID_SERVER_NAME: displayName,
119
130
  };
131
+ fs.rmSync(persistedRecipeDir, { recursive: true, force: true });
132
+ fs.cpSync(recipeDir, persistedRecipeDir, { recursive: true });
133
+ fs.writeFileSync(composePath, composeFile({ serviceName }), { mode: 0o600 });
134
+ fs.writeFileSync(path.join(installDir, '.env'), envFile({ instanceURL, username, password, displayName, releaseName }), { mode: 0o600 });
135
+ if (!dockerAvailable || !runNow) {
136
+ return {
137
+ name: displayName,
138
+ releaseName,
139
+ installDir,
140
+ composePath,
141
+ serviceName,
142
+ prepared: true,
143
+ runCommand: `docker compose -f ${composePath} up -d`,
144
+ };
145
+ }
146
+ run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, persistedRecipeDir]);
120
147
  run('docker', ['compose', '-f', composePath, 'up', '-d'], { cwd: installDir, env });
121
148
  return {
122
149
  name: displayName,