@jacebenson/jsn 4.3.7-dev.26 → 4.3.7-dev.28

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.26",
3
+ "version": "4.3.7-dev.28",
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": {
@@ -1,9 +1,11 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { input, password as passwordPrompt, select } from '@inquirer/prompts';
2
4
  import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt } from '../helpers.js';
3
5
  import { FormatAuto } from '../output.js';
4
6
  import { declareCapabilities } from '../capabilities.js';
5
7
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
6
- import { detectDocker, defaultRun, installDockerMidServer, validateMidCredentials } from '../midserver-install.js';
8
+ import { detectDocker, defaultRun, installDockerMidServer, midInstallDirectory, validateMidCredentials } from '../midserver-install.js';
7
9
 
8
10
  const AGENT_TABLE = 'ecc_agent';
9
11
  const QUEUE_TABLE = 'ecc_queue';
@@ -190,6 +192,41 @@ async function waitForMidServerUp(app, name, timeoutMs, intervalMs) {
190
192
  throw new Error(`MID Server '${fullName}' did not report Up within ${timeoutMs}ms`);
191
193
  }
192
194
 
195
+ function removeLocalMidServer(instanceURL, name) {
196
+ const installDir = midInstallDirectory(instanceURL, name);
197
+ const composePath = path.join(installDir, 'docker-compose.yaml');
198
+ const envPath = path.join(installDir, '.env');
199
+ if (!fs.existsSync(composePath)) return false;
200
+ const composeArgs = ['compose', ...(fs.existsSync(envPath) ? ['--env-file', envPath] : []), '-f', composePath, 'down'];
201
+ try {
202
+ defaultRun('docker', composeArgs, { cwd: installDir });
203
+ } catch {
204
+ defaultRun('sudo', ['systemctl', 'start', 'docker']);
205
+ defaultRun('sudo', ['docker', ...composeArgs], { cwd: installDir });
206
+ }
207
+ try {
208
+ fs.rmSync(installDir, { recursive: true, force: true });
209
+ } catch {
210
+ defaultRun('sudo', ['rm', '-rf', '--', installDir]);
211
+ }
212
+ return true;
213
+ }
214
+
215
+ function showMidServerLogs(app, identifier, argv) {
216
+ const installDir = midInstallDirectory(app.getEffectiveInstance(), identifier);
217
+ const composePath = path.join(installDir, 'docker-compose.yaml');
218
+ const envPath = path.join(installDir, '.env');
219
+ if (!fs.existsSync(composePath)) throw new Error(`No local Compose installation found for MID Server '${identifier}'`);
220
+ const args = ['compose', ...(fs.existsSync(envPath) ? ['--env-file', envPath] : []), '-f', composePath, 'logs', '--tail', String(argv.tail)];
221
+ if (argv.follow) args.push('--follow');
222
+ try {
223
+ defaultRun('docker', args, { cwd: installDir });
224
+ } catch {
225
+ defaultRun('sudo', ['systemctl', 'start', 'docker']);
226
+ defaultRun('sudo', ['docker', ...args], { cwd: installDir });
227
+ }
228
+ }
229
+
193
230
  export function midserversCmd(wrap) {
194
231
  return {
195
232
  command: 'midservers [subcommand]',
@@ -218,6 +255,26 @@ export function midserversCmd(wrap) {
218
255
  app.ok(record, { summary: `MID Server: ${getStringField(record, 'name') || argv.identifier}` });
219
256
  }),
220
257
  })
