@microwiseai/snapshot 0.5.2 → 0.6.0

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 (134) hide show
  1. package/dist/commands/doctor.d.ts +2 -0
  2. package/dist/commands/doctor.d.ts.map +1 -0
  3. package/dist/commands/doctor.js +37 -0
  4. package/dist/commands/doctor.js.map +1 -0
  5. package/dist/commands/install.d.ts.map +1 -1
  6. package/dist/commands/install.js +68 -206
  7. package/dist/commands/install.js.map +1 -1
  8. package/dist/commands/publish.d.ts +0 -4
  9. package/dist/commands/publish.d.ts.map +1 -1
  10. package/dist/commands/publish.js +1 -35
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/commands/setup.d.ts +5 -0
  13. package/dist/commands/setup.d.ts.map +1 -0
  14. package/dist/commands/setup.js +52 -0
  15. package/dist/commands/setup.js.map +1 -0
  16. package/dist/index.js +18 -27
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/config.js +1 -1
  19. package/dist/lib/config.js.map +1 -1
  20. package/dist/lib/gitlab.d.ts.map +1 -1
  21. package/dist/lib/gitlab.js +8 -6
  22. package/dist/lib/gitlab.js.map +1 -1
  23. package/dist/lib/prerequisites.d.ts +33 -0
  24. package/dist/lib/prerequisites.d.ts.map +1 -0
  25. package/dist/lib/prerequisites.js +123 -0
  26. package/dist/lib/prerequisites.js.map +1 -0
  27. package/dist/lib/transitive-resolver.d.ts +0 -121
  28. package/dist/lib/transitive-resolver.d.ts.map +1 -1
  29. package/dist/lib/transitive-resolver.js +0 -190
  30. package/dist/lib/transitive-resolver.js.map +1 -1
  31. package/package.json +3 -20
  32. package/dist/commands/clone-all.d.ts +0 -21
  33. package/dist/commands/clone-all.d.ts.map +0 -1
  34. package/dist/commands/clone-all.js +0 -57
  35. package/dist/commands/clone-all.js.map +0 -1
  36. package/dist/commands/preflight.d.ts +0 -31
  37. package/dist/commands/preflight.d.ts.map +0 -1
  38. package/dist/commands/preflight.js +0 -83
  39. package/dist/commands/preflight.js.map +0 -1
  40. package/dist/lib/distkit-closure-bridge.d.ts +0 -102
  41. package/dist/lib/distkit-closure-bridge.d.ts.map +0 -1
  42. package/dist/lib/distkit-closure-bridge.js +0 -177
  43. package/dist/lib/distkit-closure-bridge.js.map +0 -1
  44. package/dist/lib/distkit-closure-parity.d.ts +0 -110
  45. package/dist/lib/distkit-closure-parity.d.ts.map +0 -1
  46. package/dist/lib/distkit-closure-parity.js +0 -134
  47. package/dist/lib/distkit-closure-parity.js.map +0 -1
  48. package/dist/lib/distkit-routing-port.d.ts +0 -37
  49. package/dist/lib/distkit-routing-port.d.ts.map +0 -1
  50. package/dist/lib/distkit-routing-port.js +0 -72
  51. package/dist/lib/distkit-routing-port.js.map +0 -1
  52. package/dist/lib/install-timeline.d.ts +0 -81
  53. package/dist/lib/install-timeline.d.ts.map +0 -1
  54. package/dist/lib/install-timeline.js +0 -243
  55. package/dist/lib/install-timeline.js.map +0 -1
  56. package/dist/lib/preflight-adapter.d.ts +0 -52
  57. package/dist/lib/preflight-adapter.d.ts.map +0 -1
  58. package/dist/lib/preflight-adapter.js +0 -224
  59. package/dist/lib/preflight-adapter.js.map +0 -1
  60. package/node_modules/@gitdot/clone-lib/README.md +0 -117
  61. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts +0 -9
  62. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts.map +0 -1
  63. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js +0 -154
  64. package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js.map +0 -1
  65. package/node_modules/@gitdot/clone-lib/dist/clone.d.ts +0 -21
  66. package/node_modules/@gitdot/clone-lib/dist/clone.d.ts.map +0 -1
  67. package/node_modules/@gitdot/clone-lib/dist/clone.js +0 -228
  68. package/node_modules/@gitdot/clone-lib/dist/clone.js.map +0 -1
  69. package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts +0 -10
  70. package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts.map +0 -1
  71. package/node_modules/@gitdot/clone-lib/dist/dotpath.js +0 -27
  72. package/node_modules/@gitdot/clone-lib/dist/dotpath.js.map +0 -1
  73. package/node_modules/@gitdot/clone-lib/dist/git.d.ts +0 -23
  74. package/node_modules/@gitdot/clone-lib/dist/git.d.ts.map +0 -1
  75. package/node_modules/@gitdot/clone-lib/dist/git.js +0 -134
  76. package/node_modules/@gitdot/clone-lib/dist/git.js.map +0 -1
  77. package/node_modules/@gitdot/clone-lib/dist/index.d.ts +0 -17
  78. package/node_modules/@gitdot/clone-lib/dist/index.d.ts.map +0 -1
  79. package/node_modules/@gitdot/clone-lib/dist/index.js +0 -35
  80. package/node_modules/@gitdot/clone-lib/dist/index.js.map +0 -1
  81. package/node_modules/@gitdot/clone-lib/dist/lock.d.ts +0 -24
  82. package/node_modules/@gitdot/clone-lib/dist/lock.d.ts.map +0 -1
  83. package/node_modules/@gitdot/clone-lib/dist/lock.js +0 -97
  84. package/node_modules/@gitdot/clone-lib/dist/lock.js.map +0 -1
  85. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts +0 -15
  86. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts.map +0 -1
  87. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js +0 -124
  88. package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js.map +0 -1
  89. package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts +0 -12
  90. package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts.map +0 -1
  91. package/node_modules/@gitdot/clone-lib/dist/resolve/index.js +0 -31
  92. package/node_modules/@gitdot/clone-lib/dist/resolve/index.js.map +0 -1
  93. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts +0 -32
  94. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts.map +0 -1
  95. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js +0 -159
  96. package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js.map +0 -1
  97. package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts +0 -44
  98. package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts.map +0 -1
  99. package/node_modules/@gitdot/clone-lib/dist/resolve/member.js +0 -163
  100. package/node_modules/@gitdot/clone-lib/dist/resolve/member.js.map +0 -1
  101. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts +0 -18
  102. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts.map +0 -1
  103. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js +0 -155
  104. package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js.map +0 -1
  105. package/node_modules/@gitdot/clone-lib/dist/types.d.ts +0 -225
  106. package/node_modules/@gitdot/clone-lib/dist/types.d.ts.map +0 -1
  107. package/node_modules/@gitdot/clone-lib/dist/types.js +0 -9
  108. package/node_modules/@gitdot/clone-lib/dist/types.js.map +0 -1
  109. package/node_modules/@gitdot/clone-lib/package.json +0 -53
  110. package/node_modules/@microwiseai/install-preflight/dist/index.d.ts +0 -27
  111. package/node_modules/@microwiseai/install-preflight/dist/index.d.ts.map +0 -1
  112. package/node_modules/@microwiseai/install-preflight/dist/index.js +0 -26
  113. package/node_modules/@microwiseai/install-preflight/dist/index.js.map +0 -1
  114. package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts +0 -42
  115. package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts.map +0 -1
  116. package/node_modules/@microwiseai/install-preflight/dist/policy.js +0 -79
  117. package/node_modules/@microwiseai/install-preflight/dist/policy.js.map +0 -1
  118. package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts +0 -18
  119. package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts.map +0 -1
  120. package/node_modules/@microwiseai/install-preflight/dist/preflight.js +0 -0
  121. package/node_modules/@microwiseai/install-preflight/dist/preflight.js.map +0 -1
  122. package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts +0 -26
  123. package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts.map +0 -1
  124. package/node_modules/@microwiseai/install-preflight/dist/resolvers.js +0 -63
  125. package/node_modules/@microwiseai/install-preflight/dist/resolvers.js.map +0 -1
  126. package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts +0 -45
  127. package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts.map +0 -1
  128. package/node_modules/@microwiseai/install-preflight/dist/semver.js +0 -195
  129. package/node_modules/@microwiseai/install-preflight/dist/semver.js.map +0 -1
  130. package/node_modules/@microwiseai/install-preflight/dist/types.d.ts +0 -136
  131. package/node_modules/@microwiseai/install-preflight/dist/types.d.ts.map +0 -1
  132. package/node_modules/@microwiseai/install-preflight/dist/types.js +0 -11
  133. package/node_modules/@microwiseai/install-preflight/dist/types.js.map +0 -1
  134. package/node_modules/@microwiseai/install-preflight/package.json +0 -49
