@microwiseai/snapshot 0.5.2 → 0.7.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 +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +83 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/install.d.ts +41 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +396 -216
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/publish.d.ts +0 -4
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +44 -58
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +52 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/index.js +22 -28
- package/dist/index.js.map +1 -1
- package/dist/lib/config.js +1 -1
- 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/gitlab.d.ts.map +1 -1
- package/dist/lib/gitlab.js +8 -6
- package/dist/lib/gitlab.js.map +1 -1
- 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/prerequisites.d.ts +33 -0
- package/dist/lib/prerequisites.d.ts.map +1 -0
- package/dist/lib/prerequisites.js +123 -0
- package/dist/lib/prerequisites.js.map +1 -0
- package/dist/lib/transitive-resolver.d.ts +0 -121
- package/dist/lib/transitive-resolver.d.ts.map +1 -1
- package/dist/lib/transitive-resolver.js +0 -190
- 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 +9 -19
- package/dist/commands/clone-all.d.ts +0 -21
- package/dist/commands/clone-all.d.ts.map +0 -1
- package/dist/commands/clone-all.js +0 -57
- package/dist/commands/clone-all.js.map +0 -1
- package/dist/commands/preflight.d.ts +0 -31
- package/dist/commands/preflight.d.ts.map +0 -1
- package/dist/commands/preflight.js +0 -83
- package/dist/commands/preflight.js.map +0 -1
- package/dist/lib/distkit-closure-bridge.d.ts +0 -102
- package/dist/lib/distkit-closure-bridge.d.ts.map +0 -1
- package/dist/lib/distkit-closure-bridge.js +0 -177
- package/dist/lib/distkit-closure-bridge.js.map +0 -1
- package/dist/lib/distkit-closure-parity.d.ts +0 -110
- package/dist/lib/distkit-closure-parity.d.ts.map +0 -1
- package/dist/lib/distkit-closure-parity.js +0 -134
- package/dist/lib/distkit-closure-parity.js.map +0 -1
- package/dist/lib/distkit-routing-port.d.ts +0 -37
- package/dist/lib/distkit-routing-port.d.ts.map +0 -1
- package/dist/lib/distkit-routing-port.js +0 -72
- package/dist/lib/distkit-routing-port.js.map +0 -1
- package/dist/lib/install-timeline.d.ts +0 -81
- package/dist/lib/install-timeline.d.ts.map +0 -1
- package/dist/lib/install-timeline.js +0 -243
- package/dist/lib/install-timeline.js.map +0 -1
- package/dist/lib/preflight-adapter.d.ts +0 -52
- package/dist/lib/preflight-adapter.d.ts.map +0 -1
- package/dist/lib/preflight-adapter.js +0 -224
- package/dist/lib/preflight-adapter.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/README.md +0 -117
- package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts +0 -9
- package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js +0 -154
- package/node_modules/@gitdot/clone-lib/dist/bin/gitdot-resolve.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/clone.d.ts +0 -21
- package/node_modules/@gitdot/clone-lib/dist/clone.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/clone.js +0 -228
- package/node_modules/@gitdot/clone-lib/dist/clone.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts +0 -10
- package/node_modules/@gitdot/clone-lib/dist/dotpath.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/dotpath.js +0 -27
- package/node_modules/@gitdot/clone-lib/dist/dotpath.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/git.d.ts +0 -23
- package/node_modules/@gitdot/clone-lib/dist/git.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/git.js +0 -134
- package/node_modules/@gitdot/clone-lib/dist/git.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/index.d.ts +0 -17
- package/node_modules/@gitdot/clone-lib/dist/index.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/index.js +0 -35
- package/node_modules/@gitdot/clone-lib/dist/index.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/lock.d.ts +0 -24
- package/node_modules/@gitdot/clone-lib/dist/lock.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/lock.js +0 -97
- package/node_modules/@gitdot/clone-lib/dist/lock.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts +0 -15
- package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js +0 -124
- package/node_modules/@gitdot/clone-lib/dist/resolve/gitlab.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts +0 -12
- package/node_modules/@gitdot/clone-lib/dist/resolve/index.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/index.js +0 -31
- package/node_modules/@gitdot/clone-lib/dist/resolve/index.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts +0 -32
- package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js +0 -159
- package/node_modules/@gitdot/clone-lib/dist/resolve/manifest.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts +0 -44
- package/node_modules/@gitdot/clone-lib/dist/resolve/member.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/member.js +0 -163
- package/node_modules/@gitdot/clone-lib/dist/resolve/member.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts +0 -18
- package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js +0 -155
- package/node_modules/@gitdot/clone-lib/dist/resolve/resolve.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/types.d.ts +0 -225
- package/node_modules/@gitdot/clone-lib/dist/types.d.ts.map +0 -1
- package/node_modules/@gitdot/clone-lib/dist/types.js +0 -9
- package/node_modules/@gitdot/clone-lib/dist/types.js.map +0 -1
- package/node_modules/@gitdot/clone-lib/package.json +0 -53
- package/node_modules/@microwiseai/install-preflight/dist/index.d.ts +0 -27
- package/node_modules/@microwiseai/install-preflight/dist/index.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/index.js +0 -26
- package/node_modules/@microwiseai/install-preflight/dist/index.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts +0 -42
- package/node_modules/@microwiseai/install-preflight/dist/policy.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/policy.js +0 -79
- package/node_modules/@microwiseai/install-preflight/dist/policy.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts +0 -18
- package/node_modules/@microwiseai/install-preflight/dist/preflight.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/preflight.js +0 -0
- package/node_modules/@microwiseai/install-preflight/dist/preflight.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts +0 -26
- package/node_modules/@microwiseai/install-preflight/dist/resolvers.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/resolvers.js +0 -63
- package/node_modules/@microwiseai/install-preflight/dist/resolvers.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts +0 -45
- package/node_modules/@microwiseai/install-preflight/dist/semver.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/semver.js +0 -195
- package/node_modules/@microwiseai/install-preflight/dist/semver.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/types.d.ts +0 -136
- package/node_modules/@microwiseai/install-preflight/dist/types.d.ts.map +0 -1
- package/node_modules/@microwiseai/install-preflight/dist/types.js +0 -11
- package/node_modules/@microwiseai/install-preflight/dist/types.js.map +0 -1
- package/node_modules/@microwiseai/install-preflight/package.json +0 -49
package/dist/commands/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { execSync
|
|
3
|
-
import { writeFileSync, existsSync, readFileSync, unlinkSync
|
|
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,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 {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// SNAPSHOT_INSTALL_TIMELINE is set. Adds zero behavior — only measurement.
|
|
22
|
-
let activeTimeline = createInstallTimeline({ enabled: false });
|
|
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';
|
|
23
21
|
// CLI version — dynamically read from package.json (SSOT)
|
|
24
22
|
import { createRequire } from 'module';
|
|
25
23
|
const _require = createRequire(import.meta.url);
|
|
@@ -200,74 +198,27 @@ function commandExists(cmd) {
|
|
|
200
198
|
// Captures the failure reason of the most recent runCommand() call so callers
|
|
201
199
|
// can surface it (debugging visibility). Undefined when the last call succeeded.
|
|
202
200
|
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
201
|
function runCommand(command) {
|
|
219
202
|
lastCommandError = undefined;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
});
|
|
203
|
+
try {
|
|
204
|
+
// Keep stdout visible live, but capture stderr so we can surface the raw
|
|
205
|
+
// error reason instead of swallowing it on failure.
|
|
206
|
+
execSync(command, { stdio: ['inherit', 'inherit', 'pipe'] });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const exitCode = err?.status;
|
|
211
|
+
const stderr = err?.stderr?.toString().trim();
|
|
212
|
+
const message = err?.message ? String(err.message).trim() : undefined;
|
|
213
|
+
const detail = stderr || message || 'unknown error';
|
|
214
|
+
lastCommandError = `exit code ${exitCode ?? 'unknown'} — ${detail}`;
|
|
215
|
+
// Surface the raw failure (stderr is no longer shown live since it is piped).
|
|
216
|
+
console.log(chalk.red(` Command failed (exit code ${exitCode ?? 'unknown'}): ${command}`));
|
|
217
|
+
if (detail) {
|
|
218
|
+
console.log(chalk.red(detail));
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
271
222
|
}
|
|
272
223
|
/**
|
|
273
224
|
* Check if a skit package is already installed with the specified version
|
|
@@ -300,85 +251,16 @@ function isNpmGlobalInstalled(name, version) {
|
|
|
300
251
|
return false;
|
|
301
252
|
}
|
|
302
253
|
}
|
|
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
254
|
/**
|
|
369
255
|
* Skit 패키지 설치 (lib 우선, CLI fallback)
|
|
370
256
|
* @ist/skit lib가 있으면 직접 호출, 없으면 CLI 사용
|
|
371
257
|
*/
|
|
372
|
-
async function installSkitPackages(packages, snapshotName, indent, sessionId) {
|
|
258
|
+
async function installSkitPackages(packages, snapshotName, indent, sessionId, recorder) {
|
|
373
259
|
const result = { installed: [], failed: [] };
|
|
374
260
|
const packageEntries = Object.entries(packages);
|
|
375
261
|
if (packageEntries.length === 0) {
|
|
376
262
|
return result;
|
|
377
263
|
}
|
|
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
264
|
const total = packageEntries.length;
|
|
383
265
|
console.log('');
|
|
384
266
|
console.log(chalk.yellow(`${indent}=== ${snapshotName}: ${total} skits ===`));
|
|
@@ -391,14 +273,7 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
|
|
|
391
273
|
if (isSkitInstalled(entry.name, entry.version)) {
|
|
392
274
|
return { success: true, skipped: true };
|
|
393
275
|
}
|
|
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
276
|
const installResult = await installSkitPackage(entry.name, entry.version, skitOptions);
|
|
401
|
-
pkgSpan.end({ success: installResult.success });
|
|
402
277
|
return {
|
|
403
278
|
success: installResult.success,
|
|
404
279
|
error: installResult.error,
|
|
@@ -421,12 +296,20 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId) {
|
|
|
421
296
|
});
|
|
422
297
|
result.installed.push(...parallelResult.installed);
|
|
423
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
|
+
});
|
|
424
307
|
return result;
|
|
425
308
|
}
|
|
426
309
|
/**
|
|
427
310
|
* 타입별 패키지 설치 (non-skit)
|
|
428
311
|
*/
|
|
429
|
-
async function installPackagesByType(packages, installerName, installers, snapshotName, indent, typeLabel, sessionId) {
|
|
312
|
+
async function installPackagesByType(packages, installerName, installers, snapshotName, indent, typeLabel, sessionId, recorder) {
|
|
430
313
|
const result = { installed: [], failed: [] };
|
|
431
314
|
const packageEntries = Object.entries(packages);
|
|
432
315
|
if (packageEntries.length === 0) {
|
|
@@ -434,7 +317,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
434
317
|
}
|
|
435
318
|
// Skit packages use dedicated function
|
|
436
319
|
if (installerName === 'skit') {
|
|
437
|
-
return installSkitPackages(packages, snapshotName, indent, sessionId);
|
|
320
|
+
return installSkitPackages(packages, snapshotName, indent, sessionId, recorder);
|
|
438
321
|
}
|
|
439
322
|
const installerConfig = installers[installerName] || DEFAULT_INSTALLERS[installerName];
|
|
440
323
|
if (!installerConfig) {
|
|
@@ -450,7 +333,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
450
333
|
if (!commandExists(cmdName)) {
|
|
451
334
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
452
335
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
453
|
-
if (
|
|
336
|
+
if (runCommand(installerConfig.hint)) {
|
|
454
337
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
455
338
|
}
|
|
456
339
|
else {
|
|
@@ -483,7 +366,15 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
483
366
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
484
367
|
const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
|
|
485
368
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
486
|
-
const success =
|
|
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
|
+
});
|
|
487
378
|
if (success) {
|
|
488
379
|
for (const spec of specs) {
|
|
489
380
|
console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -498,7 +389,14 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
498
389
|
for (const entry of toInstall) {
|
|
499
390
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
500
391
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
501
|
-
const ok =
|
|
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
|
+
});
|
|
502
400
|
if (ok) {
|
|
503
401
|
console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
504
402
|
result.installed.push(pkgSpec);
|
|
@@ -516,7 +414,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
516
414
|
const parallelResult = await parallelInstall(entries, async (entry) => {
|
|
517
415
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
518
416
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
519
|
-
const success =
|
|
417
|
+
const success = runCommand(command);
|
|
520
418
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
521
419
|
}, {
|
|
522
420
|
concurrency: 4,
|
|
@@ -533,15 +431,65 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
533
431
|
});
|
|
534
432
|
result.installed.push(...parallelResult.installed);
|
|
535
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
|
+
});
|
|
536
442
|
}
|
|
537
443
|
return result;
|
|
538
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
|
+
}
|
|
539
481
|
export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}, options = {}) {
|
|
540
482
|
const indent = ' '.repeat(depth);
|
|
541
483
|
const result = {
|
|
542
484
|
installed: [],
|
|
543
485
|
failed: [],
|
|
544
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
|
+
}
|
|
545
493
|
// Merge installers: snapshot + parent (parent takes precedence for overrides like npm-proxy)
|
|
546
494
|
let installers = { ...DEFAULT_INSTALLERS };
|
|
547
495
|
if (snapshot.installers) {
|
|
@@ -641,21 +589,21 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
641
589
|
const resolved = await resolveSnapshotDependencies(snapshot, { skipRegistry: true });
|
|
642
590
|
// skit 패키지 설치 (skit-adapter 사용: lib 우선, CLI fallback)
|
|
643
591
|
if (Object.keys(resolved.skit).length > 0) {
|
|
644
|
-
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);
|
|
645
593
|
result.installed.push(...skitResult.installed);
|
|
646
594
|
result.failed.push(...skitResult.failed);
|
|
647
595
|
}
|
|
648
596
|
// npm 패키지 설치 (proxy 사용 시 npm-proxy, 아니면 glpkg)
|
|
649
597
|
if (Object.keys(resolved.npm).length > 0) {
|
|
650
598
|
const npmInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
651
|
-
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);
|
|
652
600
|
result.installed.push(...npmResult.installed);
|
|
653
601
|
result.failed.push(...npmResult.failed);
|
|
654
602
|
}
|
|
655
603
|
// generic 패키지 설치 (glpkg)
|
|
656
604
|
if (Object.keys(resolved.generic).length > 0) {
|
|
657
605
|
const genericInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
658
|
-
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);
|
|
659
607
|
result.installed.push(...genericResult.installed);
|
|
660
608
|
result.failed.push(...genericResult.failed);
|
|
661
609
|
}
|
|
@@ -672,7 +620,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
672
620
|
continue;
|
|
673
621
|
// Skit packages: use skit-adapter (lib 우선, CLI fallback)
|
|
674
622
|
if (installerName === 'skit') {
|
|
675
|
-
const skitResult = await installSkitPackages(packages, snapshot.name, indent, options.sessionId);
|
|
623
|
+
const skitResult = await installSkitPackages(packages, snapshot.name, indent, options.sessionId, options.recorder);
|
|
676
624
|
result.installed.push(...skitResult.installed);
|
|
677
625
|
result.failed.push(...skitResult.failed);
|
|
678
626
|
continue;
|
|
@@ -691,7 +639,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
691
639
|
if (!commandExists(cmdName)) {
|
|
692
640
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
693
641
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
694
|
-
if (
|
|
642
|
+
if (runCommand(installerConfig.hint)) {
|
|
695
643
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
696
644
|
}
|
|
697
645
|
else {
|
|
@@ -725,7 +673,15 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
725
673
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
726
674
|
const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
|
|
727
675
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
728
|
-
const success =
|
|
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
|
+
});
|
|
729
685
|
if (success) {
|
|
730
686
|
for (const spec of specs) {
|
|
731
687
|
console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -740,7 +696,14 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
740
696
|
for (const entry of toInstall) {
|
|
741
697
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
742
698
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
743
|
-
const ok =
|
|
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
|
+
});
|
|
744
707
|
if (ok) {
|
|
745
708
|
console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
746
709
|
result.installed.push(pkgSpec);
|
|
@@ -758,7 +721,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
758
721
|
const parallelResult = await parallelInstall(installEntries, async (entry) => {
|
|
759
722
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
760
723
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
761
|
-
const success =
|
|
724
|
+
const success = runCommand(command);
|
|
762
725
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
763
726
|
}, {
|
|
764
727
|
concurrency: 4,
|
|
@@ -775,6 +738,14 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
775
738
|
});
|
|
776
739
|
result.installed.push(...parallelResult.installed);
|
|
777
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
|
+
});
|
|
778
749
|
}
|
|
779
750
|
}
|
|
780
751
|
}
|
|
@@ -788,7 +759,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
788
759
|
if (!commandExists(cmdName)) {
|
|
789
760
|
console.log(chalk.yellow(`${indent}glpkg not found. Attempting auto-install...`));
|
|
790
761
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
791
|
-
if (
|
|
762
|
+
if (runCommand(installerConfig.hint)) {
|
|
792
763
|
console.log(chalk.green(`${indent} ✓ glpkg installed successfully`));
|
|
793
764
|
}
|
|
794
765
|
else {
|
|
@@ -806,7 +777,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
806
777
|
const parallelResult = await parallelInstall(legacyEntries, async (entry) => {
|
|
807
778
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
808
779
|
const command = legacyCfg.command.replace('{pkg}', pkgSpec);
|
|
809
|
-
const success =
|
|
780
|
+
const success = runCommand(command);
|
|
810
781
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
811
782
|
}, {
|
|
812
783
|
concurrency: 4,
|
|
@@ -823,11 +794,19 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
823
794
|
});
|
|
824
795
|
result.installed.push(...parallelResult.installed);
|
|
825
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
|
+
});
|
|
826
805
|
}
|
|
827
806
|
}
|
|
828
807
|
// Legacy: skits (use skit-adapter: lib 우선, CLI fallback)
|
|
829
808
|
if (snapshot.skits && Object.keys(snapshot.skits).length > 0) {
|
|
830
|
-
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);
|
|
831
810
|
result.installed.push(...skitResult.installed);
|
|
832
811
|
result.failed.push(...skitResult.failed);
|
|
833
812
|
}
|
|
@@ -887,7 +866,7 @@ async function collectAllNpmPackages(snapshot, useProxy = true) {
|
|
|
887
866
|
* This ensures .npmrc is configured with ALL scopes before installation begins,
|
|
888
867
|
* including scopes from base snapshots (e.g., @microwiseai from @ist/minimal).
|
|
889
868
|
*/
|
|
890
|
-
async function collectAllPackages(snapshot) {
|
|
869
|
+
export async function collectAllPackages(snapshot) {
|
|
891
870
|
const allPackages = {};
|
|
892
871
|
// Collect from current snapshot
|
|
893
872
|
if (snapshot.packages) {
|
|
@@ -1017,7 +996,7 @@ async function runReplaceMode(snapshotName, newSet, options, installers) {
|
|
|
1017
996
|
continue;
|
|
1018
997
|
}
|
|
1019
998
|
console.log(chalk.gray(` Removing ${item.name}...`));
|
|
1020
|
-
if (
|
|
999
|
+
if (runCommand(cmd)) {
|
|
1021
1000
|
console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
|
|
1022
1001
|
removed.push(item);
|
|
1023
1002
|
}
|
|
@@ -1078,10 +1057,6 @@ async function previewInstall(snapshot, options) {
|
|
|
1078
1057
|
console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
|
|
1079
1058
|
}
|
|
1080
1059
|
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
1060
|
console.log(chalk.blue.bold(`
|
|
1086
1061
|
╔══════════════════════════════════════════════════════════════╗
|
|
1087
1062
|
║ Snapshot Installer ║
|
|
@@ -1115,7 +1090,6 @@ export async function installCommand(name, options = {}) {
|
|
|
1115
1090
|
//
|
|
1116
1091
|
// License key CANNOT be used as Bearer token for registry-proxy — they are
|
|
1117
1092
|
// distinct: AuthN (uid via IST JWT) vs AuthZ (entitlement via license key).
|
|
1118
|
-
const authSpan = activeTimeline.start('auth-token', { category: 'phase' });
|
|
1119
1093
|
const licenseKey = getLicenseKey();
|
|
1120
1094
|
let authToken = null;
|
|
1121
1095
|
let authType = 'anonymous';
|
|
@@ -1132,16 +1106,13 @@ export async function installCommand(name, options = {}) {
|
|
|
1132
1106
|
console.log(chalk.yellow('⚠') + ' No auth token — anonymous access');
|
|
1133
1107
|
}
|
|
1134
1108
|
const useProxy = true;
|
|
1135
|
-
authSpan.end({ authType });
|
|
1136
1109
|
// Fetch snapshot first — access check depends on snapshot.json content
|
|
1137
1110
|
console.log(chalk.gray('Fetching snapshot...'));
|
|
1138
|
-
const fetchSpan = activeTimeline.start('fetch-snapshot', { category: 'phase' });
|
|
1139
1111
|
let snapshot;
|
|
1140
1112
|
try {
|
|
1141
1113
|
snapshot = await fetchSnapshotFromRepo(name, authToken);
|
|
1142
1114
|
}
|
|
1143
1115
|
catch (error) {
|
|
1144
|
-
fetchSpan.end({ success: false });
|
|
1145
1116
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1146
1117
|
if (options.dryRun) {
|
|
1147
1118
|
// Pure preview: a fetch failure (offline / no auth) is not fatal — we just
|
|
@@ -1156,7 +1127,6 @@ export async function installCommand(name, options = {}) {
|
|
|
1156
1127
|
console.log(chalk.yellow('Check the snapshot name and version, e.g. snapshot install @scope/name@version'));
|
|
1157
1128
|
process.exit(1);
|
|
1158
1129
|
}
|
|
1159
|
-
fetchSpan.end({ success: true });
|
|
1160
1130
|
console.log(chalk.green('✓') + ` Snapshot: ${snapshot.name} v${snapshot.version}`);
|
|
1161
1131
|
if (snapshot.description) {
|
|
1162
1132
|
console.log(chalk.gray(` ${snapshot.description}`));
|
|
@@ -1174,30 +1144,62 @@ export async function installCommand(name, options = {}) {
|
|
|
1174
1144
|
await previewInstall(snapshot, options);
|
|
1175
1145
|
return;
|
|
1176
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
|
+
}
|
|
1177
1172
|
// ========== Access Check (based on snapshot.json access field) ==========
|
|
1178
|
-
//
|
|
1179
|
-
// - access: "public"
|
|
1173
|
+
// Determine access type from snapshot.json:
|
|
1174
|
+
// - access: "public" → no login/license required
|
|
1175
|
+
// - access: "authenticated" → IST login required, no license needed
|
|
1180
1176
|
// - access: { type: "licensed", productId } → license required
|
|
1181
|
-
// -
|
|
1182
|
-
// (registry-proxy / license API are authoritative; a 401/403 fails loud).
|
|
1183
|
-
// No client-side login gate: access control is backend-authoritative.
|
|
1177
|
+
// - access field absent → treated as licensed (requires license)
|
|
1184
1178
|
const accessField = snapshot.access;
|
|
1185
1179
|
const isPublicAccess = accessField === 'public';
|
|
1180
|
+
const isAuthenticatedAccess = accessField === 'authenticated';
|
|
1186
1181
|
const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
|
|
1187
|
-
//
|
|
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).
|
|
1182
|
+
// --- IST Login Check ---
|
|
1190
1183
|
const istCredentials = getIstCredentials();
|
|
1184
|
+
if (istCredentials?.userId) {
|
|
1185
|
+
console.log(chalk.green('✓') + ' IST login verified');
|
|
1186
|
+
}
|
|
1187
|
+
else if (isPublicAccess) {
|
|
1188
|
+
console.log(chalk.yellow('⚠') + ' No IST login - installing public package');
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
console.log(chalk.red('⛔ IST login required for non-public packages'));
|
|
1192
|
+
console.log(chalk.gray(' Run: ist auth login'));
|
|
1193
|
+
console.log(chalk.gray(' Or install a public snapshot first (no login required)'));
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
}
|
|
1191
1196
|
// --- License Check ---
|
|
1192
|
-
const accessSpan = activeTimeline.start('access-license', {
|
|
1193
|
-
category: 'phase',
|
|
1194
|
-
meta: { access: isPublicAccess ? 'public' : isLicensedAccess ? 'licensed' : 'server-decided' },
|
|
1195
|
-
});
|
|
1196
1197
|
const storedLicenseKey = getLicenseKey();
|
|
1197
1198
|
let licenseValid = false;
|
|
1198
|
-
if (isPublicAccess) {
|
|
1199
|
-
// Public snapshots — skip license check entirely
|
|
1200
|
-
|
|
1199
|
+
if (isPublicAccess || isAuthenticatedAccess) {
|
|
1200
|
+
// Public and authenticated snapshots — skip license check entirely
|
|
1201
|
+
const label = isAuthenticatedAccess ? 'Authenticated' : 'Public';
|
|
1202
|
+
console.log(chalk.green('✓') + ` ${label} snapshot - no license required`);
|
|
1201
1203
|
}
|
|
1202
1204
|
else if (storedLicenseKey) {
|
|
1203
1205
|
try {
|
|
@@ -1239,10 +1241,8 @@ export async function installCommand(name, options = {}) {
|
|
|
1239
1241
|
}
|
|
1240
1242
|
console.log(chalk.green('✓') + ` License verified for ${productId}`);
|
|
1241
1243
|
}
|
|
1242
|
-
accessSpan.end({ licenseValid });
|
|
1243
1244
|
// =======================================================================
|
|
1244
1245
|
// ========== Authorize Session with Registry Proxy ==========
|
|
1245
|
-
const authorizeSpan = activeTimeline.start('authorize-session', { category: 'phase' });
|
|
1246
1246
|
let sessionId;
|
|
1247
1247
|
if (authToken) {
|
|
1248
1248
|
// Collect all packages for session authorization
|
|
@@ -1278,10 +1278,8 @@ export async function installCommand(name, options = {}) {
|
|
|
1278
1278
|
}
|
|
1279
1279
|
}
|
|
1280
1280
|
}
|
|
1281
|
-
authorizeSpan.end({ sessionAuthorized: sessionId !== undefined });
|
|
1282
1281
|
// =============================================================
|
|
1283
1282
|
// Collect all packages from entire extends chain to determine scopes
|
|
1284
|
-
const configureSpan = activeTimeline.start('configure-npmrc', { category: 'phase' });
|
|
1285
1283
|
const allPackages = await collectAllPackages(snapshot);
|
|
1286
1284
|
// Configure .npmrc for proxy (with or without auth token)
|
|
1287
1285
|
let npmrcBackup = null;
|
|
@@ -1301,7 +1299,6 @@ export async function installCommand(name, options = {}) {
|
|
|
1301
1299
|
}
|
|
1302
1300
|
}
|
|
1303
1301
|
}
|
|
1304
|
-
configureSpan.end({ packageCount: Object.keys(allPackages).length });
|
|
1305
1302
|
// Guarantee .npmrc restoration even on ^C. A try/finally only covers thrown
|
|
1306
1303
|
// errors and normal returns — it does NOT run on SIGINT/SIGTERM, so an
|
|
1307
1304
|
// interrupt during the pre-install batch or the main install would leave a
|
|
@@ -1336,13 +1333,24 @@ export async function installCommand(name, options = {}) {
|
|
|
1336
1333
|
effectiveInstallers['glpkg'] = DEFAULT_INSTALLERS['npm-proxy'];
|
|
1337
1334
|
effectiveInstallers['gitlab-install'] = DEFAULT_INSTALLERS['npm-proxy'];
|
|
1338
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();
|
|
1339
1340
|
// ========== Pre-install: batch all npm packages from extends chain ==========
|
|
1340
1341
|
// Collect all npm-type packages across the entire extends chain and install
|
|
1341
1342
|
// 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
1343
|
const allNpmPackages = await collectAllNpmPackages(snapshot, useProxy);
|
|
1345
|
-
|
|
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);
|
|
1346
1354
|
if (npmEntries.length > 0) {
|
|
1347
1355
|
console.log('');
|
|
1348
1356
|
console.log(chalk.cyan(`► Pre-installing ${npmEntries.length} npm packages from entire extends chain...`));
|
|
@@ -1358,10 +1366,18 @@ export async function installCommand(name, options = {}) {
|
|
|
1358
1366
|
}
|
|
1359
1367
|
if (toInstall.length > 0) {
|
|
1360
1368
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
1361
|
-
batchSpecs = specs;
|
|
1362
1369
|
const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
|
|
1363
1370
|
console.log(chalk.gray(` Running single batch: npm install -g (${specs.length} packages)`));
|
|
1364
|
-
const batchSuccess =
|
|
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
|
+
});
|
|
1365
1381
|
if (batchSuccess) {
|
|
1366
1382
|
for (const spec of specs) {
|
|
1367
1383
|
console.log(` ${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
|
|
@@ -1377,7 +1393,14 @@ export async function installCommand(name, options = {}) {
|
|
|
1377
1393
|
for (const entry of toInstall) {
|
|
1378
1394
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
1379
1395
|
const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
|
|
1380
|
-
const ok =
|
|
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
|
+
});
|
|
1381
1404
|
if (ok) {
|
|
1382
1405
|
console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
|
|
1383
1406
|
}
|
|
@@ -1391,13 +1414,19 @@ export async function installCommand(name, options = {}) {
|
|
|
1391
1414
|
console.log(chalk.green('✓ All npm packages already installed'));
|
|
1392
1415
|
}
|
|
1393
1416
|
}
|
|
1394
|
-
batchSpan.end({ specs: batchSpecs, npmDeclared: npmEntries.length, npmToInstall: batchSpecs.length });
|
|
1395
1417
|
// =============================================================================
|
|
1396
1418
|
// Install (handles extends recursively)
|
|
1397
1419
|
// installSnapshot's internal isNpmGlobalInstalled checks will skip already-installed packages
|
|
1398
1420
|
let installError = null;
|
|
1399
1421
|
let result = { installed: [], failed: [] };
|
|
1400
|
-
|
|
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;
|
|
1401
1430
|
try {
|
|
1402
1431
|
// Auto-detect 모드: snapshot.install이 없으면 자동 판별
|
|
1403
1432
|
const autoDetect = !snapshot.install;
|
|
@@ -1405,18 +1434,56 @@ export async function installCommand(name, options = {}) {
|
|
|
1405
1434
|
useProxy,
|
|
1406
1435
|
autoDetect,
|
|
1407
1436
|
sessionId,
|
|
1437
|
+
recorder,
|
|
1438
|
+
overrides,
|
|
1408
1439
|
});
|
|
1409
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
|
+
}
|
|
1410
1482
|
}
|
|
1411
1483
|
catch (err) {
|
|
1412
1484
|
installError = err instanceof Error ? err : new Error(String(err));
|
|
1413
1485
|
}
|
|
1414
1486
|
finally {
|
|
1415
|
-
installSpan.end({
|
|
1416
|
-
installed: result.installed.length,
|
|
1417
|
-
failed: result.failed.length,
|
|
1418
|
-
success: installError === null,
|
|
1419
|
-
});
|
|
1420
1487
|
// Restore .npmrc + clear the session-token env (idempotent: a SIGINT/SIGTERM
|
|
1421
1488
|
// handler may have already run this). Then drop the signal handlers we
|
|
1422
1489
|
// registered, so they don't leak across multiple installs in one process.
|
|
@@ -1425,7 +1492,6 @@ export async function installCommand(name, options = {}) {
|
|
|
1425
1492
|
process.removeListener('SIGTERM', onSignal);
|
|
1426
1493
|
}
|
|
1427
1494
|
if (installError) {
|
|
1428
|
-
activeTimeline.finalize();
|
|
1429
1495
|
console.log(chalk.red(`Error during installation: ${installError.message}`));
|
|
1430
1496
|
process.exit(1);
|
|
1431
1497
|
}
|
|
@@ -1436,6 +1502,16 @@ export async function installCommand(name, options = {}) {
|
|
|
1436
1502
|
console.log(chalk.yellow('═══════════════════════════════════'));
|
|
1437
1503
|
const total = result.installed.length + result.failed.length;
|
|
1438
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
|
+
}
|
|
1439
1515
|
if (result.failed.length > 0) {
|
|
1440
1516
|
console.log('');
|
|
1441
1517
|
console.log(chalk.red('Failed:'));
|
|
@@ -1456,6 +1532,101 @@ export async function installCommand(name, options = {}) {
|
|
|
1456
1532
|
console.log('');
|
|
1457
1533
|
console.log(chalk.green('✓') + ' Snapshot info saved');
|
|
1458
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
|
+
}
|
|
1459
1630
|
// Record references for the ENTIRE declared set (installed + dedup-skipped),
|
|
1460
1631
|
// so replace mode has accurate referencedBy data (ADR 001 §c). This runs on
|
|
1461
1632
|
// every install, not just --replace, to build up the reference graph.
|
|
@@ -1472,9 +1643,18 @@ export async function installCommand(name, options = {}) {
|
|
|
1472
1643
|
await runReplaceMode(snapshot.name, allPackages, options, effectiveInstallers);
|
|
1473
1644
|
}
|
|
1474
1645
|
}
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1646
|
+
// Post-install: run doctor to check prerequisites
|
|
1647
|
+
console.log('');
|
|
1648
|
+
const { checkPrerequisites, MINIMAL_PREREQUISITES } = await import('../lib/prerequisites.js');
|
|
1649
|
+
const prereqResults = checkPrerequisites(MINIMAL_PREREQUISITES);
|
|
1650
|
+
const missingPrereqs = prereqResults.filter(r => !r.installed);
|
|
1651
|
+
if (missingPrereqs.length > 0) {
|
|
1652
|
+
console.log(chalk.yellow(`\n⚠ ${missingPrereqs.length} prerequisite(s) not satisfied:`));
|
|
1653
|
+
for (const r of missingPrereqs) {
|
|
1654
|
+
console.log(` ${chalk.red('❌')} ${r.name} — required by ${r.requiredBy.join(', ')}`);
|
|
1655
|
+
}
|
|
1656
|
+
console.log(chalk.gray(`\nRun 'snapshot setup' to install, or 'snapshot doctor' for details.`));
|
|
1657
|
+
}
|
|
1478
1658
|
// Send telemetry (fire and forget)
|
|
1479
1659
|
sendInstallTelemetry({
|
|
1480
1660
|
package: snapshot.name,
|