@microwiseai/snapshot 0.3.71 → 0.5.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 (78) hide show
  1. package/dist/commands/clone-all.d.ts +21 -0
  2. package/dist/commands/clone-all.d.ts.map +1 -0
  3. package/dist/commands/clone-all.js +57 -0
  4. package/dist/commands/clone-all.js.map +1 -0
  5. package/dist/commands/install.d.ts +9 -1
  6. package/dist/commands/install.d.ts.map +1 -1
  7. package/dist/commands/install.js +219 -49
  8. package/dist/commands/install.js.map +1 -1
  9. package/dist/commands/license.js.map +1 -1
  10. package/dist/commands/list.d.ts +15 -0
  11. package/dist/commands/list.d.ts.map +1 -1
  12. package/dist/commands/list.js +21 -39
  13. package/dist/commands/list.js.map +1 -1
  14. package/dist/commands/publish.d.ts.map +1 -1
  15. package/dist/commands/publish.js +22 -0
  16. package/dist/commands/publish.js.map +1 -1
  17. package/dist/index.js +51 -29
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +15 -10
  21. package/dist/lib/config.js.map +1 -1
  22. package/dist/lib/gitlab.d.ts.map +1 -1
  23. package/dist/lib/gitlab.js +6 -8
  24. package/dist/lib/gitlab.js.map +1 -1
  25. package/dist/lib/transitive-resolver.js.map +1 -1
  26. package/dist/lib/types.d.ts +1 -0
  27. package/dist/lib/types.d.ts.map +1 -1
  28. package/dist/lib/version-validator.d.ts +28 -0
  29. package/dist/lib/version-validator.d.ts.map +1 -0
  30. package/dist/lib/version-validator.js +105 -0
  31. package/dist/lib/version-validator.js.map +1 -0
  32. package/dist/lib.js.map +1 -1
  33. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.d.ts +86 -1
  34. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.d.ts.map +1 -1
  35. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.js +171 -5
  36. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.js.map +1 -1
  37. package/node_modules/@microwiseai/snapshot-dedup/dist/index.d.ts +1 -1
  38. package/node_modules/@microwiseai/snapshot-dedup/dist/index.d.ts.map +1 -1
  39. package/node_modules/@microwiseai/snapshot-dedup/dist/index.js +4 -2
  40. package/node_modules/@microwiseai/snapshot-dedup/dist/index.js.map +1 -1
  41. package/node_modules/@microwiseai/snapshot-dedup/package.json +8 -2
  42. package/package.json +18 -4
  43. package/dist/commands/doctor.d.ts +0 -2
  44. package/dist/commands/doctor.d.ts.map +0 -1
  45. package/dist/commands/doctor.js +0 -37
  46. package/dist/commands/doctor.js.map +0 -1
  47. package/dist/commands/setup.d.ts +0 -5
  48. package/dist/commands/setup.d.ts.map +0 -1
  49. package/dist/commands/setup.js +0 -52
  50. package/dist/commands/setup.js.map +0 -1
  51. package/dist/lib/access-sync.test.d.ts +0 -2
  52. package/dist/lib/access-sync.test.d.ts.map +0 -1
  53. package/dist/lib/access-sync.test.js +0 -112
  54. package/dist/lib/access-sync.test.js.map +0 -1
  55. package/dist/lib/auth-flow.test.d.ts +0 -2
  56. package/dist/lib/auth-flow.test.d.ts.map +0 -1
  57. package/dist/lib/auth-flow.test.js +0 -265
  58. package/dist/lib/auth-flow.test.js.map +0 -1
  59. package/dist/lib/config.test.d.ts +0 -2
  60. package/dist/lib/config.test.d.ts.map +0 -1
  61. package/dist/lib/config.test.js +0 -151
  62. package/dist/lib/config.test.js.map +0 -1
  63. package/dist/lib/gitlab.test.d.ts +0 -2
  64. package/dist/lib/gitlab.test.d.ts.map +0 -1
  65. package/dist/lib/gitlab.test.js +0 -63
  66. package/dist/lib/gitlab.test.js.map +0 -1
  67. package/dist/lib/prerequisites.d.ts +0 -33
  68. package/dist/lib/prerequisites.d.ts.map +0 -1
  69. package/dist/lib/prerequisites.js +0 -123
  70. package/dist/lib/prerequisites.js.map +0 -1
  71. package/dist/lib/registry.d.ts +0 -3
  72. package/dist/lib/registry.d.ts.map +0 -1
  73. package/dist/lib/registry.js +0 -53
  74. package/dist/lib/registry.js.map +0 -1
  75. package/dist/lib/transitive-resolver.test.d.ts +0 -2
  76. package/dist/lib/transitive-resolver.test.d.ts.map +0 -1
  77. package/dist/lib/transitive-resolver.test.js +0 -279
  78. package/dist/lib/transitive-resolver.test.js.map +0 -1
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `snapshot clone-all` — clone the full SOURCE closure a snapshot declares.
3
+ *
4
+ * Thin adapter over the published, typed @gitdot/clone-lib@0.2.0: it loads
5
+ * snapshot.json (the install set + registry scope→group), resolves it to a
6
+ * resolved-lock (S2), writes snapshot.lock.json, and clones the closure. All
7
+ * real logic — name→projectId resolution, coordinate/visibility/commitSha
8
+ * pinning, closure cloning, structured reporting — lives in the library.
9
+ *
10
+ * The GitLab token is the caller's own (getGitlabToken → glpkg token /
11
+ * GITLAB_TOKEN), decoupled from the install proxy: cloning is plain git, judged
12
+ * by repo visibility + caller access.
13
+ */
14
+ export interface CloneAllOptions {
15
+ manifest?: string;
16
+ mode?: string;
17
+ dest?: string;
18
+ json?: boolean;
19
+ }
20
+ export declare function cloneAllCommand(options?: CloneAllOptions): Promise<void>;
21
+ //# sourceMappingURL=clone-all.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone-all.d.ts","sourceRoot":"","sources":["../../src/commands/clone-all.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,eAAe,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DlF"}
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { resolveFromManifest, cloneFromLock, writeLock, createGitLabApi } from '@gitdot/clone-lib';
5
+ import { getGitlabToken } from '../lib/config.js';
6
+ export async function cloneAllCommand(options = {}) {
7
+ const manifestPath = options.manifest || 'snapshot.json';
8
+ const mode = options.mode === 'pinned' ? 'pinned' : 'head';
9
+ const dest = options.dest || process.cwd();
10
+ const token = getGitlabToken();
11
+ if (!token) {
12
+ console.error(chalk.red('Error: no GitLab token. Set GITLAB_TOKEN or configure glpkg (glpkg config save <token>).'));
13
+ process.exit(1);
14
+ }
15
+ let manifest;
16
+ try {
17
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
18
+ }
19
+ catch (err) {
20
+ console.error(chalk.red(`Error: could not read manifest "${manifestPath}": ${err.message}`));
21
+ process.exit(1);
22
+ return;
23
+ }
24
+ const registry = manifest.registry || {};
25
+ const subset = manifest.name;
26
+ const api = createGitLabApi({ token, host: 'gitlab.com' });
27
+ const { lock, warnings } = await resolveFromManifest(manifest, {
28
+ api,
29
+ scopes: Object.keys(registry),
30
+ subset,
31
+ });
32
+ mkdirSync(dest, { recursive: true });
33
+ const lockPath = join(dest, 'snapshot.lock.json');
34
+ writeLock(lockPath, lock);
35
+ const report = await cloneFromLock(lockPath, { mode, dest });
36
+ if (options.json) {
37
+ // --json is the primary contract: emit the structured report verbatim.
38
+ console.log(JSON.stringify({ report, lock: lockPath, warnings }, null, 2));
39
+ }
40
+ else {
41
+ console.log(chalk.blue.bold('snapshot clone-all'));
42
+ console.log(chalk.gray(`manifest: ${manifestPath} | mode: ${mode} | dest: ${dest}`));
43
+ console.log(chalk.gray(`lock: ${lockPath}`));
44
+ console.log(`cloned ${chalk.green(report.cloned.length)} | ` +
45
+ `skipped ${report.skipped.length} | ` +
46
+ `no-access ${report['skipped-no-access'].length} | ` +
47
+ `not-found ${report['not-found'].length} | ` +
48
+ `failed ${chalk.red(report.failed.length)}`);
49
+ console.log(chalk.gray(`closureSize ${report.closureSize} | credentialsNeeded ${report.credentialsNeeded}`));
50
+ for (const w of warnings)
51
+ console.log(chalk.yellow(`⚠ ${w}`));
52
+ }
53
+ if (report.failed.length > 0 || report['not-found'].length > 0) {
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+ //# sourceMappingURL=clone-all.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone-all.js","sourceRoot":"","sources":["../../src/commands/clone-all.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnG,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAsBlD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAA2B,EAAE;IACjE,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACzD,MAAM,IAAI,GAAc,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IACtE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAE3C,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,0FAA0F,CAAC,CACtG,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAa,CAAC;IACxE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,mCAAmC,YAAY,MAAO,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACxG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAC;IACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC;IAE7B,MAAM,GAAG,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IAC3D,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE;QAC7D,GAAG;QACH,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7B,MAAM;KACP,CAAC,CAAC;IAEH,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAE1B,MAAM,MAAM,GAAgB,MAAM,aAAa,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1E,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,uEAAuE;QACvE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,YAAY,YAAY,IAAI,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC;QACrF,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CACT,UAAU,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK;YAC9C,WAAW,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK;YACrC,aAAa,MAAM,CAAC,mBAAmB,CAAC,CAAC,MAAM,KAAK;YACpD,aAAa,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,KAAK;YAC5C,UAAU,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAC9C,CAAC;QACF,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,IAAI,CAAC,eAAe,MAAM,CAAC,WAAW,wBAAwB,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAChG,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,QAAQ;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}
@@ -12,5 +12,13 @@ export declare function installSnapshot(snapshot: Snapshot, depth?: number, pare
12
12
  result: InstallResult;
13
13
  installers: Record<string, InstallerConfig>;
14
14
  }>;
15
- export declare function installCommand(name: string): Promise<void>;
15
+ export interface InstallCommandOptions {
16
+ /** Converge onto the new version: remove previous-version-only packages (ADR 001). */
17
+ replace?: boolean;
18
+ /** With --replace, show what would change without modifying anything. */
19
+ dryRun?: boolean;
20
+ /** Skip the removal confirmation prompt. */
21
+ yes?: boolean;
22
+ }
23
+ export declare function installCommand(name: string, options?: InstallCommandOptions): Promise<void>;
16
24
  //# sourceMappingURL=install.d.ts.map
@@ -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;AAMjE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AA2eD,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,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqYhE"}
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"}
@@ -12,7 +12,8 @@ import { installSkitPackage } from '../lib/skit-adapter.js';
12
12
  import { markLicenseChecked } from '@microwiseai/license';
13
13
  import { authorizeSession, packagesToSpecs } from '../lib/session.js';
14
14
  import { parallelInstall, recordToEntries } from '@microwiseai/snapshot-parallel';
15
- import { dedupWithReport, dedupInstallBlock, recordInstalledSpecs } from '@microwiseai/snapshot-dedup';
15
+ import { dedupWithReport, dedupInstallBlock, recordInstalledSpecs, recordReferences, computeReplacePlan, applyReplaceHistory, } from '@microwiseai/snapshot-dedup';
16
+ import { createInterface } from 'readline';
16
17
  // CLI version — dynamically read from package.json (SSOT)
17
18
  import { createRequire } from 'module';
18
19
  const _require = createRequire(import.meta.url);
@@ -60,22 +61,28 @@ const DEFAULT_INSTALLERS = {
60
61
  command: 'glpkg install {pkg}',
61
62
  check: 'glpkg --version',
62
63
  hint: 'npm install -g @glpkg/cli',
64
+ // glpkg has no `uninstall` subcommand; its packages are global npm packages,
65
+ // so removal falls back to npm. (ADR 001 §d)
66
+ uninstall: 'npm uninstall -g {pkg}',
63
67
  },
64
68
  // Legacy alias for backward compatibility
65
69
  'gitlab-install': {
66
70
  command: 'glpkg install {pkg}',
67
71
  check: 'glpkg --version',
68
72
  hint: 'npm install -g @glpkg/cli (gitlab-install is deprecated, use glpkg)',
73
+ uninstall: 'npm uninstall -g {pkg}',
69
74
  },
70
75
  'skit': {
71
76
  command: 'skit install {pkg}',
72
77
  check: 'skit --version',
73
78
  hint: 'npm install -g @microwiseai/skit',
79
+ uninstall: 'skit uninstall {pkg}', // verified: `skit uninstall|rm <package>`
74
80
  },
75
81
  'npm-proxy': {
76
82
  command: 'npm install -g {pkg}',
77
83
  check: 'npm --version',
78
84
  hint: 'npm is required',
85
+ uninstall: 'npm uninstall -g {pkg}', // verified: `npm uninstall`
79
86
  },
80
87
  };
81
88
  /**
@@ -209,20 +216,6 @@ function runCommand(command) {
209
216
  return false;
210
217
  }
211
218
  }
212
- /**
213
- * PackageType을 installer 이름으로 변환
214
- */
215
- function packageTypeToInstaller(type, useProxy) {
216
- switch (type) {
217
- case 'skit':
218
- return 'skit';
219
- case 'npm':
220
- return useProxy ? 'npm-proxy' : 'glpkg';
221
- case 'generic':
222
- default:
223
- return 'glpkg';
224
- }
225
- }
226
219
  /**
227
220
  * Check if a skit package is already installed with the specified version
228
221
  */
@@ -793,7 +786,169 @@ async function collectAllPackages(snapshot) {
793
786
  }
794
787
  return allPackages;
795
788
  }