@@ -0,0 +1,2 @@
1
+ export declare function doctorCommand(): Promise<void>;
2
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAGA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAoCnD"}
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ import { checkPrerequisites, MINIMAL_PREREQUISITES } from '../lib/prerequisites.js';
3
+ export async function doctorCommand() {
4
+ console.log(chalk.blue('Checking prerequisites...\n'));
5
+ const results = checkPrerequisites(MINIMAL_PREREQUISITES);
6
+ const missing = results.filter(r => !r.installed);
7
+ // Print results
8
+ for (const r of results) {
9
+ const icon = r.installed ? chalk.green('✅') : chalk.red('❌');
10
+ console.log(` ${icon} ${r.name}`);
11
+ if (!r.installed) {
12
+ console.log(chalk.gray(` Required by: ${r.requiredBy.join(', ')}`));
13
+ if (r.installCommand) {
14
+ console.log(chalk.gray(` Fix: ${r.installCommand}`));
15
+ }
16
+ if (r.guide) {
17
+ console.log(chalk.gray(` ${r.guide}`));
18
+ }
19
+ }
20
+ }
21
+ console.log('');
22
+ if (missing.length === 0) {
23
+ console.log(chalk.green('All prerequisites satisfied! ✓'));
24
+ }
25
+ else {
26
+ console.log(chalk.yellow(`${missing.length} issue(s) found.`));
27
+ const fixable = missing.filter(r => r.installCommand);
28
+ if (fixable.length > 0) {
29
+ console.log(chalk.gray(`Run 'snapshot setup' to install ${fixable.length} missing prerequisite(s).`));
30
+ }
31
+ const manual = missing.filter(r => !r.installCommand);
32
+ if (manual.length > 0) {
33
+ console.log(chalk.gray(`${manual.length} require manual action (see above).`));
34
+ }
35
+ }
36
+ }
37
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEpF,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAEvD,MAAM,OAAO,GAAG,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAElD,gBAAgB;IAChB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;YACxE,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;gBACrB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;YAC3D,CAAC;YACD,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACZ,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;IAC7D,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,kBAAkB,CAAC,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;QACtD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,mCAAmC,OAAO,CAAC,MAAM,2BAA2B,CAAC,CAAC,CAAC;QACxG,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;QACtD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,qCAAqC,CAAC,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -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;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
+ {"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,CA6arG"}
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { execSync, spawn } from 'child_process';
3
- import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { writeFileSync, existsSync, readFileSync, unlinkSync } 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,12 +14,6 @@ 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 });
23
17
  // CLI version — dynamically read from package.json (SSOT)
