@jacebenson/jsn 4.3.7-dev.35 → 4.3.7-dev.36

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.35",
3
+ "version": "4.3.7-dev.36",
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,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import process from 'node:process';
3
4
  import { input, password as passwordPrompt, select } from '@inquirer/prompts';
4
5
  import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt, assertSafeExactMatch } from '../helpers.js';
5
6
  import { FormatAuto } from '../output.js';
@@ -171,6 +172,40 @@ function midDiagnosticsCommand(kind, wrap) {
171
172
  };
172
173
  }
173
174
 
175
+ // Progress for long, otherwise-silent direct-install phases. Human terminals
176
+ // only, and on stderr, so the stdout envelope and --json output stay clean.
177
+ export function directProgress(app) {
178
+ const human = app.output?.getFormat() === FormatAuto && (canPrompt() || process.stderr.isTTY === true);
179
+ if (!human) return () => {};
180
+ const mib = bytes => (Number(bytes || 0) / (1024 * 1024)).toFixed(0);
181
+ return event => {
182
+ if (event.phase === 'download-start') {
183
+ process.stderr.write(`Downloading the official MID ZIP from ServiceNow (about 532 MiB).\n ${event.archiveURL}\n`);
184
+ } else if (event.phase === 'download') {
185
+ const percent = event.total ? ` (${Math.floor((event.received / event.total) * 100)}%)` : '';
186
+ process.stderr.write(`\r ${mib(event.received)} MiB${percent} downloaded...`);
187
+ if (event.done) process.stderr.write('\n');
188
+ } else if (event.phase === 'extract-start') {
189
+ process.stderr.write(`Extracting ${event.entries} files into ${event.installDir}...\n`);
190
+ } else if (event.phase === 'extract-done') {
191
+ process.stderr.write('Extraction complete. Configuring the MID Server...\n');
192
+ }
193
+ };
194
+ }
195
+
196
+ // A registration wait is minutes of silence otherwise; report elapsed progress.
197
+ export function registrationProgress(app, timeoutMs) {
198
+ const human = app.output?.getFormat() === FormatAuto && (canPrompt() || process.stderr.isTTY === true);
199
+ if (!human) return undefined;
200
+ const seconds = Math.round(timeoutMs / 1000);
201
+ let lastReport = 0;
202
+ return elapsedMs => {
203
+ if (elapsedMs - lastReport < 15000) return;
204
+ lastReport = elapsedMs;
205
+ process.stderr.write(`Waiting for ServiceNow to register the MID Server... ${Math.round(elapsedMs / 1000)}s of ${seconds}s\n`);
206
+ };
207
+ }
208
+
174
209
  async function listAgents(app, argv) {
175
210
  const columns = argv.columns
176
211
  ? [...new Set(argv.columns.split(',').map(column => column.trim()).filter(Boolean))]
@@ -266,10 +301,11 @@ async function promptMidCredentials(app, argv) {
266
301
  }
267
302
  }
268
303
 