258
+ .command({
259
+ command: 'logs <identifier>',
260
+ describe: 'Show local Docker Compose logs for a MID Server',
261
+ builder: (b) => b
262
+ .option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
263
+ .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' }),
264
+ handler: wrap(async (argv, app) => {
265
+ showMidServerLogs(app, argv.identifier, argv);
266
+ }),
267
+ })
268
+ .command({
269
+ command: '<identifier> logs',
270
+ describe: 'Show local Docker Compose logs for a MID Server',
271
+ builder: (b) => b
272
+ .option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
273
+ .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' }),
274
+ handler: wrap(async (argv, app) => {
275
+ showMidServerLogs(app, argv.identifier, argv);
276
+ }),
277
+ })
221
278
  .command({
222
279
  command: 'add',
223
280
  describe: 'Install and verify a MID Server',
@@ -226,7 +283,7 @@ export function midserversCmd(wrap) {
226
283
  .option('username', { type: 'string', describe: 'MID Server username (password is always prompted)' })
227
284
  .option('install-method', { type: 'string', choices: ['docker', 'direct'], describe: 'Installation method (prompted when omitted)' })
228
285
  .option('directory', { type: 'string', describe: 'Docker installation directory' })
229
- .option('wait-timeout', { type: 'number', default: 120000, describe: 'How long to wait for the MID Server to report Up' })
286
+ .option('wait-timeout', { type: 'number', default: 300000, describe: 'How long to wait for the MID Server to report Up' })
230
287
  .option('wait-interval', { type: 'number', default: 3000, describe: 'MID Server status polling interval in milliseconds' }),
231
288
  handler: wrap(async (argv, app) => {
232
289
  app.requireInstance();
@@ -272,13 +329,14 @@ export function midserversCmd(wrap) {
272
329
  });
273
330
  if (installMethod === 'sudo') {
274
331
  defaultRun('sudo', ['systemctl', 'start', 'docker']);
275
- defaultRun('sudo', ['docker', 'compose', '-f', installation.composePath, 'up', '-d'], { cwd: installation.installDir });
332
+ defaultRun('sudo', ['docker', 'compose', '--env-file', path.join(installation.installDir, '.env'), '-f', installation.composePath, 'up', '-d'], { cwd: installation.installDir });
276
333
  installation.prepared = false;
277
334
  }
278
335
  if (installation.prepared) {
279
336
  app.ok(installation, { summary: `Prepared MID Server files in ${installation.installDir}. Run: ${installation.runCommand}` });
280
337
  return;
281
338
  }
339
+ console.log('MID Server container started. ServiceNow may take a few minutes to register it and report Up.');
282
340
  let record;
283
341
  try {
284
342
  record = await waitForMidServerUp(app, installation.name, Math.max(1, argv['wait-timeout']), Math.max(500, argv['wait-interval']));
@@ -302,7 +360,14 @@ export function midserversCmd(wrap) {
302
360
  handler: wrap(async (argv, app) => {
303
361
  let record;
304
362
  if (argv.identifier) {
305
- record = await resolveMidServer(app.sdk, argv.identifier);
363
+ try {
364
+ record = await resolveMidServer(app.sdk, argv.identifier);
365
+ } catch (error) {
366
+ if (error.code !== 'not_found') throw error;
367
+ const localDir = midInstallDirectory(app.getEffectiveInstance(), argv.identifier);
368
+ if (!fs.existsSync(path.join(localDir, 'docker-compose.yaml'))) throw error;
369
+ record = null;
370
+ }
306
371
  } else {
307
372
  if (!canPrompt() || app.output.getFormat() !== FormatAuto) {
308
373
  throw new Error('An identifier is required outside an interactive terminal');
@@ -323,11 +388,12 @@ export function midserversCmd(wrap) {
323
388
  });
324
389
  if (!record) return;
325
390
  }
326
- const sysId = unwrapSysId(record);
391
+ const sysId = record ? unwrapSysId(record) : null;
327
392
  const name = getStringField(record, 'name') || argv.identifier || sysId;
328
393
  await confirmDelete(app, argv, `Remove MID Server '${name}'`);
329
- await app.sdk.delete(AGENT_TABLE, sysId);
330
- app.ok({ name, sys_id: sysId, deleted: true }, { summary: `Removed MID Server '${name}'` });
394
+ const localRemoved = removeLocalMidServer(app.getEffectiveInstance(), name);
395
+ if (record) await app.sdk.delete(AGENT_TABLE, sysId);
396
+ app.ok({ name, sys_id: sysId, deleted: Boolean(record), local_compose_removed: localRemoved }, { summary: `Removed MID Server '${name}'${localRemoved ? ' and its local Compose stack' : ''}` });
331
397
  }),
332
398
  })
333
399
  .command({
@@ -90,6 +90,11 @@ function instanceName(instanceURL) {
90
90
  return hostname.replace(/\.service-now\.com$|\.servicenowservices\.com$/i, '').replace(/[^A-Za-z0-9._-]/g, '_');
91
91
  }
92
92
 
93
+ export function midInstallDirectory(instanceURL, name, homeDirectory = os.homedir()) {
94
+ const shortName = normalizeMidName(name);
95
+ return path.join(homeDirectory, '.servicenow', 'mids', instanceName(instanceURL), shortName);
96
+ }
97
+
93
98
  export async function installDockerMidServer({
94
99
  instanceURL,
95
100
  username,
@@ -106,11 +111,11 @@ export async function installDockerMidServer({
106
111
  const displayName = `mid.server.${shortName}`;
107
112
  const releaseName = suppliedReleaseName || (await validateMidCredentials(instanceURL, username, password, fetchImpl)).releaseName;
108
113
  const recipeURL = buildMidRecipeUrl(releaseName);
109
- const serviceNowInstance = instanceName(instanceURL);
110
- const installDir = directory || path.join(os.homedir(), '.servicenow', 'mids', serviceNowInstance, shortName);
114
+ const installDir = directory || midInstallDirectory(instanceURL, name);
111
115
  fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
112
116
  const exportDir = path.join(installDir, 'export');
113
- fs.mkdirSync(exportDir, { recursive: true, mode: 0o700 });
117
+ fs.mkdirSync(exportDir, { recursive: true, mode: 0o777 });
118
+ fs.chmodSync(exportDir, 0o777);
114
119
  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsn-mid-'));
115
120
  const filename = path.basename(recipeURL);
116
121
  const archive = path.join(workDir, filename);
@@ -144,18 +149,18 @@ export async function installDockerMidServer({
144
149
  composePath,
145
150
  serviceName,
146
151
  prepared: true,
147
- runCommand: `docker compose -f ${composePath} up -d`,
152
+ runCommand: `docker compose --env-file ${envPath} -f ${composePath} up -d`,
148
153
  };
149
154
  }
150
155
  run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, persistedRecipeDir]);
151
- run('docker', ['compose', '-f', composePath, 'up', '-d'], { cwd: installDir, env });
156
+ run('docker', ['compose', '--env-file', envPath, '-f', composePath, 'up', '-d'], { cwd: installDir, env });
152
157
  return {
153
158
  name: displayName,
154
159
  releaseName,
155
160
  installDir,
156
161
  composePath,
157
162
  serviceName,
158
- rollback: () => run('docker', ['compose', '-f', composePath, 'down'], { cwd: installDir, env }),
163
+ rollback: () => run('docker', ['compose', '--env-file', envPath, '-f', composePath, 'down'], { cwd: installDir, env }),
159
164
  };
160
165
  } finally {
161
166
  fs.rmSync(workDir, { recursive: true, force: true });