796
- export async function installCommand(name) {
789
+ /**
790
+ * Whether a package was installed as a skit (→ removed via `skit uninstall`).
791
+ * Determined offline from ~/.ist/installed-kits.json; everything else is treated
792
+ * as a global npm package (`npm uninstall -g`).
793
+ */
794
+ function isSkitPackage(name) {
795
+ try {
796
+ const p = join(homedir(), '.ist', 'installed-kits.json');
797
+ if (!existsSync(p))
798
+ return false;
799
+ const kits = JSON.parse(readFileSync(p, 'utf-8'));
800
+ return !!kits[name];
801
+ }
802
+ catch {
803
+ return false;
804
+ }
805
+ }
806
+ /** Build the uninstall command for a package, or null if no uninstaller is known. */
807
+ function uninstallCommandFor(name, installers) {
808
+ const installerName = isSkitPackage(name) ? 'skit' : 'npm-proxy';
809
+ const cfg = installers[installerName] || DEFAULT_INSTALLERS[installerName];
810
+ if (!cfg?.uninstall)
811
+ return null;
812
+ return cfg.uninstall.replace('{pkg}', name); // {pkg} = name only (uninstall takes no version)
813
+ }
814
+ /** Yes/No confirmation. Non-interactive (no TTY) → false (safe default: don't remove). */
815
+ async function confirmPrompt(question) {
816
+ if (!process.stdin.isTTY)
817
+ return false;
818
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
819
+ try {
820
+ const answer = await new Promise((resolve) => {
821
+ rl.question(chalk.yellow(`${question} [y/N] `), resolve);
822
+ });
823
+ return /^y(es)?$/i.test(answer.trim());
824
+ }
825
+ finally {
826
+ rl.close();
827
+ }
828
+ }
829
+ function printReplacePlan(plan) {
830
+ console.log('');
831
+ console.log(chalk.cyan('► Replace mode: converging to the new version set'));
832
+ if (plan.remove.length > 0) {
833
+ console.log(chalk.yellow(` Will remove ${plan.remove.length} package(s) no longer in this snapshot:`));
834
+ for (const item of plan.remove) {
835
+ console.log(chalk.red(` − ${item.name}@${item.version}`));
836
+ }
837
+ }
838
+ if (plan.keepShared.length > 0) {
839
+ console.log(chalk.gray(` Keeping ${plan.keepShared.length} shared package(s) (still used by other snapshots):`));
840
+ for (const item of plan.keepShared) {
841
+ console.log(chalk.gray(` • ${item.name}@${item.version} ← ${item.remainingRefs.join(', ')}`));
842
+ }
843
+ }
844
+ if (plan.stale.length > 0) {
845
+ console.log(chalk.gray(` Cleaning ${plan.stale.length} superseded history entr(y/ies) (already replaced by new version):`));
846
+ for (const item of plan.stale) {
847
+ console.log(chalk.gray(` ~ ${item.name}@${item.version}`));
848
+ }
849
+ }
850
+ }
851
+ /**
852
+ * Replace mode (ADR 001): after a successful install of a new version, remove
853
+ * packages that belonged to the previous version but are no longer part of the
854
+ * new version — UNLESS another installed snapshot still references them.
855
+ */
856
+ async function runReplaceMode(snapshotName, newSet, options, installers) {
857
+ const plan = computeReplacePlan(snapshotName, newSet);
858
+ const hasWork = plan.remove.length > 0 || plan.keepShared.length > 0 || plan.stale.length > 0;
859
+ if (!hasWork) {
860
+ console.log('');
861
+ console.log(chalk.gray('► Replace mode: nothing to converge — system already matches this version.'));
862
+ return;
863
+ }
864
+ printReplacePlan(plan);
865
+ if (options.dryRun) {
866
+ console.log('');
867
+ console.log(chalk.gray('Dry-run: no packages removed and history left unchanged.'));
868
+ return;
869
+ }
870
+ // Confirm before any destructive uninstall.
871
+ if (plan.remove.length > 0 && !options.yes) {
872
+ const ok = await confirmPrompt(`Remove ${plan.remove.length} package(s)?`);
873
+ if (!ok) {
874
+ console.log(chalk.yellow('Aborted — no packages removed.'));
875
+ if (!process.stdin.isTTY) {
876
+ console.log(chalk.gray(' (non-interactive shell — pass --yes to remove)'));
877
+ }
878
+ return;
879
+ }
880
+ }
881
+ // Uninstall each removal candidate; only history-delete the ones that succeed.
882
+ const removed = [];
883
+ const failed = [];
884
+ for (const item of plan.remove) {
885
+ const cmd = uninstallCommandFor(item.name, installers);
886
+ if (!cmd) {
887
+ console.log(chalk.yellow(` ⚠ No uninstaller for ${item.name} — skipping (manual removal required).`));
888
+ continue;
889
+ }
890
+ console.log(chalk.gray(` Removing ${item.name}...`));
891
+ if (runCommand(cmd)) {
892
+ console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
893
+ removed.push(item);
894
+ }
895
+ else {
896
+ console.log(` ${item.name}${chalk.red(' ✗ removal failed')}`);
897
+ if (lastCommandError)
898
+ console.log(chalk.gray(` ${lastCommandError}`));
899
+ failed.push(item);
900
+ }
901
+ }
902
+ // Update history: delete removed entries; drop this snapshot from kept/stale refs.
903
+ applyReplaceHistory(snapshotName, removed, plan.keepShared, plan.stale);
904
+ console.log('');
905
+ console.log(chalk.green(`✓ Replace complete: removed ${removed.length}, kept ${plan.keepShared.length} shared` +
906
+ (failed.length > 0 ? chalk.red(`, ${failed.length} failed`) : '')));
907
+ if (failed.length > 0) {
908
+ console.log(chalk.yellow(' Failed removals were left installed and remain in history for retry:'));
909
+ for (const item of failed)
910
+ console.log(chalk.red(` − ${item.name}@${item.version}`));
911
+ }
912
+ }
913
+ /**
914
+ * Dry-run preview: report what an install WOULD do, with no install side
915
+ * effects (no install/uninstall, no ~/.npmrc, no history). Import telemetry is
916
+ * a separate, global concern gated in index.ts (skipped for --dry-run there).
917
+ * Reads only (snapshot already fetched; extends bases resolved read-only inside
918
+ * collectAllPackages, which swallows fetch errors and returns a partial set).
919
+ * Never installs, uninstalls, or writes ~/.npmrc / history.
920
+ */
921
+ async function previewInstall(snapshot, options) {
922
+ console.log('');
923
+ console.log(chalk.cyan('► Dry-run: preview only — nothing will be installed or changed.'));
924
+ // Resolve the full package set across the entire extends chain (reads only).
925
+ const previewPackages = await collectAllPackages(snapshot);
926
+ const entries = Object.entries(previewPackages).sort(([a], [b]) => a.localeCompare(b));
927
+ console.log('');
928
+ if (entries.length === 0) {
929
+ console.log(chalk.gray(' (No packages declared in this snapshot or its extends chain.)'));
930
+ }
931
+ else {
932
+ console.log(chalk.yellow(`Would install ${entries.length} package(s) (extends chain resolved):`));
933
+ for (const [n, v] of entries) {
934
+ console.log(chalk.gray(` + ${n}@${v}`));
935
+ }
936
+ }
937
+ // With --replace, preview the convergence plan too (history read only, no write).
938
+ if (options.replace) {
939
+ const plan = computeReplacePlan(snapshot.name, previewPackages);
940
+ if (plan.remove.length > 0 || plan.keepShared.length > 0 || plan.stale.length > 0) {
941
+ printReplacePlan(plan);
942
+ }
943
+ else {
944
+ console.log('');
945
+ console.log(chalk.gray('► Replace preview: nothing to converge — system already matches this version.'));
946
+ }
947
+ }
948
+ console.log('');
949
+ console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
950
+ }
951
+ export async function installCommand(name, options = {}) {
797
952
  console.log(chalk.blue.bold(`
798
953
  ╔══════════════════════════════════════════════════════════════╗
799
954
  ║ Snapshot Installer ║
@@ -851,45 +1006,56 @@ export async function installCommand(name) {
851
1006
  }
852
1007
  catch (error) {
853
1008
  const errMsg = error instanceof Error ? error.message : String(error);
1009
+ if (options.dryRun) {
1010
+ // Pure preview: a fetch failure (offline / no auth) is not fatal — we just
1011
+ // cannot resolve what would be installed. Report a limited preview and
1012
+ // exit 0 without touching anything.
1013
+ console.log(chalk.yellow('⚠ Dry-run preview limited: could not fetch snapshot metadata.'));
1014
+ console.log(chalk.gray(` Reason: ${errMsg}`));
1015
+ console.log(chalk.gray(' (No packages installed, no files changed.)'));
1016
+ return;
1017
+ }
854
1018
  console.log(chalk.red(`Error: ${errMsg}`));
855
- console.log(chalk.yellow('Check available snapshots: snapshot list'));
1019
+ console.log(chalk.yellow('Check the snapshot name and version, e.g. snapshot install @scope/name@version'));
856
1020
  process.exit(1);
857
1021
  }
858
1022
  console.log(chalk.green('✓') + ` Snapshot: ${snapshot.name} v${snapshot.version}`);
859
1023
  if (snapshot.description) {
860
1024
  console.log(chalk.gray(` ${snapshot.description}`));
861
1025
  }
1026
+ // ========== Dry-run: preview with no install side effects ==========
1027
+ // A dry-run must not install/uninstall anything and must not touch ~/.npmrc,
1028
+ // the global node_modules, or the install history. We only READ snapshot
1029
+ // metadata (already fetched; extends bases resolved read-only) to report what
1030
+ // WOULD be installed — and, with --replace, what would be removed/kept/cleaned.
1031
+ // We exit here BEFORE the access gate, session authorization, .npmrc
1032
+ // configuration, the npm pre-install batch, and installSnapshot — i.e. before
1033
+ // any write or network-write. Reached before recordInstalledSpecs /
1034
+ // recordReferences, so no history mutation occurs either.
1035
+ if (options.dryRun) {
1036
+ await previewInstall(snapshot, options);
1037
+ return;
1038
+ }
862
1039
  // ========== Access Check (based on snapshot.json access field) ==========
863
- // Determine access type from snapshot.json:
864
- // - access: "public" → no login/license required
865
- // - access: "authenticated" → IST login required, no license needed
1040
+ // snapshot is a general installer. Access is simplified to two tiers:
1041
+ // - access: "public" → no license required
866
1042
  // - access: { type: "licensed", productId } → license required
867
- // - access field absent → treated as licensed (requires license)
1043
+ // - anything else / absent → access decided SERVER-side
1044
+ // (registry-proxy / license API are authoritative; a 401/403 fails loud).
1045
+ // No client-side login gate: access control is backend-authoritative.
868
1046
  const accessField = snapshot.access;
869
1047
  const isPublicAccess = accessField === 'public';
870
- const isAuthenticatedAccess = accessField === 'authenticated';
871
1048
  const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
872
- // --- IST Login Check ---
1049
+ // Optional identity for telemetry only (anonymous when absent). NOT a gate.
1050
+ // A wrapper (e.g. ist-setup) may layer a login system on top of snapshot via
1051
+ // the authToken injection point (IST_SESSION_TOKEN env / getProxyAuthToken).
873
1052
  const istCredentials = getIstCredentials();
874
- if (istCredentials?.userId) {
875
- console.log(chalk.green('✓') + ' IST login verified');
876
- }
877
- else if (isPublicAccess) {
878
- console.log(chalk.yellow('⚠') + ' No IST login - installing public package');
879
- }
880
- else {
881
- console.log(chalk.red('⛔ IST login required for non-public packages'));
882
- console.log(chalk.gray(' Run: ist auth login'));
883
- console.log(chalk.gray(' Or install a public snapshot first (no login required)'));
884
- process.exit(1);
885
- }
886
1053
  // --- License Check ---
887
1054
  const storedLicenseKey = getLicenseKey();
888
1055
  let licenseValid = false;
889
- if (isPublicAccess || isAuthenticatedAccess) {
890
- // Public and authenticated snapshots — skip license check entirely
891
- const label = isAuthenticatedAccess ? 'Authenticated' : 'Public';
892
- console.log(chalk.green('✓') + ` ${label} snapshot - no license required`);
1056
+ if (isPublicAccess) {
1057
+ // Public snapshots — skip license check entirely
1058
+ console.log(chalk.green('✓') + ' Public snapshot - no license required');
893
1059
  }
894
1060
  else if (storedLicenseKey) {
895
1061
  try {
@@ -1132,17 +1298,21 @@ export async function installCommand(name) {
1132
1298
  console.log('');
1133
1299
  console.log(chalk.green('✓') + ' Snapshot info saved');
1134
1300
  }
1135
- // Post-install: run doctor to check prerequisites
1136
- console.log('');
1137
- const { checkPrerequisites, MINIMAL_PREREQUISITES } = await import('../lib/prerequisites.js');
1138
- const prereqResults = checkPrerequisites(MINIMAL_PREREQUISITES);
1139
- const missingPrereqs = prereqResults.filter(r => !r.installed);
1140
- if (missingPrereqs.length > 0) {
1141
- console.log(chalk.yellow(`\n⚠ ${missingPrereqs.length} prerequisite(s) not satisfied:`));
1142
- for (const r of missingPrereqs) {
1143
- console.log(` ${chalk.red('❌')} ${r.name} — required by ${r.requiredBy.join(', ')}`);
1301
+ // Record references for the ENTIRE declared set (installed + dedup-skipped),
1302
+ // so replace mode has accurate referencedBy data (ADR 001 §c). This runs on
1303
+ // every install, not just --replace, to build up the reference graph.
1304
+ recordReferences(allPackages, snapshot.name);
1305
+ // ========== Replace mode (ADR 001) ==========
1306
+ // Only converge after a fully successful install — never remove old packages
1307
+ // when the new version's install partially failed.
1308
+ if (options.replace) {
1309
+ if (result.failed.length > 0) {
1310
+ console.log('');
1311
+ console.log(chalk.yellow('⚠ Skipping --replace: install had failures; not removing previous packages.'));
1312
+ }
1313
+ else {
1314
+ await runReplaceMode(snapshot.name, allPackages, options, effectiveInstallers);
1144
1315
  }
1145
- console.log(chalk.gray(`\nRun 'snapshot setup' to install, or 'snapshot doctor' for details.`));
1146
1316
  }
1147
1317
  // Send telemetry (fire and forget)
1148
1318
  sendInstallTelemetry({