@haystackeditor/cli 0.27.0 → 0.27.1
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.
|
@@ -62,7 +62,8 @@ export function recordedApps(gitRoot, candidates) {
|
|
|
62
62
|
return readCaptureConfig(join(gitRoot, dir))?.app === dir;
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
|
-
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(dir === '.' ? message : message.replace(CAPTURE_CONFIG_PATH, posix.join(dir, CAPTURE_CONFIG_PATH)));
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
}
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* stores or reads one), production's environment, the start command, the deploy and the off switches (13c).
|
|
22
22
|
*/
|
|
23
23
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
24
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { accessSync, constants, existsSync, lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { createRequire } from 'node:module';
|
|
26
|
-
import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
26
|
+
import { delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import { classifyHttpError, repoApiPath } from '../utils/haystack-api.js';
|
|
@@ -103,7 +103,8 @@ function section(json, name) {
|
|
|
103
103
|
}
|
|
104
104
|
const LOCKFILES = [['pnpm-lock.yaml', 'pnpm'], ['package-lock.json', 'npm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun']];
|
|
105
105
|
/** The package manager that owns the app's lockfile: the nearest `packageManager` field, else the nearest lockfile, from
|
|
106
|
-
* the app up to the repository root. null: no lockfile, so the dependency is written into package.json
|
|
106
|
+
* the app up to the repository root (`from` says which). null: no lockfile, so the dependency is written into package.json
|
|
107
|
+
* itself. */
|
|
107
108
|
function packageManager(gitRoot, appDir) {
|
|
108
109
|
for (let directory = appDir;; directory = dirname(directory)) {
|
|
109
110
|
const manifest = join(directory, 'package.json');
|
|
@@ -111,16 +112,32 @@ function packageManager(gitRoot, appDir) {
|
|
|
111
112
|
const declared = readPackage(manifest).json.packageManager;
|
|
112
113
|
const name = typeof declared === 'string' ? /^(pnpm|npm|yarn|bun)@/.exec(declared)?.[1] : undefined;
|
|
113
114
|
if (name)
|
|
114
|
-
return { name: name, root: directory };
|
|
115
|
+
return { name: name, root: directory, from: `${inside(gitRoot, manifest)}'s packageManager field` };
|
|
115
116
|
}
|
|
116
117
|
for (const [lockfile, name] of LOCKFILES) {
|
|
117
118
|
if (isFile(join(directory, lockfile)))
|
|
118
|
-
return { name, root: directory };
|
|
119
|
+
return { name, root: directory, from: inside(gitRoot, join(directory, lockfile)) };
|
|
119
120
|
}
|
|
120
121
|
if (directory === gitRoot || dirname(directory) === directory)
|
|
121
122
|
return null;
|
|
122
123
|
}
|
|
123
124
|
}
|
|
125
|
+
/** Whether `command` is an executable on PATH (a package manager init would run, checked before it plans to). */
|
|
126
|
+
function onPath(command) {
|
|
127
|
+
const extensions = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
128
|
+
for (const directory of (process.env.PATH ?? '').split(delimiter)) {
|
|
129
|
+
if (directory === '')
|
|
130
|
+
continue;
|
|
131
|
+
for (const extension of extensions) {
|
|
132
|
+
try {
|
|
133
|
+
accessSync(join(directory, `${command}${extension}`), constants.X_OK);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch { /* not in this directory */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
124
141
|
/** The first release whose sites record repository-relative paths (what a registered tenant's src_prefix '' relies on)
|
|
125
142
|
* and that ships the `./next` entry: earlier releases are not compatible with either integration. */
|
|
126
143
|
const CLI_COMPATIBLE_SINCE = [0, 24, 0];
|
|
@@ -216,14 +233,29 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
216
233
|
const spec = `${CLI_PACKAGE}@${version}`;
|
|
217
234
|
const dev = field === 'devDependencies';
|
|
218
235
|
const workspaceRoot = isFile(join(appDir, 'pnpm-workspace.yaml')) ? ['--workspace-root'] : [];
|
|
219
|
-
// Each package manager's add, as it changes no lockfile entry but the CLI's own (lockfile-pin.ts
|
|
220
|
-
// nested under the CLI (hoisting would replace packages the app already locks), pnpm through the merge.
|
|
236
|
+
// Each package manager's add, as it changes no lockfile entry but the CLI's own (lockfile-pin.ts; pnpm through the merge).
|
|
221
237
|
const argv = manager === null ? null : {
|
|
222
238
|
pnpm: ['pnpm', 'add', '--save-exact', dev ? '--save-dev' : '--save-prod', ...workspaceRoot, spec],
|
|
223
|
-
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod',
|
|
239
|
+
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod', spec],
|
|
224
240
|
yarn: ['yarn', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
225
241
|
bun: ['bun', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
226
242
|
}[manager.name];
|
|
243
|
+
const appPath = inside(gitRoot, appDir) || '.';
|
|
244
|
+
// npm re-resolves entries the CLI does not own whatever the flags (39 in fosrl/pangolin, with --install-strategy=nested
|
|
245
|
+
// too), so init never rewrites an npm lockfile: the person adds the CLI with npm and sees the lockfile's diff.
|
|
246
|
+
if (manager?.name === 'npm' && argv) {
|
|
247
|
+
return { kind: 'manual', why: `${appPath} uses npm (${manager.from}), whose install also re-resolves lockfile entries the CLI does not own, so init`
|
|
248
|
+
+ ` does not run it. Run \`${argv.join(' ')}\` in ${appPath}, check package-lock.json's diff, then run \`haystack init\` again` };
|
|
249
|
+
}
|
|
250
|
+
// A package manager that is not installed is never run (rule 13: telemetry never fails init).
|
|
251
|
+
if (manager && argv && !onPath(manager.name)) {
|
|
252
|
+
const provide = (manager.name === 'pnpm' || manager.name === 'yarn') && onPath('corepack')
|
|
253
|
+
? `run \`corepack enable ${manager.name}\` (corepack is installed here)`
|
|
254
|
+
: `install it (${manager.name === 'bun' ? 'https://bun.sh' : manager.name === 'yarn' ? 'https://yarnpkg.com/getting-started/install' : 'https://pnpm.io/installation'})`;
|
|
255
|
+
return { kind: 'manual', why: `${appPath} uses ${manager.name} (${manager.from}), which is not installed here, so init did not run it. Either`
|
|
256
|
+
+ ` ${provide} and run \`haystack init\` again, or run \`${argv.join(' ')}\` in ${appPath} wherever ${manager.name} is installed and then`
|
|
257
|
+
+ ' run `haystack init` again' };
|
|
258
|
+
}
|
|
227
259
|
const action = promoting ? `Moved from devDependencies to dependencies at ${version}` : `Pinned to ${version}`;
|
|
228
260
|
const lockfileNote = manager?.name === 'pnpm'
|
|
229
261
|
? `, keeping every other entry of pnpm's lockfile as it was (pnpm's own add re-resolves packages the CLI shares with the app; init adds only the CLI's entries, then \`pnpm install --frozen-lockfile\`)`
|
|
@@ -247,12 +279,14 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
247
279
|
}
|
|
248
280
|
/** The lockfile a package manager writes, in a format lockfile-pin.ts reads (bun's binary lockb aside). */
|
|
249
281
|
function textLockfile(manager) {
|
|
250
|
-
|
|
282
|
+
if (manager.name === 'npm')
|
|
283
|
+
throw new Error('init never runs npm to pin the CLI');
|
|
284
|
+
const name = { pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock', bun: 'bun.lock' }[manager.name];
|
|
251
285
|
const path = join(manager.root, name);
|
|
252
286
|
return isFile(path) ? { path, format: manager.name } : null;
|
|
253
287
|
}
|
|
254
288
|
/** Runs the add, so that the lockfile gains the CLI's entries and nothing else moves (rule 13); throws (init then restores
|
|
255
|
-
*
|
|
289
|
+
* telemetry's files and leaves telemetry manual) when the package manager moved anything else. */
|
|
256
290
|
function pin(manager, appDir, argv) {
|
|
257
291
|
const run = (command) => { execFileSync(command[0], command.slice(1), { cwd: appDir, stdio: ['ignore', 2, 2] }); };
|
|
258
292
|
const lockfile = textLockfile(manager);
|
|
@@ -272,8 +306,8 @@ function pin(manager, appDir, argv) {
|
|
|
272
306
|
const moved = lockfileDrift(lockfile.format, before, readFileSync(lockfile.path, 'utf8'), importer, CLI_PACKAGE);
|
|
273
307
|
if (moved.length > 0) {
|
|
274
308
|
throw new Error(`\`${argv.join(' ')}\` also changed ${moved.length} lockfile ${moved.length === 1 ? 'entry' : 'entries'} the CLI does not own`
|
|
275
|
-
+ ` (${moved.slice(0, 5).join(', ')}${moved.length > 5 ? ', …' : ''})
|
|
276
|
-
+ `
|
|
309
|
+
+ ` (${moved.slice(0, 5).join(', ')}${moved.length > 5 ? ', …' : ''}). Add ${CLI_PACKAGE} at its exact version to`
|
|
310
|
+
+ ` ${inside(manager.root, appDir) || '.'} yourself, keeping the lockfile's other entries, then run \`haystack init\` again`);
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
313
|
function isNode(value) {
|
|
@@ -1006,6 +1040,22 @@ export function previewTelemetry(plan, repository, notes) {
|
|
|
1006
1040
|
const profile = dbProfile(repository, null, notes);
|
|
1007
1041
|
return { telemetry: { capture: plan.capture, server: serverPartOf(plan), dbProfileCommand: profile?.command ?? null }, steps };
|
|
1008
1042
|
}
|
|
1043
|
+
/** A telemetry plan with nothing planned, for a run whose telemetry could not be planned at all (failedTelemetry says why). */
|
|
1044
|
+
export function emptyTelemetryPlan() {
|
|
1045
|
+
return { app: null, changes: [], notices: [], server: { status: 'manual', kind: null, steps: [] }, settings: null,
|
|
1046
|
+
capture: { status: 'manual', steps: [], received: null }, captureRegistration: null };
|
|
1047
|
+
}
|
|
1048
|
+
/** Telemetry that could not be set up (a package manager failed, a file init could not read, Haystack not answering): each
|
|
1049
|
+
* part it would have set up is manual with what happened, every file telemetry changed is back, and the rest of init stands
|
|
1050
|
+
* (telemetry never fails init). */
|
|
1051
|
+
export function failedTelemetry(plan, failure) {
|
|
1052
|
+
const step = `init could not set up telemetry: ${failure.replace(/\.?\s*$/, '')}. It put back every file telemetry changed; the rest of init is`
|
|
1053
|
+
+ ' done. Fix that, then run `haystack init` again.';
|
|
1054
|
+
// A part that was already waiting for answers keeps its questions after what happened.
|
|
1055
|
+
const part = (value) => (value.status === 'unsupported' ? value
|
|
1056
|
+
: { status: 'manual', steps: [step, ...(value.status === 'manual' ? value.steps : [])], received: null });
|
|
1057
|
+
return { telemetry: { capture: part(plan.capture), server: part(serverPartOf(plan)), dbProfileCommand: null }, steps: [step] };
|
|
1058
|
+
}
|
|
1009
1059
|
/** Rule 8(e): the report, and what the person does for telemetry to arrive (the token, production's environment, the start,
|
|
1010
1060
|
* the deploy) with the off switches (rule 13c). `onboarding` and `notes` feed the data profile. */
|
|
1011
1061
|
export async function telemetryReport(plan, context) {
|
package/dist/commands/init.js
CHANGED
|
@@ -299,6 +299,33 @@ export function planInit(gitRoot) {
|
|
|
299
299
|
const notices = [];
|
|
300
300
|
return { changes: [...planStopHook(gitRoot, notices), ...planAgentNotes(gitRoot, notices)], notices };
|
|
301
301
|
}
|
|
302
|
+
/** Makes `changes` all or nothing: one that fails (an install that does not end as planned) restores every file they
|
|
303
|
+
* touched (bytes, not text: a lockfile can be binary), so no edit is left referencing what was never installed. */
|
|
304
|
+
function applyAll(gitRoot, changes, say) {
|
|
305
|
+
const before = new Map();
|
|
306
|
+
for (const planned of changes) {
|
|
307
|
+
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
308
|
+
if (!before.has(path))
|
|
309
|
+
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
for (const planned of changes) {
|
|
314
|
+
applyChange(gitRoot, planned);
|
|
315
|
+
say(chalk.green(`✓ ${planned.action === 'create' ? 'Created' : planned.action === 'delete' ? 'Deleted' : 'Updated'} ${planned.path}`));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
for (const [path, content] of before) {
|
|
320
|
+
if (content !== null)
|
|
321
|
+
writeFileSync(path, content);
|
|
322
|
+
else if (existsSync(path))
|
|
323
|
+
unlinkSync(path);
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
302
329
|
function applyChange(gitRoot, planned) {
|
|
303
330
|
const inside = relative(gitRoot, planned.target);
|
|
304
331
|
if (!inside.startsWith('..') && !isAbsolute(inside))
|
|
@@ -362,19 +389,35 @@ export async function initCommand(options) {
|
|
|
362
389
|
// Read before anything else, so notes that cannot be used stop init with nothing done.
|
|
363
390
|
const notes = options.notes === undefined ? null : readNotes(options.notes);
|
|
364
391
|
const plan = planInit(gitRoot);
|
|
365
|
-
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it.
|
|
392
|
+
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it. It never fails
|
|
393
|
+
// init: whatever goes wrong with it (a file it cannot read, a package manager that fails, Haystack not answering) puts its
|
|
394
|
+
// own files back and leaves it manual with what happened, and the rest of init goes on.
|
|
366
395
|
const telemetryModule = await import('./init-telemetry.js');
|
|
367
396
|
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent, urlRewrites: options.urlRewrites };
|
|
368
|
-
let
|
|
397
|
+
let telemetryFailure = null;
|
|
398
|
+
let telemetryPlan = telemetryModule.emptyTelemetryPlan();
|
|
399
|
+
try {
|
|
400
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, telemetryOptions);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
telemetryFailure = errorText(error);
|
|
404
|
+
}
|
|
369
405
|
const telemetryFrom = plan.changes.length;
|
|
370
406
|
plan.changes.push(...telemetryPlan.changes);
|
|
371
407
|
plan.notices.push(...telemetryPlan.notices);
|
|
372
408
|
let notesWritten = null;
|
|
373
409
|
let telemetry = null;
|
|
410
|
+
/** Telemetry did not happen: its changes leave the plan (none of them was made), and the report says why. */
|
|
411
|
+
const telemetryFailed = (error) => {
|
|
412
|
+
telemetryFailure = errorText(error);
|
|
413
|
+
plan.changes.splice(telemetryFrom);
|
|
414
|
+
say(chalk.yellow(`Telemetry was not set up: ${telemetryFailure} The rest of init goes on; the telemetry report says what to do.`));
|
|
415
|
+
};
|
|
374
416
|
// Before the apply (a preview, or no login), the report is the preview: what telemetry will do, `received` unchecked (null).
|
|
375
|
-
const report = () =>
|
|
417
|
+
const report = () => (telemetryFailure !== null ? telemetryModule.failedTelemetry(telemetryPlan, telemetryFailure)
|
|
418
|
+
: telemetry ?? telemetryModule.previewTelemetry(telemetryPlan, repository, notes));
|
|
376
419
|
const showPreview = () => {
|
|
377
|
-
const preview =
|
|
420
|
+
const preview = report();
|
|
378
421
|
for (const line of telemetryModule.formatTelemetry(preview.telemetry, preview.steps))
|
|
379
422
|
say(line);
|
|
380
423
|
say('');
|
|
@@ -424,44 +467,48 @@ export async function initCommand(options) {
|
|
|
424
467
|
return;
|
|
425
468
|
}
|
|
426
469
|
}
|
|
427
|
-
//
|
|
428
|
-
|
|
470
|
+
// Init's own changes (the agent instructions, the Stop hook) first, all or nothing: a failure there is init's.
|
|
471
|
+
try {
|
|
472
|
+
applyAll(gitRoot, plan.changes.slice(0, telemetryFrom), say);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
throw new Error(`${errorText(error)} Every file init changed was restored; nothing was changed.`);
|
|
476
|
+
}
|
|
477
|
+
// Then telemetry, all or nothing on its own. Browser capture's registration (rule 8b), which the preview announced, is
|
|
478
|
+
// checked with Haystack first, and when the files need a new key the telemetry changes are planned again with it (or
|
|
479
|
+
// without capture's, when it is refused).
|
|
429
480
|
const approved = options.yes === true || plan.changes.length > 0;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
for (const planned of plan.changes) {
|
|
441
|
-
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
442
|
-
if (!before.has(path))
|
|
443
|
-
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
481
|
+
let capture = null;
|
|
482
|
+
if (telemetryFailure === null) {
|
|
483
|
+
try {
|
|
484
|
+
capture = telemetryPlan.captureRegistration
|
|
485
|
+
? await telemetryModule.prepareCapture(telemetryPlan.captureRegistration, token, say, approved) : null;
|
|
486
|
+
if (capture?.replan) {
|
|
487
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, { ...telemetryOptions, registration: capture.replan });
|
|
488
|
+
plan.changes.splice(telemetryFrom, plan.changes.length - telemetryFrom, ...telemetryPlan.changes);
|
|
489
|
+
}
|
|
490
|
+
applyAll(gitRoot, plan.changes.slice(telemetryFrom), say);
|
|
444
491
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
492
|
+
catch (error) {
|
|
493
|
+
try {
|
|
494
|
+
await capture?.rollback(say);
|
|
495
|
+
}
|
|
496
|
+
catch (rollbackError) {
|
|
497
|
+
say(chalk.yellow(`Capture's registration could not be put back: ${errorText(rollbackError)}`));
|
|
498
|
+
}
|
|
499
|
+
telemetryFailed(error);
|
|
450
500
|
}
|
|
451
501
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
502
|
+
if (telemetryFailure === null && capture) {
|
|
503
|
+
try {
|
|
504
|
+
const captureFollowUp = await capture.finish(say);
|
|
505
|
+
if (captureFollowUp)
|
|
506
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, `Run \`haystack init\` again: Haystack did not take the app's origins (${errorText(error)}).`);
|
|
458
510
|
}
|
|
459
|
-
await capture?.rollback(say);
|
|
460
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)} Every file init changed was restored; nothing was changed.`);
|
|
461
511
|
}
|
|
462
|
-
const captureFollowUp = await capture?.finish(say);
|
|
463
|
-
if (captureFollowUp)
|
|
464
|
-
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
465
512
|
let onboarding;
|
|
466
513
|
try {
|
|
467
514
|
// The notes first: onboarding plans from the facts it starts with, and new notes are new facts (a new onboarding).
|
|
@@ -485,8 +532,18 @@ export async function initCommand(options) {
|
|
|
485
532
|
reportOnboardingState(onboarding);
|
|
486
533
|
say(formatOnboarding(onboarding));
|
|
487
534
|
say('');
|
|
488
|
-
|
|
489
|
-
|
|
535
|
+
if (telemetryFailure === null) {
|
|
536
|
+
try {
|
|
537
|
+
telemetry = await telemetryModule.telemetryReport(telemetryPlan, { repository, token, onboarding, notes });
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
// What Haystack holds could not be read: the report as previewed (`received` unchecked), and why.
|
|
541
|
+
const preview = telemetryModule.previewTelemetry(telemetryPlan, repository, notes);
|
|
542
|
+
telemetry = { telemetry: preview.telemetry, steps: [...preview.steps, `Run \`haystack init --json\` again to read what telemetry Haystack has received (it could not: ${errorText(error)}).`] };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const shown = report();
|
|
546
|
+
for (const line of telemetryModule.formatTelemetry(shown.telemetry, shown.steps))
|
|
490
547
|
say(line);
|
|
491
548
|
say('');
|
|
492
549
|
if (onboarding.state === 'ready')
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* locks older versions of packages the CLI also uses):
|
|
5
5
|
* - yarn 1, yarn 4 and bun add only the new descriptors and entries; nothing else moves.
|
|
6
6
|
* - npm hoists the new tree and replaces hoisted packages the app already locks (ws, yaml, zod moved to the CLI's
|
|
7
|
-
* versions)
|
|
7
|
+
* versions), and re-resolves other entries even with `--install-strategy=nested` (39 in fosrl/pangolin): init never
|
|
8
|
+
* runs it (init-telemetry.ts leaves the npm install to the person, who sees the lockfile's diff).
|
|
8
9
|
* - pnpm re-resolves the dependencies of every package the new tree shares with the old one to the newest version in the
|
|
9
10
|
* graph (47 entries in Rallly, from @smithy/* to express), and no flag stops it. So init lets pnpm resolve the new
|
|
10
11
|
* dependency (`--lockfile-only`), keeps every entry the lockfile had, adds the entries only the new dependency
|
|
11
12
|
* reaches (mergePnpmLockfile), and installs exactly that (`pnpm install --frozen-lockfile`, which also proves it).
|
|
12
|
-
* lockfileDrift checks the result for every format init
|
|
13
|
-
* change (and init restores
|
|
13
|
+
* lockfileDrift checks the result for every format init runs, so a package manager that moves anything else fails the
|
|
14
|
+
* change (and init restores telemetry's files, leaving telemetry manual).
|
|
14
15
|
*/
|
|
15
16
|
function unquote(text) {
|
|
16
17
|
if (text.startsWith('\'') && text.endsWith('\''))
|
|
@@ -271,11 +272,6 @@ function entriesOf(format, text, importer) {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
|
-
else if (format === 'npm') {
|
|
275
|
-
const packages = JSON.parse(text).packages ?? {};
|
|
276
|
-
for (const [key, value] of Object.entries(packages))
|
|
277
|
-
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : key, JSON.stringify(value));
|
|
278
|
-
}
|
|
279
275
|
else if (format === 'yarn') {
|
|
280
276
|
for (const block of text.split(/\n\s*\n/)) {
|
|
281
277
|
const lines = block.split('\n').filter(line => line.trim() !== '' && !line.startsWith('#'));
|