24
18
  import { createRequire } from 'module';
25
19
  const _require = createRequire(import.meta.url);
@@ -200,74 +194,27 @@ function commandExists(cmd) {
200
194
  // Captures the failure reason of the most recent runCommand() call so callers
201
195
  // can surface it (debugging visibility). Undefined when the last call succeeded.
202
196
  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.
218
197
  function runCommand(command) {
219
198
  lastCommandError = undefined;
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
- });
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
+ }
271
218
  }
272
219
  /**
273
220
  * Check if a skit package is already installed with the specified version
@@ -300,71 +247,6 @@ function isNpmGlobalInstalled(name, version) {
300
247
  return false;
301
248
  }
302
249
  }
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
- }
368
250
  /**
369
251
  * Skit 패키지 설치 (lib 우선, CLI fallback)
370
252
  * @ist/skit lib가 있으면 직접 호출, 없으면 CLI 사용
@@ -375,10 +257,6 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
375
257
  if (packageEntries.length === 0) {
376
258
  return result;
377
259
  }
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();
382
260
  const total = packageEntries.length;
383
261
  console.log('');
384
262
  console.log(chalk.yellow(`${indent}=== ${snapshotName}: ${total} skits ===`));
@@ -391,14 +269,7 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
391
269
  if (isSkitInstalled(entry.name, entry.version)) {
392
270
  return { success: true, skipped: true };
393
271
  }
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
- });
400
272
  const installResult = await installSkitPackage(entry.name, entry.version, skitOptions);
401
- pkgSpan.end({ success: installResult.success });
402
273
  return {
403
274
  success: installResult.success,
404
275
  error: installResult.error,
@@ -450,7 +321,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
450
321
  if (!commandExists(cmdName)) {
451
322
  console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
452
323
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
453
- if (await runCommand(installerConfig.hint)) {
324
+ if (runCommand(installerConfig.hint)) {
454
325
  console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
455
326
  }
456
327
  else {
@@ -483,7 +354,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
483
354
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
484
355
  const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
485
356
  console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
486
- const success = await runCommand(batchCommand);
357
+ const success = runCommand(batchCommand);
487
358
  if (success) {
488
359
  for (const spec of specs) {
489
360
  console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -498,7 +369,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
498
369
  for (const entry of toInstall) {
499
370
  const pkgSpec = `${entry.name}@${entry.version}`;
500
371
  const command = installerConfig.command.replace('{pkg}', pkgSpec);
501
- const ok = await runCommand(command);
372
+ const ok = runCommand(command);
502
373
  if (ok) {
503
374
  console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
504
375
  result.installed.push(pkgSpec);
@@ -516,7 +387,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
516
387
  const parallelResult = await parallelInstall(entries, async (entry) => {
517
388
  const pkgSpec = `${entry.name}@${entry.version}`;
518
389
  const command = installerConfig.command.replace('{pkg}', pkgSpec);
519
- const success = await runCommand(command);
390
+ const success = runCommand(command);
520
391
  return { success, error: success ? undefined : `Command failed: ${command}` };
521
392
  }, {
522
393
  concurrency: 4,
@@ -691,7 +562,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
691
562
  if (!commandExists(cmdName)) {
692
563
  console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
693
564
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
694
- if (await runCommand(installerConfig.hint)) {
565
+ if (runCommand(installerConfig.hint)) {
695
566
  console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
696
567
  }
697
568
  else {
@@ -725,7 +596,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
725
596
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
726
597
  const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
727
598
  console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
728
- const success = await runCommand(batchCommand);
599
+ const success = runCommand(batchCommand);
729
600
  if (success) {
730
601
  for (const spec of specs) {
731
602
  console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -740,7 +611,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
740
611
  for (const entry of toInstall) {
741
612
  const pkgSpec = `${entry.name}@${entry.version}`;
742
613
  const command = installCfg.command.replace('{pkg}', pkgSpec);
743
- const ok = await runCommand(command);
614
+ const ok = runCommand(command);
744
615
  if (ok) {
745
616
  console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
746
617
  result.installed.push(pkgSpec);
@@ -758,7 +629,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
758
629
  const parallelResult = await parallelInstall(installEntries, async (entry) => {
759
630
  const pkgSpec = `${entry.name}@${entry.version}`;
760
631
  const command = installCfg.command.replace('{pkg}', pkgSpec);
761
- const success = await runCommand(command);
632
+ const success = runCommand(command);
762
633
  return { success, error: success ? undefined : `Command failed: ${command}` };
763
634
  }, {
764
635
  concurrency: 4,
@@ -788,7 +659,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
788
659
  if (!commandExists(cmdName)) {
789
660
  console.log(chalk.yellow(`${indent}glpkg not found. Attempting auto-install...`));
790
661
  console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
791
- if (await runCommand(installerConfig.hint)) {
662
+ if (runCommand(installerConfig.hint)) {
792
663
  console.log(chalk.green(`${indent} ✓ glpkg installed successfully`));
793
664
  }
794
665
  else {
@@ -806,7 +677,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
806
677
  const parallelResult = await parallelInstall(legacyEntries, async (entry) => {
807
678
  const pkgSpec = `${entry.name}@${entry.version}`;
808
679
  const command = legacyCfg.command.replace('{pkg}', pkgSpec);
809
- const success = await runCommand(command);
680
+ const success = runCommand(command);
810
681
  return { success, error: success ? undefined : `Command failed: ${command}` };
811
682
  }, {
812
683
  concurrency: 4,
@@ -1017,7 +888,7 @@ async function runReplaceMode(snapshotName, newSet, options, installers) {
1017
888
  continue;
1018
889
  }
1019
890
  console.log(chalk.gray(` Removing ${item.name}...`));
1020
- if (await runCommand(cmd)) {
891
+ if (runCommand(cmd)) {
1021
892
  console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
1022
893
  removed.push(item);
1023
894
  }
@@ -1078,10 +949,6 @@ async function previewInstall(snapshot, options) {
1078
949
  console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
1079
950
  }
1080
951
  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();
1085
952
  console.log(chalk.blue.bold(`
1086
953
  ╔══════════════════════════════════════════════════════════════╗
1087
954
  ║ Snapshot Installer ║
@@ -1115,7 +982,6 @@ export async function installCommand(name, options = {}) {
1115
982
  //
1116
983
  // License key CANNOT be used as Bearer token for registry-proxy — they are
1117
984
  // distinct: AuthN (uid via IST JWT) vs AuthZ (entitlement via license key).
1118
- const authSpan = activeTimeline.start('auth-token', { category: 'phase' });
1119
985
  const licenseKey = getLicenseKey();
1120
986
  let authToken = null;
1121
987
  let authType = 'anonymous';
@@ -1132,16 +998,13 @@ export async function installCommand(name, options = {}) {
1132
998
  console.log(chalk.yellow('⚠') + ' No auth token — anonymous access');
1133
999
  }
1134
1000
  const useProxy = true;
1135
- authSpan.end({ authType });
1136
1001
  // Fetch snapshot first — access check depends on snapshot.json content
1137
1002
  console.log(chalk.gray('Fetching snapshot...'));
1138
- const fetchSpan = activeTimeline.start('fetch-snapshot', { category: 'phase' });
1139
1003
  let snapshot;
1140
1004
  try {
1141
1005
  snapshot = await fetchSnapshotFromRepo(name, authToken);
1142
1006
  }
1143
1007
  catch (error) {
1144
- fetchSpan.end({ success: false });
1145
1008
  const errMsg = error instanceof Error ? error.message : String(error);
1146
1009
  if (options.dryRun) {
1147
1010
  // Pure preview: a fetch failure (offline / no auth) is not fatal — we just
@@ -1156,7 +1019,6 @@ export async function installCommand(name, options = {}) {
1156
1019
  console.log(chalk.yellow('Check the snapshot name and version, e.g. snapshot install @scope/name@version'));
1157
1020
  process.exit(1);
1158
1021
  }
1159
- fetchSpan.end({ success: true });
1160
1022
  console.log(chalk.green('✓') + ` Snapshot: ${snapshot.name} v${snapshot.version}`);
1161
1023
  if (snapshot.description) {
1162
1024
  console.log(chalk.gray(` ${snapshot.description}`));
@@ -1175,29 +1037,36 @@ export async function installCommand(name, options = {}) {
1175
1037
  return;
1176
1038
  }
1177
1039
  // ========== Access Check (based on snapshot.json access field) ==========
1178
- // snapshot is a general installer. Access is simplified to two tiers:
1179
- // - access: "public" → no license required
1040
+ // Determine access type from snapshot.json:
1041
+ // - access: "public" → no login/license required
1042
+ // - access: "authenticated" → IST login required, no license needed
1180
1043
  // - access: { type: "licensed", productId } → license required
1181
- // - anything else / absent → access decided SERVER-side
1182
- // (registry-proxy / license API are authoritative; a 401/403 fails loud).
1183
- // No client-side login gate: access control is backend-authoritative.
1044
+ // - access field absent → treated as licensed (requires license)
1184
1045
  const accessField = snapshot.access;
1185
1046
  const isPublicAccess = accessField === 'public';
1047
+ const isAuthenticatedAccess = accessField === 'authenticated';
1186
1048
  const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
1187
- // Optional identity for telemetry only (anonymous when absent). NOT a gate.
1188
- // A wrapper (e.g. ist-setup) may layer a login system on top of snapshot via
1189
- // the authToken injection point (IST_SESSION_TOKEN env / getProxyAuthToken).
1049
+ // --- IST Login Check ---
1190
1050
  const istCredentials = getIstCredentials();
1051
+ if (istCredentials?.userId) {
1052
+ console.log(chalk.green('✓') + ' IST login verified');
1053
+ }
1054
+ else if (isPublicAccess) {
1055
+ console.log(chalk.yellow('⚠') + ' No IST login - installing public package');
1056
+ }
1057
+ else {
1058
+ console.log(chalk.red('⛔ IST login required for non-public packages'));
1059
+ console.log(chalk.gray(' Run: ist auth login'));
1060
+ console.log(chalk.gray(' Or install a public snapshot first (no login required)'));
1061
+ process.exit(1);
1062
+ }
1191
1063
  // --- License Check ---
1192
- const accessSpan = activeTimeline.start('access-license', {
1193
- category: 'phase',
1194
- meta: { access: isPublicAccess ? 'public' : isLicensedAccess ? 'licensed' : 'server-decided' },
1195
- });
1196
1064
  const storedLicenseKey = getLicenseKey();
1197
1065
  let licenseValid = false;
1198
- if (isPublicAccess) {
1199
- // Public snapshots — skip license check entirely
1200
- console.log(chalk.green('✓') + ' Public snapshot - no license required');
1066
+ if (isPublicAccess || isAuthenticatedAccess) {
1067
+ // Public and authenticated snapshots — skip license check entirely
1068
+ const label = isAuthenticatedAccess ? 'Authenticated' : 'Public';
1069
+ console.log(chalk.green('✓') + ` ${label} snapshot - no license required`);
1201
1070
  }
1202
1071
  else if (storedLicenseKey) {
1203
1072
  try {
@@ -1239,10 +1108,8 @@ export async function installCommand(name, options = {}) {
1239
1108
  }
1240
1109
  console.log(chalk.green('✓') + ` License verified for ${productId}`);
1241
1110
  }
1242
- accessSpan.end({ licenseValid });
1243
1111
  // =======================================================================
1244
1112
  // ========== Authorize Session with Registry Proxy ==========
1245
- const authorizeSpan = activeTimeline.start('authorize-session', { category: 'phase' });
1246
1113
  let sessionId;
1247
1114
  if (authToken) {
1248
1115
  // Collect all packages for session authorization
@@ -1278,10 +1145,8 @@ export async function installCommand(name, options = {}) {
1278
1145
  }
1279
1146
  }
1280
1147
  }
1281
- authorizeSpan.end({ sessionAuthorized: sessionId !== undefined });
1282
1148
  // =============================================================
1283
1149
  // Collect all packages from entire extends chain to determine scopes
1284
- const configureSpan = activeTimeline.start('configure-npmrc', { category: 'phase' });
1285
1150
  const allPackages = await collectAllPackages(snapshot);
1286
1151
  // Configure .npmrc for proxy (with or without auth token)
1287
1152
  let npmrcBackup = null;
@@ -1301,7 +1166,6 @@ export async function installCommand(name, options = {}) {
1301
1166
  }
1302
1167
  }
1303
1168
  }
1304
- configureSpan.end({ packageCount: Object.keys(allPackages).length });
1305
1169
  // Guarantee .npmrc restoration even on ^C. A try/finally only covers thrown
1306
1170
  // errors and normal returns — it does NOT run on SIGINT/SIGTERM, so an
1307
1171
  // interrupt during the pre-install batch or the main install would leave a
@@ -1339,8 +1203,6 @@ export async function installCommand(name, options = {}) {
1339
1203
  // ========== Pre-install: batch all npm packages from extends chain ==========
1340
1204
  // Collect all npm-type packages across the entire extends chain and install
1341
1205
  // 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 = [];
1344
1206
  const allNpmPackages = await collectAllNpmPackages(snapshot, useProxy);
1345
1207
  const npmEntries = Object.entries(allNpmPackages);
1346
1208
  if (npmEntries.length > 0) {
@@ -1358,10 +1220,9 @@ export async function installCommand(name, options = {}) {
1358
1220
  }
1359
1221
  if (toInstall.length > 0) {
1360
1222
  const specs = toInstall.map(e => `${e.name}@${e.version}`);
1361
- batchSpecs = specs;
1362
1223
  const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
1363
1224
  console.log(chalk.gray(` Running single batch: npm install -g (${specs.length} packages)`));
1364
- const batchSuccess = await runCommand(npmCommand);
1225
+ const batchSuccess = runCommand(npmCommand);
1365
1226
  if (batchSuccess) {
1366
1227
  for (const spec of specs) {
1367
1228
  console.log(` ${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
@@ -1377,7 +1238,7 @@ export async function installCommand(name, options = {}) {
1377
1238
  for (const entry of toInstall) {
1378
1239
  const pkgSpec = `${entry.name}@${entry.version}`;
1379
1240
  const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
1380
- const ok = await runCommand(cmd);
1241
+ const ok = runCommand(cmd);
1381
1242
  if (ok) {
1382
1243
  console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
1383
1244
  }
@@ -1391,13 +1252,11 @@ export async function installCommand(name, options = {}) {
1391
1252
  console.log(chalk.green('✓ All npm packages already installed'));
1392
1253
  }
1393
1254
  }
1394
- batchSpan.end({ specs: batchSpecs, npmDeclared: npmEntries.length, npmToInstall: batchSpecs.length });
1395
1255
  // =============================================================================
1396
1256
  // Install (handles extends recursively)
1397
1257
  // installSnapshot's internal isNpmGlobalInstalled checks will skip already-installed packages
1398
1258
  let installError = null;
1399
1259
  let result = { installed: [], failed: [] };
1400
- const installSpan = activeTimeline.start('install-snapshot', { category: 'phase' });
1401
1260
  try {
1402
1261
  // Auto-detect 모드: snapshot.install이 없으면 자동 판별
1403
1262
  const autoDetect = !snapshot.install;
@@ -1412,11 +1271,6 @@ export async function installCommand(name, options = {}) {
1412
1271
  installError = err instanceof Error ? err : new Error(String(err));
1413
1272
  }
1414
1273
  finally {
1415
- installSpan.end({
1416
- installed: result.installed.length,
1417
- failed: result.failed.length,
1418
- success: installError === null,
1419
- });
1420
1274
  // Restore .npmrc + clear the session-token env (idempotent: a SIGINT/SIGTERM
1421
1275
  // handler may have already run this). Then drop the signal handlers we
1422
1276
  // registered, so they don't leak across multiple installs in one process.
@@ -1425,7 +1279,6 @@ export async function installCommand(name, options = {}) {
1425
1279
  process.removeListener('SIGTERM', onSignal);
1426
1280
  }
1427
1281
  if (installError) {
1428
- activeTimeline.finalize();
1429
1282
  console.log(chalk.red(`Error during installation: ${installError.message}`));
1430
1283
  process.exit(1);
1431
1284
  }
@@ -1472,9 +1325,18 @@ export async function installCommand(name, options = {}) {
1472
1325
  await runReplaceMode(snapshot.name, allPackages, options, effectiveInstallers);
1473
1326
  }
1474
1327
  }
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();
1328
+ // Post-install: run doctor to check prerequisites
1329
+ console.log('');
1330
+ const { checkPrerequisites, MINIMAL_PREREQUISITES } = await import('../lib/prerequisites.js');
1331
+ const prereqResults = checkPrerequisites(MINIMAL_PREREQUISITES);
1332
+ const missingPrereqs = prereqResults.filter(r => !r.installed);
1333
+ if (missingPrereqs.length > 0) {
1334
+ console.log(chalk.yellow(`\n⚠ ${missingPrereqs.length} prerequisite(s) not satisfied:`));
1335
+ for (const r of missingPrereqs) {
1336
+ console.log(` ${chalk.red('❌')} ${r.name} — required by ${r.requiredBy.join(', ')}`);
1337
+ }
1338
+ console.log(chalk.gray(`\nRun 'snapshot setup' to install, or 'snapshot doctor' for details.`));
1339
+ }
1478
1340
  // Send telemetry (fire and forget)
1479
1341
  sendInstallTelemetry({
1480
1342
  package: snapshot.name,