@microwiseai/snapshot 0.5.0 → 0.5.2

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.
Files changed (113) hide show
  1. package/dist/commands/install.d.ts.map +1 -1
  2. package/dist/commands/install.js +195 -34
  3. package/dist/commands/install.js.map +1 -1
  4. package/dist/commands/preflight.d.ts +31 -0
  5. package/dist/commands/preflight.d.ts.map +1 -0
  6. package/dist/commands/preflight.js +83 -0
  7. package/dist/commands/preflight.js.map +1 -0
  8. package/dist/commands/publish.d.ts +4 -0
  9. package/dist/commands/publish.d.ts.map +1 -1
  10. package/dist/commands/publish.js +35 -1
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/index.js +13 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/distkit-closure-bridge.d.ts +102 -0
  15. package/dist/lib/distkit-closure-bridge.d.ts.map +1 -0
  16. package/dist/lib/distkit-closure-bridge.js +177 -0
  17. package/dist/lib/distkit-closure-bridge.js.map +1 -0
  18. package/dist/lib/distkit-closure-parity.d.ts +110 -0
  19. package/dist/lib/distkit-closure-parity.d.ts.map +1 -0
  20. package/dist/lib/distkit-closure-parity.js +134 -0
  21. package/dist/lib/distkit-closure-parity.js.map +1 -0
  22. package/dist/lib/distkit-routing-port.d.ts +37 -0
  23. package/dist/lib/distkit-routing-port.d.ts.map +1 -0
  24. package/dist/lib/distkit-routing-port.js +72 -0
  25. package/dist/lib/distkit-routing-port.js.map +1 -0
  26. package/dist/lib/install-timeline.d.ts +81 -0
  27. package/dist/lib/install-timeline.d.ts.map +1 -0
  28. package/dist/lib/install-timeline.js +243 -0
  29. package/dist/lib/install-timeline.js.map +1 -0
  30. package/dist/lib/preflight-adapter.d.ts +52 -0
  31. package/dist/lib/preflight-adapter.d.ts.map +1 -0
  32. package/dist/lib/preflight-adapter.js +224 -0
  33. package/dist/lib/preflight-adapter.js.map +1 -0
  34. package/dist/lib/transitive-resolver.d.ts +121 -0
  35. package/dist/lib/transitive-resolver.d.ts.map +1 -1
  36. package/dist/lib/transitive-resolver.js +190 -0
  37. package/dist/lib/transitive-resolver.js.map +1 -1
  38. package/node_modules/@gitdot/clone-lib/README.md +117 -0
  39. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts +9 -0
  40. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts.map +1 -0
  41. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js +154 -0
  42. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js.map +1 -0
  43. package/node_modules/@gitdot/clone-lib/dist/clone.d.ts +21 -0
  44. package/node_modules/@gitdot/clone-lib/dist/clone.d.ts.map +1 -0
  45. package/node_modules/@gitdot/clone-lib/dist/clone.js +228 -0
  46. package/node_modules/@gitdot/clone-lib/dist/clone.js.map +1 -0
  47. package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts +10 -0
  48. package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts.map +1 -0
  49. package/node_modules/@gitdot/clone-lib/dist/dotpath.js +27 -0
  50. package/node_modules/@gitdot/clone-lib/dist/dotpath.js.map +1 -0
  51. package/node_modules/@gitdot/clone-lib/dist/git.d.ts +23 -0
  52. package/node_modules/@gitdot/clone-lib/dist/git.d.ts.map +1 -0
  53. package/node_modules/@gitdot/clone-lib/dist/git.js +134 -0
  54. package/node_modules/@gitdot/clone-lib/dist/git.js.map +1 -0
  55. package/node_modules/@gitdot/clone-lib/dist/index.d.ts +17 -0
  56. package/node_modules/@gitdot/clone-lib/dist/index.d.ts.map +1 -0
  57. package/node_modules/@gitdot/clone-lib/dist/index.js +35 -0
  58. package/node_modules/@gitdot/clone-lib/dist/index.js.map +1 -0
  59. package/node_modules/@gitdot/clone-lib/dist/lock.d.ts +24 -0
  60. package/node_modules/@gitdot/clone-lib/dist/lock.d.ts.map +1 -0
  61. package/node_modules/@gitdot/clone-lib/dist/lock.js +97 -0
  62. package/node_modules/@gitdot/clone-lib/dist/lock.js.map +1 -0
  63. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts +15 -0
  64. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts.map +1 -0
  65. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js +124 -0
  66. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js.map +1 -0
  67. package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts +12 -0
  68. package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts.map +1 -0
  69. package/node_modules/@gitdot/clone-lib/dist/resolve/index.js +31 -0
  70. package/node_modules/@gitdot/clone-lib/dist/resolve/index.js.map +1 -0
  71. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts +32 -0
  72. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts.map +1 -0
  73. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js +159 -0
  74. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js.map +1 -0
  75. package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts +44 -0
  76. package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts.map +1 -0
  77. package/node_modules/@gitdot/clone-lib/dist/resolve/member.js +163 -0
  78. package/node_modules/@gitdot/clone-lib/dist/resolve/member.js.map +1 -0
  79. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts +18 -0
  80. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts.map +1 -0
  81. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js +155 -0
  82. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js.map +1 -0
  83. package/node_modules/@gitdot/clone-lib/dist/types.d.ts +225 -0
  84. package/node_modules/@gitdot/clone-lib/dist/types.d.ts.map +1 -0
  85. package/node_modules/@gitdot/clone-lib/dist/types.js +9 -0
  86. package/node_modules/@gitdot/clone-lib/dist/types.js.map +1 -0
  87. package/node_modules/@gitdot/clone-lib/package.json +53 -0
  88. package/node_modules/@microwiseai/install-preflight/dist/index.d.ts +27 -0
  89. package/node_modules/@microwiseai/install-preflight/dist/index.d.ts.map +1 -0
  90. package/node_modules/@microwiseai/install-preflight/dist/index.js +26 -0
  91. package/node_modules/@microwiseai/install-preflight/dist/index.js.map +1 -0
  92. package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts +42 -0
  93. package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts.map +1 -0
  94. package/node_modules/@microwiseai/install-preflight/dist/policy.js +79 -0
  95. package/node_modules/@microwiseai/install-preflight/dist/policy.js.map +1 -0
  96. package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts +18 -0
  97. package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts.map +1 -0
  98. package/node_modules/@microwiseai/install-preflight/dist/preflight.js +0 -0
  99. package/node_modules/@microwiseai/install-preflight/dist/preflight.js.map +1 -0
  100. package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts +26 -0
  101. package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts.map +1 -0
  102. package/node_modules/@microwiseai/install-preflight/dist/resolvers.js +63 -0
  103. package/node_modules/@microwiseai/install-preflight/dist/resolvers.js.map +1 -0
  104. package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts +45 -0
  105. package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts.map +1 -0
  106. package/node_modules/@microwiseai/install-preflight/dist/semver.js +195 -0
  107. package/node_modules/@microwiseai/install-preflight/dist/semver.js.map +1 -0
  108. package/node_modules/@microwiseai/install-preflight/dist/types.d.ts +136 -0
  109. package/node_modules/@microwiseai/install-preflight/dist/types.d.ts.map +1 -0
  110. package/node_modules/@microwiseai/install-preflight/dist/types.js +11 -0
  111. package/node_modules/@microwiseai/install-preflight/dist/types.js.map +1 -0
  112. package/node_modules/@microwiseai/install-preflight/package.json +49 -0
  113. package/package.json +12 -4
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAiBjE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAkeD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,KAAK,GAAE,MAAU,EACjB,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAM,EACtD,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAAE,CAAC,CA8VjF;AA4FD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAoLD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuZrG"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAwBjE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAsmBD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,KAAK,GAAE,MAAU,EACjB,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAM,EACtD,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAAE,CAAC,CA8VjF;AA4FD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAoLD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CA0brG"}
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { execSync } from 'child_process';
3
- import { writeFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
2
+ import { execSync, spawn } from 'child_process';
3
+ import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'fs';
4
4
  import { homedir, platform, arch } from 'os';
5
5
  import { join } from 'path';
6
6
  import { fetchSnapshotFromRepo, snapshotNameToRepoName } from '../lib/gitlab.js';
@@ -14,6 +14,12 @@ import { authorizeSession, packagesToSpecs } from '../lib/session.js';
14
14
  import { parallelInstall, recordToEntries } from '@microwiseai/snapshot-parallel';
15
15
  import { dedupWithReport, dedupInstallBlock, recordInstalledSpecs, recordReferences, computeReplacePlan, applyReplaceHistory, } from '@microwiseai/snapshot-dedup';
16
16
  import { createInterface } from 'readline';
17
+ import { createInstallTimeline } from '../lib/install-timeline.js';
18
+ // Install-timeline instrumentation (investigation only, env opt-in). A disabled
19
+ // no-op is the default so module-level callers (runCommand, installSkitPackages)
20
+ // always have a valid handle; installCommand() reassigns a live instance when
21
+ // SNAPSHOT_INSTALL_TIMELINE is set. Adds zero behavior — only measurement.
22
+ let activeTimeline = createInstallTimeline({ enabled: false });
17
23
  // CLI version — dynamically read from package.json (SSOT)
18
24
  import { createRequire } from 'module';
19
25
  const _require = createRequire(import.meta.url);
@@ -194,27 +200,74 @@ function commandExists(cmd) {
194
200
  // Captures the failure reason of the most recent runCommand() call so callers
195
201
  // can surface it (debugging visibility). Undefined when the last call succeeded.
196
202
  let lastCommandError;
203
+ // Bound the captured-stderr buffer so a chatty command can't grow it without
204
+ // limit; on overflow we keep the tail (where the actual failure usually is).
205
+ const MAX_STDERR_CAPTURE = 64 * 1024;
206
+ // Runs a shell command, streaming its output live while still capturing stderr
207
+ // for failure diagnostics.
208
+ //
209
+ // Why spawn (async) instead of execSync: long installs (e.g. `npm install -g`
210
+ // of the whole pre-install batch) push their live progress to STDERR. execSync
211
+ // with a piped stderr hid that progress entirely — the terminal sat silent for
212
+ // minutes, then dumped a summary. Here stderr is piped so we can BOTH tee it to
213
+ // our own stderr in real time (visible progress) AND accumulate it, so the raw
214
+ // reason is still available via lastCommandError when the command fails.
215
+ //
216
+ // stdout stays inherited (live, unbuffered). shell:true preserves the previous
217
+ // execSync parsing of the full command string.
197
218
  function runCommand(command) {
198
219
  lastCommandError = undefined;
199
- try {
200
- // Keep stdout visible live, but capture stderr so we can surface the raw
201
- // error reason instead of swallowing it on failure.
202
- execSync(command, { stdio: ['inherit', 'inherit', 'pipe'] });
203
- return true;
204
- }
205
- catch (err) {
206
- const exitCode = err?.status;
207
- const stderr = err?.stderr?.toString().trim();
208
- const message = err?.message ? String(err.message).trim() : undefined;
209
- const detail = stderr || message || 'unknown error';
210
- lastCommandError = `exit code ${exitCode ?? 'unknown'} — ${detail}`;
211
- // Surface the raw failure (stderr is no longer shown live since it is piped).
212
- console.log(chalk.red(` Command failed (exit code ${exitCode ?? 'unknown'}): ${command}`));
213
- if (detail) {
214
- console.log(chalk.red(detail));
215
- }
216
- return false;
217
- }
220
+ // Instrumentation only: the timeline sanitizes the command label internally,
221
+ // so tokens are never recorded. No effect when the timeline is disabled.
222
+ const span = activeTimeline.start(command, { category: 'command' });
223
+ return new Promise((resolve) => {
224
+ const child = spawn(command, {
225
+ shell: true,
226
+ stdio: ['inherit', 'inherit', 'pipe'],
227
+ });
228
+ let stderrBuf = '';
229
+ child.stderr?.on('data', (chunk) => {
230
+ // Live streaming: echo the command's progress as it happens.
231
+ process.stderr.write(chunk);
232
+ // Capture (bounded) for failure diagnostics.
233
+ stderrBuf += chunk.toString();
234
+ if (stderrBuf.length > MAX_STDERR_CAPTURE) {
235
+ stderrBuf = stderrBuf.slice(-MAX_STDERR_CAPTURE);
236
+ }
237
+ });
238
+ // 'error' (spawn failed) and 'close' (process exited) are mutually exclusive
239
+ // in practice, but guard against a double-resolve just in case.
240
+ let settled = false;
241
+ const finish = (ok) => {
242
+ if (settled)
243
+ return;
244
+ settled = true;
245
+ resolve(ok);
246
+ };
247
+ child.on('error', (err) => {
248
+ // Spawn-level failure (e.g. shell missing) — no exit code available.
249
+ lastCommandError = `spawn error — ${err.message}`;
250
+ span.end({ success: false, exitReason: 'spawn error' });
251
+ console.log(chalk.red(` Command failed to start: ${command}`));
252
+ console.log(chalk.red(err.message));
253
+ finish(false);
254
+ });
255
+ child.on('close', (code) => {
256
+ if (code === 0) {
257
+ span.end({ success: true });
258
+ finish(true);
259
+ return;
260
+ }
261
+ const detail = stderrBuf.trim() || 'unknown error';
262
+ lastCommandError = `exit code ${code ?? 'unknown'} — ${detail}`;
263
+ span.end({ success: false, exitReason: `exit code ${code ?? 'unknown'}` });
264
+ // stderr was already streamed live above, so we don't re-print the full
265
+ // detail here (that would duplicate it). Callers surface lastCommandError
266
+ // where they want a compact "Reason:" line.
267
+ console.log(chalk.red(` Command failed (exit code ${code ?? 'unknown'}): ${command}`));
268
+ finish(false);
269
+ });
270
+ });
218
271
  }
219
272
  /**
220
273
  * Check if a skit package is already installed with the specified version
@@ -247,6 +300,71 @@ function isNpmGlobalInstalled(name, version) {
247
300
  return false;
248
301
  }
249
302
  }
303
+ // Preflight is a diagnostic: it runs once per process and warns loudly, but
304
+ // does NOT abort — skit still tries, so any real error path stays intact.
305
+ let istWritabilityChecked = false;
306
+ /**
307
+ * Verify the current user can actually write into ~/.ist before skit installs.
308
+ *
309
+ * Motivation: when ~/.ist is mounted as a root:root Docker named volume and the
310
+ * process runs as a non-root user, skit's writes fail with EACCES. skit swallows
311
+ * that into an opaque "Error: undefined" and the install silently under-counts
312
+ * (e.g. 7/12). This surfaces the real cause up front with a concrete fix.
313
+ *
314
+ * Detection is a real write probe (create+delete a temp file), which is more
315
+ * reliable than fs.accessSync(W_OK) — the latter can misreport under root/ACLs.
316
+ * Missing directories are created first (that itself exercises writability of
317
+ * the parent). Never throws; a probe failure just flags the directory.
318
+ */
319
+ function checkIstWritable() {
320
+ if (istWritabilityChecked)
321
+ return;
322
+ istWritabilityChecked = true;
323
+ const istRoot = join(homedir(), '.ist');
324
+ // Directories skit writes into: the root itself (installed-kits.json lives
325
+ // here), global prompts, and profiles.
326
+ const targets = [
327
+ istRoot,
328
+ join(istRoot, 'prompts', 'global'),
329
+ join(istRoot, 'profiles'),
330
+ ];
331
+ const notWritable = [];
332
+ for (const dir of targets) {
333
+ try {
334
+ if (!existsSync(dir)) {
335
+ // Creating the dir also proves the parent is writable. If this throws
336
+ // (EACCES on a root-owned volume), the dir is not usable → flag it.
337
+ mkdirSync(dir, { recursive: true });
338
+ }
339
+ const probe = join(dir, `.snapshot-write-probe-${process.pid}`);
340
+ writeFileSync(probe, '');
341
+ unlinkSync(probe);
342
+ }
343
+ catch {
344
+ notWritable.push(dir);
345
+ }
346
+ }
347
+ // installed-kits.json may already exist with restrictive ownership even when
348
+ // its directory is writable — probe the file directly if present.
349
+ const kitsFile = join(istRoot, 'installed-kits.json');
350
+ if (existsSync(kitsFile)) {
351
+ try {
352
+ writeFileSync(kitsFile, readFileSync(kitsFile));
353
+ }
354
+ catch {
355
+ notWritable.push(kitsFile);
356
+ }
357
+ }
358
+ if (notWritable.length > 0) {
359
+ console.log('');
360
+ console.log(chalk.yellow('⚠ ~/.ist is not writable by current user (owned by root?) — skit installs will fail.'));
361
+ for (const p of notWritable) {
362
+ console.log(chalk.gray(` not writable: ${p}`));
363
+ }
364
+ console.log(chalk.gray(' Fix directory ownership: chown -R $(whoami) ~/.ist'));
365
+ console.log('');
366
+ }
367
+ }
250
368
  /**
251
369
  * Skit 패키지 설치 (lib 우선, CLI fallback)
252
370
  * @ist/skit lib가 있으면 직접 호출, 없으면 CLI 사용
@@ -257,6 +375,10 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
257
375
  if (packageEntries.length === 0) {
258
376
  return result;
259
377
  }
378
+ // Diagnostic preflight (once/process): if ~/.ist isn't writable by the current
379
+ // user, skit installs fail opaquely ("Error: undefined", under-counted total).
380
+ // Warn with the concrete fix before we start the skit loop.
381
+ checkIstWritable();
260
382
  const total = packageEntries.length;
261
383
  console.log('');
262
384
  console.log(chalk.yellow(`${indent}=== ${snapshotName}: ${total} skits ===`));
@@ -269,7 +391,14 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
269
391
  if (isSkitInstalled(entry.name, entry.version)) {
270
392
  return { success: true, skipped: true };
271
393
  }
394
+ // Per-package timing (investigation only) — surfaces individual skit
395
+ // hotspots (e.g. infra-kit/mwd-kit) under the install-snapshot phase.
396
+ const pkgSpan = activeTimeline.start(`${entry.name}@${entry.version}`, {
397
+ category: 'package',
398
+ meta: { installer: 'skit' },
399
+ });
272
400
  const installResult = await installSkitPackage(entry.name, entry.version, skitOptions);
401
+ pkgSpan.end({ success: installResult.success });
273
402
  return {
274
403
  success: installResult.success,
275
404
  error: installResult.error,
@@ -321,7 +450,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
321
450
  if (!commandExists(cmdName)) {
322
451
  console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
323
452
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
324
- if (runCommand(installerConfig.hint)) {
453
+ if (await runCommand(installerConfig.hint)) {
325
454
  console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
326
455
  }
327
456
  else {
@@ -354,7 +483,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
354
483
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
355
484
  const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
356
485
  console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
357
- const success = runCommand(batchCommand);
486
+ const success = await runCommand(batchCommand);
358
487
  if (success) {
359
488
  for (const spec of specs) {
360
489
  console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -369,7 +498,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
369
498
  for (const entry of toInstall) {
370
499
  const pkgSpec = `${entry.name}@${entry.version}`;
371
500
  const command = installerConfig.command.replace('{pkg}', pkgSpec);
372
- const ok = runCommand(command);
501
+ const ok = await runCommand(command);
373
502
  if (ok) {
374
503
  console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
375
504
  result.installed.push(pkgSpec);
@@ -387,7 +516,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
387
516
  const parallelResult = await parallelInstall(entries, async (entry) => {
388
517
  const pkgSpec = `${entry.name}@${entry.version}`;
389
518
  const command = installerConfig.command.replace('{pkg}', pkgSpec);
390
- const success = runCommand(command);
519
+ const success = await runCommand(command);
391
520
  return { success, error: success ? undefined : `Command failed: ${command}` };
392
521
  }, {
393
522
  concurrency: 4,
@@ -562,7 +691,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
562
691
  if (!commandExists(cmdName)) {
563
692
  console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
564
693
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
565
- if (runCommand(installerConfig.hint)) {
694
+ if (await runCommand(installerConfig.hint)) {
566
695
  console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
567
696
  }
568
697
  else {
@@ -596,7 +725,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
596
725
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
597
726
  const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
598
727
  console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
599
- const success = runCommand(batchCommand);
728
+ const success = await runCommand(batchCommand);
600
729
  if (success) {
601
730
  for (const spec of specs) {
602
731
  console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -611,7 +740,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
611
740
  for (const entry of toInstall) {
612
741
  const pkgSpec = `${entry.name}@${entry.version}`;
613
742
  const command = installCfg.command.replace('{pkg}', pkgSpec);
614
- const ok = runCommand(command);
743
+ const ok = await runCommand(command);
615
744
  if (ok) {
616
745
  console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
617
746
  result.installed.push(pkgSpec);
@@ -629,7 +758,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
629
758
  const parallelResult = await parallelInstall(installEntries, async (entry) => {
630
759
  const pkgSpec = `${entry.name}@${entry.version}`;
631
760
  const command = installCfg.command.replace('{pkg}', pkgSpec);
632
- const success = runCommand(command);
761
+ const success = await runCommand(command);
633
762
  return { success, error: success ? undefined : `Command failed: ${command}` };
634
763
  }, {
635
764
  concurrency: 4,
@@ -659,7 +788,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
659
788
  if (!commandExists(cmdName)) {
660
789
  console.log(chalk.yellow(`${indent}glpkg not found. Attempting auto-install...`));
661
790
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
662
- if (runCommand(installerConfig.hint)) {
791
+ if (await runCommand(installerConfig.hint)) {
663
792
  console.log(chalk.green(`${indent} ✓ glpkg installed successfully`));
664
793
  }
665
794
  else {
@@ -677,7 +806,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
677
806
  const parallelResult = await parallelInstall(legacyEntries, async (entry) => {
678
807
  const pkgSpec = `${entry.name}@${entry.version}`;
679
808
  const command = legacyCfg.command.replace('{pkg}', pkgSpec);
680
- const success = runCommand(command);
809
+ const success = await runCommand(command);
681
810
  return { success, error: success ? undefined : `Command failed: ${command}` };
682
811
  }, {
683
812
  concurrency: 4,
@@ -888,7 +1017,7 @@ async function runReplaceMode(snapshotName, newSet, options, installers) {
888
1017
  continue;
889
1018
  }
890
1019
  console.log(chalk.gray(` Removing ${item.name}...`));
891
- if (runCommand(cmd)) {
1020
+ if (await runCommand(cmd)) {
892
1021
  console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
893
1022
  removed.push(item);
894
1023
  }
@@ -949,6 +1078,10 @@ async function previewInstall(snapshot, options) {
949
1078
  console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
950
1079
  }
951
1080
  export async function installCommand(name, options = {}) {
1081
+ // Install-timeline instrumentation: live instance when SNAPSHOT_INSTALL_TIMELINE
1082
+ // is set, otherwise a zero-overhead no-op. Purely additive — never alters the
1083
+ // install path or its output when disabled.
1084
+ activeTimeline = createInstallTimeline();
952
1085
  console.log(chalk.blue.bold(`
953
1086
  ╔══════════════════════════════════════════════════════════════╗
954
1087
  ║ Snapshot Installer ║
@@ -982,6 +1115,7 @@ export async function installCommand(name, options = {}) {
982
1115
  //
983
1116
  // License key CANNOT be used as Bearer token for registry-proxy — they are
984
1117
  // distinct: AuthN (uid via IST JWT) vs AuthZ (entitlement via license key).
1118
+ const authSpan = activeTimeline.start('auth-token', { category: 'phase' });
985
1119
  const licenseKey = getLicenseKey();
986
1120
  let authToken = null;
987
1121
  let authType = 'anonymous';
@@ -998,13 +1132,16 @@ export async function installCommand(name, options = {}) {
998
1132
  console.log(chalk.yellow('⚠') + ' No auth token — anonymous access');
999
1133
  }
1000
1134
  const useProxy = true;
1135
+ authSpan.end({ authType });
1001
1136
  // Fetch snapshot first — access check depends on snapshot.json content
1002
1137
  console.log(chalk.gray('Fetching snapshot...'));
1138
+ const fetchSpan = activeTimeline.start('fetch-snapshot', { category: 'phase' });
1003
1139
  let snapshot;
1004
1140
  try {
1005
1141
  snapshot = await fetchSnapshotFromRepo(name, authToken);
1006
1142
  }
1007
1143
  catch (error) {
1144
+ fetchSpan.end({ success: false });
1008
1145
  const errMsg = error instanceof Error ? error.message : String(error);
1009
1146
  if (options.dryRun) {
1010
1147
  // Pure preview: a fetch failure (offline / no auth) is not fatal — we just
@@ -1019,6 +1156,7 @@ export async function installCommand(name, options = {}) {
1019
1156
  console.log(chalk.yellow('Check the snapshot name and version, e.g. snapshot install @scope/name@version'));
1020
1157
  process.exit(1);
1021
1158
  }
1159
+ fetchSpan.end({ success: true });
1022
1160
  console.log(chalk.green('✓') + ` Snapshot: ${snapshot.name} v${snapshot.version}`);
1023
1161
  if (snapshot.description) {
1024
1162
  console.log(chalk.gray(` ${snapshot.description}`));
@@ -1051,6 +1189,10 @@ export async function installCommand(name, options = {}) {
1051
1189
  // the authToken injection point (IST_SESSION_TOKEN env / getProxyAuthToken).
1052
1190
  const istCredentials = getIstCredentials();
1053
1191
  // --- License Check ---
1192
+ const accessSpan = activeTimeline.start('access-license', {
1193
+ category: 'phase',
1194
+ meta: { access: isPublicAccess ? 'public' : isLicensedAccess ? 'licensed' : 'server-decided' },
1195
+ });
1054
1196
  const storedLicenseKey = getLicenseKey();
1055
1197
  let licenseValid = false;
1056
1198
  if (isPublicAccess) {
@@ -1097,8 +1239,10 @@ export async function installCommand(name, options = {}) {
1097
1239
  }
1098
1240
  console.log(chalk.green('✓') + ` License verified for ${productId}`);
1099
1241
  }
1242
+ accessSpan.end({ licenseValid });
1100
1243
  // =======================================================================
1101
1244
  // ========== Authorize Session with Registry Proxy ==========
1245
+ const authorizeSpan = activeTimeline.start('authorize-session', { category: 'phase' });
1102
1246
  let sessionId;
1103
1247
  if (authToken) {
1104
1248
  // Collect all packages for session authorization
@@ -1134,8 +1278,10 @@ export async function installCommand(name, options = {}) {
1134
1278
  }
1135
1279
  }
1136
1280
  }
1281
+ authorizeSpan.end({ sessionAuthorized: sessionId !== undefined });
1137
1282
  // =============================================================
1138
1283
  // Collect all packages from entire extends chain to determine scopes
1284
+ const configureSpan = activeTimeline.start('configure-npmrc', { category: 'phase' });
1139
1285
  const allPackages = await collectAllPackages(snapshot);
1140
1286
  // Configure .npmrc for proxy (with or without auth token)
1141
1287
  let npmrcBackup = null;
@@ -1155,6 +1301,7 @@ export async function installCommand(name, options = {}) {
1155
1301
  }
1156
1302
  }
1157
1303
  }
1304
+ configureSpan.end({ packageCount: Object.keys(allPackages).length });
1158
1305
  // Guarantee .npmrc restoration even on ^C. A try/finally only covers thrown
1159
1306
  // errors and normal returns — it does NOT run on SIGINT/SIGTERM, so an
1160
1307
  // interrupt during the pre-install batch or the main install would leave a
@@ -1192,6 +1339,8 @@ export async function installCommand(name, options = {}) {
1192
1339
  // ========== Pre-install: batch all npm packages from extends chain ==========
1193
1340
  // Collect all npm-type packages across the entire extends chain and install
1194
1341
  // them in a single `npm install -g` call, instead of one per snapshot layer.
1342
+ const batchSpan = activeTimeline.start('npm-batch-preinstall', { category: 'phase' });
1343
+ let batchSpecs = [];
1195
1344
  const allNpmPackages = await collectAllNpmPackages(snapshot, useProxy);
1196
1345
  const npmEntries = Object.entries(allNpmPackages);
1197
1346
  if (npmEntries.length > 0) {
@@ -1209,9 +1358,10 @@ export async function installCommand(name, options = {}) {
1209
1358
  }
1210
1359
  if (toInstall.length > 0) {
1211
1360
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
1361
+ batchSpecs = specs;
1212
1362
  const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
1213
1363
  console.log(chalk.gray(` Running single batch: npm install -g (${specs.length} packages)`));
1214
- const batchSuccess = runCommand(npmCommand);
1364
+ const batchSuccess = await runCommand(npmCommand);
1215
1365
  if (batchSuccess) {
1216
1366
  for (const spec of specs) {
1217
1367
  console.log(` ${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -1227,7 +1377,7 @@ export async function installCommand(name, options = {}) {
1227
1377
  for (const entry of toInstall) {
1228
1378
  const pkgSpec = `${entry.name}@${entry.version}`;
1229
1379
  const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
1230
- const ok = runCommand(cmd);
1380
+ const ok = await runCommand(cmd);
1231
1381
  if (ok) {
1232
1382
  console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
1233
1383
  }
@@ -1241,11 +1391,13 @@ export async function installCommand(name, options = {}) {
1241
1391
  console.log(chalk.green('✓ All npm packages already installed'));
1242
1392
  }
1243
1393
  }
1394
+ batchSpan.end({ specs: batchSpecs, npmDeclared: npmEntries.length, npmToInstall: batchSpecs.length });
1244
1395
  // =============================================================================
1245
1396
  // Install (handles extends recursively)
1246
1397
  // installSnapshot's internal isNpmGlobalInstalled checks will skip already-installed packages
1247
1398
  let installError = null;
1248
1399
  let result = { installed: [], failed: [] };
1400
+ const installSpan = activeTimeline.start('install-snapshot', { category: 'phase' });
1249
1401
  try {
1250
1402
  // Auto-detect 모드: snapshot.install이 없으면 자동 판별
1251
1403
  const autoDetect = !snapshot.install;
@@ -1260,6 +1412,11 @@ export async function installCommand(name, options = {}) {
1260
1412
  installError = err instanceof Error ? err : new Error(String(err));
1261
1413
  }
1262
1414
  finally {
1415
+ installSpan.end({
1416
+ installed: result.installed.length,
1417
+ failed: result.failed.length,
1418
+ success: installError === null,
1419
+ });
1263
1420
  // Restore .npmrc + clear the session-token env (idempotent: a SIGINT/SIGTERM
1264
1421
  // handler may have already run this). Then drop the signal handlers we
1265
1422
  // registered, so they don't leak across multiple installs in one process.
@@ -1268,6 +1425,7 @@ export async function installCommand(name, options = {}) {
1268
1425
  process.removeListener('SIGTERM', onSignal);
1269
1426
  }
1270
1427
  if (installError) {
1428
+ activeTimeline.finalize();
1271
1429
  console.log(chalk.red(`Error during installation: ${installError.message}`));
1272
1430
  process.exit(1);
1273
1431
  }
@@ -1314,6 +1472,9 @@ export async function installCommand(name, options = {}) {
1314
1472
  await runReplaceMode(snapshot.name, allPackages, options, effectiveInstallers);
1315
1473
  }
1316
1474
  }
1475
+ // Emit the install timeline (investigation only; no-op unless opted-in) before
1476
+ // the fire-and-forget telemetry so the summary/JSONL reflect the whole run.
1477
+ activeTimeline.finalize();
1317
1478
  // Send telemetry (fire and forget)
1318
1479
  sendInstallTelemetry({
1319
1480
  package: snapshot.name,