269
- export async function waitForMidServerUp(app, name, timeoutMs, intervalMs) {
304
+ export async function waitForMidServerUp(app, name, timeoutMs, intervalMs, onWait) {
270
305
  const started = Date.now();
271
306
  const fullName = name;
272
307
  while (Date.now() - started <= timeoutMs) {
308
+ onWait?.(Date.now() - started);
273
309
  const params = new URLSearchParams({
274
310
  sysparm_query: `name=${fullName}`,
275
311
  sysparm_limit: '1',
@@ -472,15 +508,24 @@ export function midserversCmd(wrap) {
472
508
  const existing = await app.sdk.list(AGENT_TABLE, new URLSearchParams({ sysparm_query: `name=${shortName}`, sysparm_fields: 'sys_id,name', sysparm_limit: '1' }));
473
509
  if (existing.length) throw new Error(`MID '${shortName}' already exists in ServiceNow; choose a new name. Existing runtime and record were not changed.`);
474
510
  const installation = await installDirectMidServer({
475
- instanceURL: app.getEffectiveInstance(), ...credentials, name, directory: argv.directory, runNow: decision === 'start',
511
+ instanceURL: app.getEffectiveInstance(), ...credentials, name, directory: argv.directory,
512
+ runNow: decision === 'start', onProgress: directProgress(app),
476
513
  });
477
514
  if (installation.state === 'prepared') {
478
515
  app.ok(installation, { summary: `Prepared native MID '${installation.name}' in ${installation.installDir}; not started or registered. Start with jsn midservers start ${installation.name}${argv.directory ? ' --directory ' + JSON.stringify(installation.installDir) : ''}. Windows start runs in the foreground console; no service is installed.` });
479
516
  return;
480
517
  }
481
518
  let record;
482
- try { record = await waitForMidServerUp(app, installation.name, Math.max(1, argv['wait-timeout']), Math.max(500, argv['wait-interval'])); }
519
+ const directWait = Math.max(1, argv['wait-timeout']);
520
+ const directInterval = Math.max(500, argv['wait-interval']);
521
+ const onWait = registrationProgress(app, directWait);
522
+ const started = Date.now();
523
+ try {
524
+ record = await waitForMidServerUp(app, installation.name, directWait, directInterval, onWait);
525
+ onWait?.(directWait);
526
+ }
483
527
  catch {
528
+ onWait?.(Math.max(directWait, Date.now() - started));
484
529
  app.ok({ ...installation, registration: 'pending', validated: false }, { summary: `Native MID start command completed; registration is pending. Runtime retained for diagnostics at ${installation.installDir}; no forced teardown.` });
485
530
  return;
486
531
  }
@@ -71,20 +71,28 @@ function checkParents(directory, platform) {
71
71
  }
72
72
  }
73
73
 
74
- export async function downloadMidZip(url, archive, fetchImpl = fetch) {
74
+ export async function downloadMidZip(url, archive, fetchImpl = fetch, onProgress) {
75
75
  const response = await fetchImpl(url, { redirect: 'error', signal: AbortSignal.timeout(600000) });
76
76
  if (!response.ok || !response.body) throw new Error(`MID ZIP download failed (HTTP ${response.status})`);
77
77
  const length = Number(response.headers.get('content-length'));
78
78
  if (length > MAX_ARCHIVE) throw new Error('MID ZIP exceeds 1 GiB download limit');
79
+ onProgress?.({ phase: 'download', received: 0, total: Number.isFinite(length) ? length : 0 });
79
80
  const fd = fs.openSync(archive, 'wx', 0o600);
80
81
  let size = 0;
82
+ let reported = 0;
83
+ const step = Math.max(4 * 1024 * 1024, Math.floor((Number.isFinite(length) ? length : MAX_ARCHIVE) / 100));
81
84
  try {
82
85
  for await (const chunk of response.body) {
83
86
  size += chunk.length;
84
87
  if (size > MAX_ARCHIVE) throw new Error('MID ZIP exceeds 1 GiB download limit');
85
88
  fs.writeSync(fd, chunk);
89
+ if (onProgress && size - reported >= step) {
90
+ reported = size;
91
+ onProgress({ phase: 'download', received: size, total: Number.isFinite(length) ? length : 0 });
92
+ }
86
93
  }
87
94
  if (length && size !== length) throw new Error('Incomplete MID ZIP download');
95
+ onProgress?.({ phase: 'download', received: size, total: size, done: true });
88
96
  } finally { fs.closeSync(fd); }
89
97
  }
90
98
 
@@ -189,7 +197,7 @@ export function removeDirectInstallation(metadata, run = defaultRun) {
189
197
  }
190
198
 
191
199
  export async function installDirectMidServer({ instanceURL, username, password, name, directory, releaseName: suppliedRelease,
192
- runNow = false, platform = process.platform, arch = process.arch, fetchImpl = fetch, download = downloadMidZip,
200
+ runNow = false, platform = process.platform, arch = process.arch, fetchImpl = fetch, download = downloadMidZip, onProgress,
193
201
  protect = protectDirectory, run = defaultRun }) {
194
202
  const native = detectDirectPlatform(platform, arch);
195
203
  if (native === 'linux' && process.getuid?.() === 0) throw new Error('Run direct MID installation as a non-root user');
@@ -216,9 +224,12 @@ export async function installDirectMidServer({ instanceURL, username, password,
216
224
  let prepared = false;
217
225
  try {
218
226
  protect(installDir, native);
219
- await download(archiveURL, archive, fetchImpl);
227
+ onProgress?.({ phase: 'download-start', archiveURL, platform: native, installDir, name: shortName });
228
+ await download(archiveURL, archive, fetchImpl, onProgress);
220
229
  const entries = inspectMidZip(archive);
230
+ onProgress?.({ phase: 'extract-start', entries: entries.length, installDir });
221
231
  await extractMidZip(archive, installDir, entries);
232
+ onProgress?.({ phase: 'extract-done', entries: entries.length, installDir });
222
233
  const agent = path.join(installDir, 'agent');
223
234
  for (const filename of ['config.xml', 'conf/wrapper-override.conf', native === 'linux' ? 'bin/mid.sh' : 'bin/mid.bat']) {
224
235
  if (!fs.statSync(path.join(agent, filename)).isFile()) throw new Error(`Vendor ZIP missing ${filename}`);