@microwiseai/snapshot 0.6.0 → 0.8.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.
- package/dist/commands/current.d.ts +5 -1
- package/dist/commands/current.d.ts.map +1 -1
- package/dist/commands/current.js +89 -2
- package/dist/commands/current.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/install.d.ts +41 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +328 -10
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +44 -24
- package/dist/commands/publish.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/access-sync.d.ts.map +1 -1
- package/dist/lib/access-sync.js +4 -0
- package/dist/lib/access-sync.js.map +1 -1
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +38 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/core-floor.d.ts +49 -0
- package/dist/lib/core-floor.d.ts.map +1 -0
- package/dist/lib/core-floor.js +78 -0
- package/dist/lib/core-floor.js.map +1 -0
- package/dist/lib/install-verify.d.ts +101 -0
- package/dist/lib/install-verify.d.ts.map +1 -0
- package/dist/lib/install-verify.js +143 -0
- package/dist/lib/install-verify.js.map +1 -0
- package/dist/lib/ledger.d.ts +212 -0
- package/dist/lib/ledger.d.ts.map +1 -0
- package/dist/lib/ledger.js +266 -0
- package/dist/lib/ledger.js.map +1 -0
- package/dist/lib/pin-override.d.ts +53 -0
- package/dist/lib/pin-override.d.ts.map +1 -0
- package/dist/lib/pin-override.js +77 -0
- package/dist/lib/pin-override.js.map +1 -0
- package/dist/lib/skit-adapter.d.ts.map +1 -1
- package/dist/lib/skit-adapter.js +54 -20
- package/dist/lib/skit-adapter.js.map +1 -1
- package/dist/lib/transitive-resolver.d.ts.map +1 -1
- package/dist/lib/transitive-resolver.js +14 -3
- package/dist/lib/transitive-resolver.js.map +1 -1
- package/dist/lib/version-validator.d.ts +9 -0
- package/dist/lib/version-validator.d.ts.map +1 -1
- package/dist/lib/version-validator.js +69 -0
- package/dist/lib/version-validator.js.map +1 -1
- package/package.json +13 -2
package/dist/commands/install.js
CHANGED
|
@@ -14,6 +14,10 @@ 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 { StepRecorder, buildLedger, writeLedger, queryDependencyTree, } from '../lib/ledger.js';
|
|
18
|
+
import { coreFloorAssert, formatCoreFloorOffenders, DEFAULT_CORE_FLOOR_PKG, DEFAULT_CORE_FLOOR_MIN_VERSION, } from '../lib/core-floor.js';
|
|
19
|
+
import { verifyInstallMap, runConverge, readInstalledNpmVersion, formatVerificationFailure, } from '../lib/install-verify.js';
|
|
20
|
+
import { parsePins, applyPinOverrides, unmatchedOverrides, PinParseError, } from '../lib/pin-override.js';
|
|
17
21
|
// CLI version — dynamically read from package.json (SSOT)
|
|
18
22
|
import { createRequire } from 'module';
|
|
19
23
|
const _require = createRequire(import.meta.url);
|
|
@@ -251,7 +255,7 @@ function isNpmGlobalInstalled(name, version) {
|
|
|
251
255
|
* Skit 패키지 설치 (lib 우선, CLI fallback)
|
|
252
256
|
* @ist/skit lib가 있으면 직접 호출, 없으면 CLI 사용
|
|
253
257
|
*/
|
|
254
|
-
async function installSkitPackages(packages, snapshotName, indent, sessionId) {
|
|
258
|
+
async function installSkitPackages(packages, snapshotName, indent, sessionId, recorder) {
|
|
255
259
|
const result = { installed: [], failed: [] };
|
|
256
260
|
const packageEntries = Object.entries(packages);
|
|
257
261
|
if (packageEntries.length === 0) {
|
|
@@ -292,12 +296,20 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
|
|
|
292
296
|
});
|
|
293
297
|
result.installed.push(...parallelResult.installed);
|
|
294
298
|
result.failed.push(...parallelResult.failed);
|
|
299
|
+
// S3 — record the skit install as one ordered step.
|
|
300
|
+
recorder?.record({
|
|
301
|
+
phase: 'skit',
|
|
302
|
+
installer: 'skit',
|
|
303
|
+
command: `skit install (${entries.length} skits)`,
|
|
304
|
+
specs: entries.map((e) => `${e.name}@${e.version}`),
|
|
305
|
+
result: result.failed.length > 0 ? 'failed' : 'ok',
|
|
306
|
+
});
|
|
295
307
|
return result;
|
|
296
308
|
}
|
|
297
309
|
/**
|
|
298
310
|
* 타입별 패키지 설치 (non-skit)
|
|
299
311
|
*/
|
|
300
|
-
async function installPackagesByType(packages, installerName, installers, snapshotName, indent, typeLabel, sessionId) {
|
|
312
|
+
async function installPackagesByType(packages, installerName, installers, snapshotName, indent, typeLabel, sessionId, recorder) {
|
|
301
313
|
const result = { installed: [], failed: [] };
|
|
302
314
|
const packageEntries = Object.entries(packages);
|
|
303
315
|
if (packageEntries.length === 0) {
|
|
@@ -305,7 +317,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
305
317
|
}
|
|
306
318
|
// Skit packages use dedicated function
|
|
307
319
|
if (installerName === 'skit') {
|
|
308
|
-
return installSkitPackages(packages, snapshotName, indent, sessionId);
|
|
320
|
+
return installSkitPackages(packages, snapshotName, indent, sessionId, recorder);
|
|
309
321
|
}
|
|
310
322
|
const installerConfig = installers[installerName] || DEFAULT_INSTALLERS[installerName];
|
|
311
323
|
if (!installerConfig) {
|
|
@@ -355,6 +367,14 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
355
367
|
const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
|
|
356
368
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
357
369
|
const success = runCommand(batchCommand);
|
|
370
|
+
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
371
|
+
recorder?.record({
|
|
372
|
+
phase: 'layer-batch',
|
|
373
|
+
installer: installerName,
|
|
374
|
+
command: batchCommand,
|
|
375
|
+
specs,
|
|
376
|
+
result: success ? 'ok' : 'fallback',
|
|
377
|
+
});
|
|
358
378
|
if (success) {
|
|
359
379
|
for (const spec of specs) {
|
|
360
380
|
console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -370,6 +390,13 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
370
390
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
371
391
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
372
392
|
const ok = runCommand(command);
|
|
393
|
+
recorder?.record({
|
|
394
|
+
phase: 'layer-fallback',
|
|
395
|
+
installer: installerName,
|
|
396
|
+
command,
|
|
397
|
+
specs: [pkgSpec],
|
|
398
|
+
result: ok ? 'ok' : 'failed',
|
|
399
|
+
});
|
|
373
400
|
if (ok) {
|
|
374
401
|
console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
375
402
|
result.installed.push(pkgSpec);
|
|
@@ -404,15 +431,65 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
404
431
|
});
|
|
405
432
|
result.installed.push(...parallelResult.installed);
|
|
406
433
|
result.failed.push(...parallelResult.failed);
|
|
434
|
+
// S3 — record the whole parallel (non-npm) install as one step.
|
|
435
|
+
recorder?.record({
|
|
436
|
+
phase: 'layer-parallel',
|
|
437
|
+
installer: installerName,
|
|
438
|
+
command: installerConfig.command.replace('{pkg}', '<each>'),
|
|
439
|
+
specs: entries.map((e) => `${e.name}@${e.version}`),
|
|
440
|
+
result: parallelResult.failed.length > 0 ? 'failed' : 'ok',
|
|
441
|
+
});
|
|
407
442
|
}
|
|
408
443
|
return result;
|
|
409
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* FB — rewrite a snapshot's install/packages maps so any package with a pin
|
|
447
|
+
* override uses the override version. Returns a new snapshot (input untouched);
|
|
448
|
+
* only keys already present are overridden (no injection of undeclared pkgs).
|
|
449
|
+
*/
|
|
450
|
+
function applySnapshotOverrides(snapshot, overrides) {
|
|
451
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
452
|
+
return snapshot;
|
|
453
|
+
const next = { ...snapshot };
|
|
454
|
+
if (snapshot.install) {
|
|
455
|
+
const install = {};
|
|
456
|
+
for (const [installer, pkgs] of Object.entries(snapshot.install)) {
|
|
457
|
+
install[installer] = applyPinOverrides(pkgs, overrides);
|
|
458
|
+
}
|
|
459
|
+
next.install = install;
|
|
460
|
+
}
|
|
461
|
+
if (snapshot.packages) {
|
|
462
|
+
next.packages = applyPinOverrides(snapshot.packages, overrides);
|
|
463
|
+
}
|
|
464
|
+
return next;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* FB — the subset of pin overrides that actually apply to this install (i.e. the
|
|
468
|
+
* overridden package IS declared in the effective npm map). These are the
|
|
469
|
+
* packages we targeted-converge so a --pin always holds even without a global
|
|
470
|
+
* --converge. Overrides for packages not in the snapshot are dropped (already
|
|
471
|
+
* warned via unmatchedOverrides). Returns name → override version.
|
|
472
|
+
*/
|
|
473
|
+
export function overrideConvergeTargets(effectiveNpmMap, overrides) {
|
|
474
|
+
const targets = {};
|
|
475
|
+
for (const [name, version] of Object.entries(overrides)) {
|
|
476
|
+
if (name in effectiveNpmMap)
|
|
477
|
+
targets[name] = version;
|
|
478
|
+
}
|
|
479
|
+
return targets;
|
|
480
|
+
}
|
|
410
481
|
export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}, options = {}) {
|
|
411
482
|
const indent = ' '.repeat(depth);
|
|
412
483
|
const result = {
|
|
413
484
|
installed: [],
|
|
414
485
|
failed: [],
|
|
415
486
|
};
|
|
487
|
+
// FB — apply pin overrides to THIS layer's install/packages maps before doing
|
|
488
|
+
// anything, so both this layer and the extends recursion (which carries the
|
|
489
|
+
// same options.overrides) install the override version, never the original pin.
|
|
490
|
+
if (options.overrides) {
|
|
491
|
+
snapshot = applySnapshotOverrides(snapshot, options.overrides);
|
|
492
|
+
}
|
|
416
493
|
// Merge installers: snapshot + parent (parent takes precedence for overrides like npm-proxy)
|
|
417
494
|
let installers = { ...DEFAULT_INSTALLERS };
|
|
418
495
|
if (snapshot.installers) {
|
|
@@ -512,21 +589,21 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
512
589
|
const resolved = await resolveSnapshotDependencies(snapshot, { skipRegistry: true });
|
|
513
590
|
// skit 패키지 설치 (skit-adapter 사용: lib 우선, CLI fallback)
|
|
514
591
|
if (Object.keys(resolved.skit).length > 0) {
|
|
515
|
-
const skitResult = await installSkitPackages(resolved.skit, snapshot.name, indent, options.sessionId);
|
|
592
|
+
const skitResult = await installSkitPackages(resolved.skit, snapshot.name, indent, options.sessionId, options.recorder);
|
|
516
593
|
result.installed.push(...skitResult.installed);
|
|
517
594
|
result.failed.push(...skitResult.failed);
|
|
518
595
|
}
|
|
519
596
|
// npm 패키지 설치 (proxy 사용 시 npm-proxy, 아니면 glpkg)
|
|
520
597
|
if (Object.keys(resolved.npm).length > 0) {
|
|
521
598
|
const npmInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
522
|
-
const npmResult = await installPackagesByType(resolved.npm, npmInstaller, installers, snapshot.name, indent, 'npm packages', options.sessionId);
|
|
599
|
+
const npmResult = await installPackagesByType(resolved.npm, npmInstaller, installers, snapshot.name, indent, 'npm packages', options.sessionId, options.recorder);
|
|
523
600
|
result.installed.push(...npmResult.installed);
|
|
524
601
|
result.failed.push(...npmResult.failed);
|
|
525
602
|
}
|
|
526
603
|
// generic 패키지 설치 (glpkg)
|
|
527
604
|
if (Object.keys(resolved.generic).length > 0) {
|
|
528
605
|
const genericInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
529
|
-
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', options.sessionId);
|
|
606
|
+
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', options.sessionId, options.recorder);
|
|
530
607
|
result.installed.push(...genericResult.installed);
|
|
531
608
|
result.failed.push(...genericResult.failed);
|
|
532
609
|
}
|
|
@@ -543,7 +620,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
543
620
|
continue;
|
|
544
621
|
// Skit packages: use skit-adapter (lib 우선, CLI fallback)
|
|
545
622
|
if (installerName === 'skit') {
|
|
546
|
-
const skitResult = await installSkitPackages(packages, snapshot.name, indent, options.sessionId);
|
|
623
|
+
const skitResult = await installSkitPackages(packages, snapshot.name, indent, options.sessionId, options.recorder);
|
|
547
624
|
result.installed.push(...skitResult.installed);
|
|
548
625
|
result.failed.push(...skitResult.failed);
|
|
549
626
|
continue;
|
|
@@ -597,6 +674,14 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
597
674
|
const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
|
|
598
675
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
599
676
|
const success = runCommand(batchCommand);
|
|
677
|
+
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
678
|
+
options.recorder?.record({
|
|
679
|
+
phase: 'layer-batch',
|
|
680
|
+
installer: installerName,
|
|
681
|
+
command: batchCommand,
|
|
682
|
+
specs,
|
|
683
|
+
result: success ? 'ok' : 'fallback',
|
|
684
|
+
});
|
|
600
685
|
if (success) {
|
|
601
686
|
for (const spec of specs) {
|
|
602
687
|
console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -612,6 +697,13 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
612
697
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
613
698
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
614
699
|
const ok = runCommand(command);
|
|
700
|
+
options.recorder?.record({
|
|
701
|
+
phase: 'layer-fallback',
|
|
702
|
+
installer: installerName,
|
|
703
|
+
command,
|
|
704
|
+
specs: [pkgSpec],
|
|
705
|
+
result: ok ? 'ok' : 'failed',
|
|
706
|
+
});
|
|
615
707
|
if (ok) {
|
|
616
708
|
console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
617
709
|
result.installed.push(pkgSpec);
|
|
@@ -646,6 +738,14 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
646
738
|
});
|
|
647
739
|
result.installed.push(...parallelResult.installed);
|
|
648
740
|
result.failed.push(...parallelResult.failed);
|
|
741
|
+
// S3 — record the whole parallel (non-npm) install as one step.
|
|
742
|
+
options.recorder?.record({
|
|
743
|
+
phase: 'layer-parallel',
|
|
744
|
+
installer: installerName,
|
|
745
|
+
command: installCfg.command.replace('{pkg}', '<each>'),
|
|
746
|
+
specs: installEntries.map((e) => `${e.name}@${e.version}`),
|
|
747
|
+
result: parallelResult.failed.length > 0 ? 'failed' : 'ok',
|
|
748
|
+
});
|
|
649
749
|
}
|
|
650
750
|
}
|
|
651
751
|
}
|
|
@@ -694,11 +794,19 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
694
794
|
});
|
|
695
795
|
result.installed.push(...parallelResult.installed);
|
|
696
796
|
result.failed.push(...parallelResult.failed);
|
|
797
|
+
// S3 — record the legacy glpkg parallel install as one step.
|
|
798
|
+
options.recorder?.record({
|
|
799
|
+
phase: 'layer-parallel',
|
|
800
|
+
installer: 'glpkg',
|
|
801
|
+
command: legacyCfg.command.replace('{pkg}', '<each>'),
|
|
802
|
+
specs: legacyEntries.map((e) => `${e.name}@${e.version}`),
|
|
803
|
+
result: parallelResult.failed.length > 0 ? 'failed' : 'ok',
|
|
804
|
+
});
|
|
697
805
|
}
|
|
698
806
|
}
|
|
699
807
|
// Legacy: skits (use skit-adapter: lib 우선, CLI fallback)
|
|
700
808
|
if (snapshot.skits && Object.keys(snapshot.skits).length > 0) {
|
|
701
|
-
const skitResult = await installSkitPackages(snapshot.skits, snapshot.name, indent, options.sessionId);
|
|
809
|
+
const skitResult = await installSkitPackages(snapshot.skits, snapshot.name, indent, options.sessionId, options.recorder);
|
|
702
810
|
result.installed.push(...skitResult.installed);
|
|
703
811
|
result.failed.push(...skitResult.failed);
|
|
704
812
|
}
|
|
@@ -758,7 +866,7 @@ async function collectAllNpmPackages(snapshot, useProxy = true) {
|
|
|
758
866
|
* This ensures .npmrc is configured with ALL scopes before installation begins,
|
|
759
867
|
* including scopes from base snapshots (e.g., @microwiseai from @ist/minimal).
|
|
760
868
|
*/
|
|
761
|
-
async function collectAllPackages(snapshot) {
|
|
869
|
+
export async function collectAllPackages(snapshot) {
|
|
762
870
|
const allPackages = {};
|
|
763
871
|
// Collect from current snapshot
|
|
764
872
|
if (snapshot.packages) {
|
|
@@ -1036,6 +1144,31 @@ export async function installCommand(name, options = {}) {
|
|
|
1036
1144
|
await previewInstall(snapshot, options);
|
|
1037
1145
|
return;
|
|
1038
1146
|
}
|
|
1147
|
+
// ========== FB — parse pin overrides (--pin) ==========
|
|
1148
|
+
// Malformed specs are fail-loud (exit 1) BEFORE any install side effect. The
|
|
1149
|
+
// top snapshot's maps are rewritten now so downstream session authorization,
|
|
1150
|
+
// scope collection, the pre-install batch AND installSnapshot all see the
|
|
1151
|
+
// override version. The extends chain is covered via options.overrides.
|
|
1152
|
+
let overrides = {};
|
|
1153
|
+
try {
|
|
1154
|
+
overrides = parsePins(options.pin);
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
if (err instanceof PinParseError) {
|
|
1158
|
+
console.log(chalk.red(`⛔ Invalid --pin: ${err.message}`));
|
|
1159
|
+
console.log(chalk.gray(' Expected form: --pin @scope/name@exact-version (e.g. --pin @ist/sesh@0.4.0)'));
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
throw err;
|
|
1163
|
+
}
|
|
1164
|
+
if (Object.keys(overrides).length > 0) {
|
|
1165
|
+
snapshot = applySnapshotOverrides(snapshot, overrides);
|
|
1166
|
+
console.log('');
|
|
1167
|
+
console.log(chalk.cyan('► FB pin overrides:'));
|
|
1168
|
+
for (const [name, version] of Object.entries(overrides)) {
|
|
1169
|
+
console.log(chalk.cyan(` ⇄ ${name} → ${version}`));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1039
1172
|
// ========== Access Check (based on snapshot.json access field) ==========
|
|
1040
1173
|
// Determine access type from snapshot.json:
|
|
1041
1174
|
// - access: "public" → no login/license required
|
|
@@ -1200,11 +1333,24 @@ export async function installCommand(name, options = {}) {
|
|
|
1200
1333
|
effectiveInstallers['glpkg'] = DEFAULT_INSTALLERS['npm-proxy'];
|
|
1201
1334
|
effectiveInstallers['gitlab-install'] = DEFAULT_INSTALLERS['npm-proxy'];
|
|
1202
1335
|
}
|
|
1336
|
+
// S3 — one step recorder for the whole install, threaded through the
|
|
1337
|
+
// pre-install batch AND installSnapshot (via options.recorder), so the ledger
|
|
1338
|
+
// captures the ordered install steps.
|
|
1339
|
+
const recorder = new StepRecorder();
|
|
1203
1340
|
// ========== Pre-install: batch all npm packages from extends chain ==========
|
|
1204
1341
|
// Collect all npm-type packages across the entire extends chain and install
|
|
1205
1342
|
// them in a single `npm install -g` call, instead of one per snapshot layer.
|
|
1206
1343
|
const allNpmPackages = await collectAllNpmPackages(snapshot, useProxy);
|
|
1207
|
-
|
|
1344
|
+
// FB — flatten overrides across the whole chain. `snapshot` was already
|
|
1345
|
+
// rewritten, but base layers fetched inside collectAllNpmPackages were not, so
|
|
1346
|
+
// apply once more to the flattened map. This is the "expected" pin set (what
|
|
1347
|
+
// SHOULD be on disk) used for the pre-install batch AND verification.
|
|
1348
|
+
const effectiveNpmMap = applyPinOverrides(allNpmPackages, overrides);
|
|
1349
|
+
const unmatched = unmatchedOverrides(allNpmPackages, overrides);
|
|
1350
|
+
if (unmatched.length > 0) {
|
|
1351
|
+
console.log(chalk.yellow(`⚠ --pin ignored for package(s) not in this snapshot: ${unmatched.join(', ')}`));
|
|
1352
|
+
}
|
|
1353
|
+
const npmEntries = Object.entries(effectiveNpmMap);
|
|
1208
1354
|
if (npmEntries.length > 0) {
|
|
1209
1355
|
console.log('');
|
|
1210
1356
|
console.log(chalk.cyan(`► Pre-installing ${npmEntries.length} npm packages from entire extends chain...`));
|
|
@@ -1223,6 +1369,15 @@ export async function installCommand(name, options = {}) {
|
|
|
1223
1369
|
const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
|
|
1224
1370
|
console.log(chalk.gray(` Running single batch: npm install -g (${specs.length} packages)`));
|
|
1225
1371
|
const batchSuccess = runCommand(npmCommand);
|
|
1372
|
+
// S3 — record the up-front batch (the single step where the beta npm tree
|
|
1373
|
+
// is actually resolved). 'fallback' means it failed and we retried below.
|
|
1374
|
+
recorder.record({
|
|
1375
|
+
phase: 'pre-install-batch',
|
|
1376
|
+
installer: 'npm-proxy',
|
|
1377
|
+
command: npmCommand,
|
|
1378
|
+
specs,
|
|
1379
|
+
result: batchSuccess ? 'ok' : 'fallback',
|
|
1380
|
+
});
|
|
1226
1381
|
if (batchSuccess) {
|
|
1227
1382
|
for (const spec of specs) {
|
|
1228
1383
|
console.log(` ${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -1239,6 +1394,13 @@ export async function installCommand(name, options = {}) {
|
|
|
1239
1394
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
1240
1395
|
const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
|
|
1241
1396
|
const ok = runCommand(cmd);
|
|
1397
|
+
recorder.record({
|
|
1398
|
+
phase: 'pre-install-fallback',
|
|
1399
|
+
installer: 'npm-proxy',
|
|
1400
|
+
command: cmd,
|
|
1401
|
+
specs: [pkgSpec],
|
|
1402
|
+
result: ok ? 'ok' : 'failed',
|
|
1403
|
+
});
|
|
1242
1404
|
if (ok) {
|
|
1243
1405
|
console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
1244
1406
|
}
|
|
@@ -1257,6 +1419,14 @@ export async function installCommand(name, options = {}) {
|
|
|
1257
1419
|
// installSnapshot's internal isNpmGlobalInstalled checks will skip already-installed packages
|
|
1258
1420
|
let installError = null;
|
|
1259
1421
|
let result = { installed: [], failed: [] };
|
|
1422
|
+
// S1 — post-install verification verdict (disk vs manifest pins). Computed
|
|
1423
|
+
// inside the try BELOW so it runs while the registry-proxy .npmrc + session
|
|
1424
|
+
// token are still active (runConverge issues `npm install -g`).
|
|
1425
|
+
let verification;
|
|
1426
|
+
// ★ Convergence is ON BY DEFAULT (owner decision). `--no-converge` opts out
|
|
1427
|
+
// for a raw/fast install (commander sets options.converge=false). Programmatic
|
|
1428
|
+
// lib callers get the default (true) unless they pass converge:false.
|
|
1429
|
+
const converge = options.converge !== false;
|
|
1260
1430
|
try {
|
|
1261
1431
|
// Auto-detect 모드: snapshot.install이 없으면 자동 판별
|
|
1262
1432
|
const autoDetect = !snapshot.install;
|
|
@@ -1264,8 +1434,51 @@ export async function installCommand(name, options = {}) {
|
|
|
1264
1434
|
useProxy,
|
|
1265
1435
|
autoDetect,
|
|
1266
1436
|
sessionId,
|
|
1437
|
+
recorder,
|
|
1438
|
+
overrides,
|
|
1267
1439
|
});
|
|
1268
1440
|
result = installResult.result;
|
|
1441
|
+
// ===== S1/FB — verify (and optionally converge) BEFORE cleanup =====
|
|
1442
|
+
// SSOT enforcement: the manifest pins (with FB overrides applied) are the
|
|
1443
|
+
// truth. Check the disk actually matches. This MUST run before the finally
|
|
1444
|
+
// restores .npmrc, because re-pinning via `npm install -g` needs the
|
|
1445
|
+
// registry-proxy config + token still in place.
|
|
1446
|
+
if (Object.keys(effectiveNpmMap).length > 0) {
|
|
1447
|
+
// Re-pin one package to an exact version and record it as a ledger step.
|
|
1448
|
+
const repin = (phase) => (name, version) => {
|
|
1449
|
+
const spec = `${name}@${version}`;
|
|
1450
|
+
const cmd = `npm install -g ${spec}`;
|
|
1451
|
+
const ok = runCommand(cmd);
|
|
1452
|
+
recorder.record({ phase, installer: 'npm-proxy', command: cmd, specs: [spec], result: ok ? 'ok' : 'failed' });
|
|
1453
|
+
};
|
|
1454
|
+
if (converge) {
|
|
1455
|
+
// Default: re-pin EVERY package that diverged from its pin (self-healing).
|
|
1456
|
+
console.log('');
|
|
1457
|
+
console.log(chalk.cyan('► Verifying & converging install to manifest pins...'));
|
|
1458
|
+
verification = runConverge(allNpmPackages, overrides, {
|
|
1459
|
+
install: repin('converge'),
|
|
1460
|
+
readVersion: readInstalledNpmVersion,
|
|
1461
|
+
maxRounds: 3,
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
// --no-converge (raw/fast): do NOT auto-repair general divergence. But a
|
|
1466
|
+
// --pin must STILL hold (npm dedup can revert it), so converge JUST the
|
|
1467
|
+
// overridden packages (targeted), then take the final verdict over the
|
|
1468
|
+
// full map. Non-overridden divergences fail-loud (see below).
|
|
1469
|
+
const pinTargets = overrideConvergeTargets(effectiveNpmMap, overrides);
|
|
1470
|
+
if (Object.keys(pinTargets).length > 0) {
|
|
1471
|
+
console.log('');
|
|
1472
|
+
console.log(chalk.cyan(`► Holding ${Object.keys(pinTargets).length} pinned package(s) to the override version...`));
|
|
1473
|
+
runConverge(pinTargets, {}, {
|
|
1474
|
+
install: repin('pin-converge'),
|
|
1475
|
+
readVersion: readInstalledNpmVersion,
|
|
1476
|
+
maxRounds: 3,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
verification = verifyInstallMap(effectiveNpmMap, { readVersion: readInstalledNpmVersion });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1269
1482
|
}
|
|
1270
1483
|
catch (err) {
|
|
1271
1484
|
installError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1289,6 +1502,16 @@ export async function installCommand(name, options = {}) {
|
|
|
1289
1502
|
console.log(chalk.yellow('═══════════════════════════════════'));
|
|
1290
1503
|
const total = result.installed.length + result.failed.length;
|
|
1291
1504
|
console.log(`Installed: ${chalk.green(result.installed.length)}/${total}`);
|
|
1505
|
+
// S1 — promote "Installed N/N" to the real health signal: "verified N/N"
|
|
1506
|
+
// (disk actually matches the manifest pins), not just "npm exited 0".
|
|
1507
|
+
if (verification) {
|
|
1508
|
+
if (verification.ok) {
|
|
1509
|
+
console.log(`Verified: ${chalk.green(verification.verifiedCount)}/${verification.total} ${chalk.gray('(disk matches manifest pins)')}`);
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
console.log(`Verified: ${chalk.red(verification.verifiedCount)}/${verification.total} ${chalk.red('(disk diverged from manifest pins)')}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1292
1515
|
if (result.failed.length > 0) {
|
|
1293
1516
|
console.log('');
|
|
1294
1517
|
console.log(chalk.red('Failed:'));
|
|
@@ -1309,6 +1532,101 @@ export async function installCommand(name, options = {}) {
|
|
|
1309
1532
|
console.log('');
|
|
1310
1533
|
console.log(chalk.green('✓') + ' Snapshot info saved');
|
|
1311
1534
|
}
|
|
1535
|
+
// ========== S3: install ledger (provenance + step order) ==========
|
|
1536
|
+
// Record where every declared npm package's on-disk version came from
|
|
1537
|
+
// (resolved version, tarball URL, resolution source, install-step order),
|
|
1538
|
+
// so "이 버전이 어디서 왔나" is answerable from the ledger ALONE (AC3.1).
|
|
1539
|
+
// Written on every install with npm packages, best-effort — a ledger failure
|
|
1540
|
+
// must never fail the install itself.
|
|
1541
|
+
// AC1.4 — core-floor verdict (computed inside the ledger block, folded in +
|
|
1542
|
+
// used for fail-loud below). undefined when there are no npm packages.
|
|
1543
|
+
let coreFloor;
|
|
1544
|
+
if (Object.keys(allNpmPackages).length > 0) {
|
|
1545
|
+
try {
|
|
1546
|
+
// ★ DEEP, nested-visible tree (NOT --depth=0). Scope to the install's npm
|
|
1547
|
+
// packages + the singleton so an OLD @ist/adapters-core buried under
|
|
1548
|
+
// another package (which re-fragments the S2 registry) is surfaced.
|
|
1549
|
+
const deepNames = Array.from(new Set([...Object.keys(effectiveNpmMap), DEFAULT_CORE_FLOOR_PKG]));
|
|
1550
|
+
const dependencyTreeRaw = queryDependencyTree(deepNames);
|
|
1551
|
+
// AC1.4 (R-A) — no copy of @ist/adapters-core (top-level OR nested) may be
|
|
1552
|
+
// below the 0.4.2 floor; an old nested copy silently re-breaks codex.
|
|
1553
|
+
coreFloor = coreFloorAssert(dependencyTreeRaw); // defaults: @ist/adapters-core >= 0.4.2
|
|
1554
|
+
const ledger = buildLedger({
|
|
1555
|
+
snapshotName: snapshot.name,
|
|
1556
|
+
snapshotVersion: snapshot.version,
|
|
1557
|
+
cliVersion: CLI_VERSION,
|
|
1558
|
+
registry: REGISTRY_PROXY_URL,
|
|
1559
|
+
// "expected" = the pins we intended to land = manifest pins with FB
|
|
1560
|
+
// overrides applied. This is what verification checks against too.
|
|
1561
|
+
expected: effectiveNpmMap,
|
|
1562
|
+
installer: 'npm-proxy',
|
|
1563
|
+
steps: recorder.getSteps(),
|
|
1564
|
+
overrides, // FB pin overrides (name → version); {} when no --pin
|
|
1565
|
+
dependencyTreeRaw, // deep tree (nested-visible) — provenance + AC1.4
|
|
1566
|
+
}, new Date().toISOString());
|
|
1567
|
+
// S1 — fold the verification verdict into the ledger (single artifact for
|
|
1568
|
+
// provenance + verdict). VerificationResult is structurally the ledger's
|
|
1569
|
+
// verification shape { ok, verifiedCount, total, mismatches[{name,expected,got}] }.
|
|
1570
|
+
if (verification)
|
|
1571
|
+
ledger.verification = verification;
|
|
1572
|
+
// AC1.4 — fold the core-floor verdict too.
|
|
1573
|
+
ledger.coreFloor = {
|
|
1574
|
+
ok: coreFloor.ok,
|
|
1575
|
+
pkg: DEFAULT_CORE_FLOOR_PKG,
|
|
1576
|
+
minVersion: DEFAULT_CORE_FLOOR_MIN_VERSION,
|
|
1577
|
+
offenders: coreFloor.offenders,
|
|
1578
|
+
};
|
|
1579
|
+
if (writeLedger(ledger)) {
|
|
1580
|
+
console.log(chalk.green('✓') + ' Install ledger written (see: snapshot current --json)');
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
catch {
|
|
1584
|
+
// Ledger is diagnostic-only — swallow any failure.
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
// ========== Divergence handling — S1 verify + AC1.4 core-floor ==========
|
|
1588
|
+
// Owner decision: NO "green on broken". Any residual divergence is a hard
|
|
1589
|
+
// fail-loud (exit 1), in BOTH modes:
|
|
1590
|
+
// • core-floor offenders → ALWAYS exit 1 (convergence cannot repair an old
|
|
1591
|
+
// NESTED @ist/adapters-core copy; AC1.4 / R-A).
|
|
1592
|
+
// • verify divergence → exit 1 in both modes. The only difference: default
|
|
1593
|
+
// (converge) mode ATTEMPTED auto-repair first and still couldn't reconcile
|
|
1594
|
+
// (e.g. an unpublished pin), whereas --no-converge (strict/raw) never
|
|
1595
|
+
// attempted repair. The ledger persisted the full evidence either way.
|
|
1596
|
+
const verifyFailed = !!(verification && !verification.ok);
|
|
1597
|
+
const floorFailed = !!(coreFloor && !coreFloor.ok);
|
|
1598
|
+
if (floorFailed) {
|
|
1599
|
+
console.log('');
|
|
1600
|
+
console.log(chalk.red(formatCoreFloorOffenders(coreFloor.offenders)));
|
|
1601
|
+
console.log(chalk.gray(` Every ${DEFAULT_CORE_FLOOR_PKG} copy (incl. nested) must be >= ${DEFAULT_CORE_FLOOR_MIN_VERSION} for the S2 singleton to hold.`));
|
|
1602
|
+
}
|
|
1603
|
+
if (verifyFailed) {
|
|
1604
|
+
console.log('');
|
|
1605
|
+
if (!converge) {
|
|
1606
|
+
// strict (--no-converge): no auto-repair was attempted.
|
|
1607
|
+
console.log(chalk.red(formatVerificationFailure(verification.mismatches)));
|
|
1608
|
+
if (Object.keys(overrides).length > 0) {
|
|
1609
|
+
// --pin packages were held via targeted convergence; the divergences
|
|
1610
|
+
// above are OTHER packages — don't suggest re-pinning what's pinned.
|
|
1611
|
+
console.log(chalk.gray(' The pinned package(s) are held; the divergence(s) above are other packages — drop --no-converge to auto-repair them.'));
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
console.log(chalk.gray(' Drop --no-converge to auto-repair, or --pin <pkg>@<version> to force a specific version.'));
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
// default (converge ON): repair was attempted but couldn't reconcile → still
|
|
1619
|
+
// a hard failure (no silent green-on-broken).
|
|
1620
|
+
console.log(chalk.red('✗ Converge could not reconcile the disk to the manifest pins after 3 rounds:'));
|
|
1621
|
+
console.log(chalk.red(formatVerificationFailure(verification.mismatches)));
|
|
1622
|
+
console.log(chalk.gray(' The above may be unpublished/unavailable versions. See: snapshot current --json (ledger has full evidence).'));
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// No green-on-broken: exit non-zero on ANY core-floor offender OR verify
|
|
1626
|
+
// divergence, in both converge and --no-converge modes.
|
|
1627
|
+
if (floorFailed || verifyFailed) {
|
|
1628
|
+
process.exit(1);
|
|
1629
|
+
}
|
|
1312
1630
|
// Record references for the ENTIRE declared set (installed + dedup-skipped),
|
|
1313
1631
|
// so replace mode has accurate referencedBy data (ADR 001 §c). This runs on
|
|
1314
1632
|
// every install, not just --replace, to build up the reference graph.
|