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

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.36",
3
+ "version": "4.3.7-dev.37",
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": {
@@ -7,7 +7,7 @@ import { FormatAuto } from '../output.js';
7
7
  import { declareCapabilities } from '../capabilities.js';
8
8
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
9
9
  import { detectDocker, defaultRun, installDockerMidServer, midInstallDirectory, normalizeMidName, validateMidCredentials } from '../midserver-install.js';
10
- import { detectDirectPlatform, installDirectMidServer, readDirectInstallation, removeDirectInstallation, runDirectInstallation } from '../midserver-direct.js';
10
+ import { detectDirectPlatform, followDirectLog, installDirectMidServer, readDirectInstallation, removeDirectInstallation, resolveDirectLogFile, runDirectInstallation } from '../midserver-direct.js';
11
11
 
12
12
  const AGENT_TABLE = 'ecc_agent';
13
13
  const QUEUE_TABLE = 'ecc_queue';
@@ -344,14 +344,13 @@ function removeLocalMidServer(instanceURL, name, directory) {
344
344
  return true;
345
345
  }
346
346
 
347
- function showMidServerLogs(app, identifier, argv) {
347
+ async function showMidServerLogs(app, identifier, argv) {
348
348
  const installDir = argv.directory || midInstallDirectory(app.getEffectiveInstance(), identifier);
349
349
  const direct = readDirectInstallation(app.getEffectiveInstance(), identifier, argv.directory);
350
350
  if (direct) {
351
351
  if (!Number.isInteger(argv.tail) || argv.tail < 1 || argv.tail > 10000) throw new Error('Use --tail between 1 and 10000');
352
- if (argv.follow) throw new Error('Direct log follow is not supported; read logs again or use the native host log viewer');
353
- const logfile = path.join(installDir, 'agent', 'logs', 'agent0.log');
354
- if (!fs.existsSync(logfile)) throw new Error('No MID agent log yet; the installation may only be prepared');
352
+ const logfile = resolveDirectLogFile(installDir);
353
+ if (!logfile) throw new Error('No MID agent log yet; the installation may only be prepared');
355
354
  const stat = fs.lstatSync(logfile);
356
355
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Untrusted MID log path');
357
356
  const fd = fs.openSync(logfile, 'r');
@@ -360,8 +359,15 @@ function showMidServerLogs(app, identifier, argv) {
360
359
  const length = Math.min(stat.size, 1024 * 1024);
361
360
  const buffer = Buffer.alloc(length);
362
361
  fs.readSync(fd, buffer, 0, length, stat.size - length);
363
- text = buffer.toString('utf8').split('\n').slice(-argv.tail).join('\n');
362
+ const all = buffer.toString('utf8').split('\n');
363
+ if (all.length && all[all.length - 1] === '') all.pop();
364
+ text = all.slice(-argv.tail).join('\n');
364
365
  } finally { fs.closeSync(fd); }
366
+ if (argv.follow) {
367
+ // Following streams new lines directly; the JSON envelope would be unreachable.
368
+ process.stdout.write(`Tailing ${logfile} (Ctrl+C to stop)\n${text}\n`);
369
+ return followDirectLog(installDir);
370
+ }
365
371
  app.ok({ installation: 'direct', logfile, text, _formatted: `${text}\n` }, { summary: 'Local MID agent log (last 1 MiB maximum)' });
366
372
  return;
367
373
  }
@@ -413,10 +419,10 @@ export function midserversCmd(wrap) {
413
419
  describe: 'Show local native or Docker Compose MID logs',
414
420
  builder: (b) => b
415
421
  .option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
416
- .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs (Compose only)' })
422
+ .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
417
423
  .option('directory', { type: 'string', describe: 'Local installation directory override' }),
418
424
  handler: wrap(async (argv, app) => {
419
- showMidServerLogs(app, argv.identifier, argv);
425
+ await showMidServerLogs(app, argv.identifier, argv);
420
426
  }),
421
427
  })
422
428
  .command({
@@ -424,10 +430,10 @@ export function midserversCmd(wrap) {
424
430
  describe: 'Show local native or Docker Compose MID logs',
425
431
  builder: (b) => b
426
432
  .option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
427
- .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs (Compose only)' })
433
+ .option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
428
434
  .option('directory', { type: 'string', describe: 'Local installation directory override' }),
429
435
  handler: wrap(async (argv, app) => {
430
- showMidServerLogs(app, argv.identifier, argv);
436
+ await showMidServerLogs(app, argv.identifier, argv);
431
437
  }),
432
438
  })
433
439
  .command({
@@ -153,6 +153,55 @@ export function readDirectInstallation(instanceURL, name, directory) {
153
153
  return metadata;
154
154
  }
155
155
 
156
+ // The vendor rotates agent0.log to agent0.log.0 (and upward), so the plain
157
+ // name may not exist on a healthy install. Newest real file wins; links and
158
+ // non-files are never followed.
159
+ export function resolveDirectLogFile(installDir) {
160
+ const directory = path.join(installDir, 'agent', 'logs');
161
+ let names;
162
+ try { names = fs.readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
163
+ const candidates = [];
164
+ for (const name of names) {
165
+ const agent = /^agent0\.log(\.\d+)?$/.test(name);
166
+ if (!agent && name !== 'wrapper.log') continue;
167
+ const full = path.join(directory, name);
168
+ const stat = fs.lstatSync(full);
169
+ if (!stat.isFile() || stat.isSymbolicLink()) continue;
170
+ candidates.push({ full, agent, mtime: stat.mtimeMs });
171
+ }
172
+ candidates.sort((a, b) => (b.agent - a.agent) || (b.mtime - a.mtime));
173
+ return candidates.length ? candidates[0].full : null;
174
+ }
175
+
176
+ /**
177
+ * Tail a direct install's agent log, following rotation.
178
+ *
179
+ * The vendor rotates by creating a new file, so the followed path can change
180
+ * between polls; a shrinking file is treated as truncation and re-read from
181
+ * the start. Each appended chunk is handed to `writer` unchanged.
182
+ */
183
+ export async function followDirectLog(installDir, { writer = chunk => process.stdout.write(chunk), intervalMs = 1000, iterations = Infinity, sleep } = {}) {
184
+ const pause = sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
185
+ let file = resolveDirectLogFile(installDir);
186
+ if (!file) throw new Error('No MID agent log yet; the installation may only be prepared');
187
+ let offset = fs.statSync(file).size;
188
+ for (let seen = 0; seen < iterations; seen++) {
189
+ await pause(intervalMs);
190
+ const next = resolveDirectLogFile(installDir) || file;
191
+ if (next !== file) { file = next; offset = 0; }
192
+ const stat = fs.lstatSync(file);
193
+ if (!stat.isFile() || stat.isSymbolicLink()) return;
194
+ if (stat.size < offset) offset = 0;
195
+ if (stat.size === offset) continue;
196
+ const length = stat.size - offset;
197
+ const buffer = Buffer.alloc(length);
198
+ const fd = fs.openSync(file, 'r');
199
+ try { fs.readSync(fd, buffer, 0, length, offset); } finally { fs.closeSync(fd); }
200
+ offset = stat.size;
201
+ writer(buffer.toString('utf8'));
202
+ }
203
+ }
204
+
156
205
  function uncertainState(metadata) {
157
206
  return `MID '${metadata.name}' runtime state is uncertain after an interrupted lifecycle command. Confirm no vendor MID process from ${metadata.installDir} is running, then rerun; installed files are kept.`;
158
207
  }