@ekanos/cli 0.22.0 → 0.23.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/README.md +18 -4
- package/dist/commands/add.js +21 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/contract.js +12 -2
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/dev.js +33 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +80 -4
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +44 -0
- package/dist/commands/status.js +79 -18
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.js +287 -144
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/docs-links.js +5 -1
- package/dist/docs-links.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/exit-codes.d.ts +3 -0
- package/dist/exit-codes.js +3 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/experience/checks.js +2 -1
- package/dist/experience/checks.js.map +1 -1
- package/dist/experience/definition-checks.d.ts +7 -0
- package/dist/experience/definition-checks.js +36 -4
- package/dist/experience/definition-checks.js.map +1 -1
- package/dist/experience/init.js +10 -0
- package/dist/experience/init.js.map +1 -1
- package/dist/experience/load-experience.js +33 -2
- package/dist/experience/load-experience.js.map +1 -1
- package/dist/experience/pins.d.ts +10 -0
- package/dist/experience/pins.js +19 -1
- package/dist/experience/pins.js.map +1 -1
- package/dist/generated/phosphor-names.d.ts +3 -0
- package/dist/generated/phosphor-names.js +1541 -0
- package/dist/generated/phosphor-names.js.map +1 -0
- package/dist/harness-scaffold.d.ts +13 -0
- package/dist/harness-scaffold.js +19 -3
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/icon-findings.d.ts +12 -17
- package/dist/icon-findings.js +49 -74
- package/dist/icon-findings.js.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/lockfile-instances.d.ts +60 -0
- package/dist/lockfile-instances.js +177 -0
- package/dist/lockfile-instances.js.map +1 -0
- package/dist/pack-hygiene.d.ts +8 -0
- package/dist/pack-hygiene.js +57 -0
- package/dist/pack-hygiene.js.map +1 -0
- package/dist/package-manager.d.ts +34 -6
- package/dist/package-manager.js +130 -20
- package/dist/package-manager.js.map +1 -1
- package/dist/project.js +11 -0
- package/dist/project.js.map +1 -1
- package/dist/repo/scaffold.js +4 -0
- package/dist/repo/scaffold.js.map +1 -1
- package/dist/schema-skew.js.map +1 -1
- package/dist/validate-findings.js +10 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/workspace-root.d.ts +16 -0
- package/dist/workspace-root.js +50 -0
- package/dist/workspace-root.js.map +1 -0
- package/package.json +4 -4
- package/templates/AGENTS.md.tmpl +11 -15
- package/templates/experience/package.json.tmpl +8 -6
- package/templates/experience/src/widgets/hello-widget.test.tsx.tmpl +49 -0
- package/templates/experience/vitest.config.ts.tmpl +21 -0
- package/templates/harness/next.config.mjs.tmpl +7 -4
- package/templates/package.json.tmpl +5 -5
- package/templates/skills/build-integration/references/widget-design.md +2 -3
- package/templates/skills/polish-widgets/references/patterns.md +5 -5
- package/dist/generated/fa-free-names.d.ts +0 -9
- package/dist/generated/fa-free-names.js +0 -2599
- package/dist/generated/fa-free-names.js.map +0 -1
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { discoverRepo, isRepoRoot } from '@ekanos/integration-schema/repo';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as os from 'node:os';
|
|
@@ -7,7 +8,7 @@ import { cliVersion } from '../compatibility.js';
|
|
|
7
8
|
import { invalidStateError, networkError, preconditionError, validationError, } from '../errors.js';
|
|
8
9
|
import { EXIT_CODES } from '../exit-codes.js';
|
|
9
10
|
import { readInstalledPackageManifest } from '../harness-scaffold.js';
|
|
10
|
-
import { detectPackageManager, installExactArgs,
|
|
11
|
+
import { detectPackageManager, frozenInstallArgs, installExactArgs, isMultiPackageWorkspaceRoot, lockfileNames, } from '../package-manager.js';
|
|
11
12
|
import { ensureBuildScriptsAnswered, skippedBuildScriptsHint, findIgnoredBuilds, ignoredBuildsHint, isIgnoredBuildsFailure, WORKSPACE_FILE, } from '../pnpm-builds.js';
|
|
12
13
|
import { loadProject } from '../project.js';
|
|
13
14
|
import { classifyPath, writeFileNoFollow } from '../safe-fs.js';
|
|
@@ -18,18 +19,72 @@ import { displayPath } from '../ui.js';
|
|
|
18
19
|
import { collectAllFindings } from '../validate-findings.js';
|
|
19
20
|
import { AGENT_CONTEXT_FILES, DOCS_GROUNDING_FILES, copyBundledSkillTrees, } from './init.js';
|
|
20
21
|
import { promptYesNo } from './publish.js';
|
|
22
|
+
const TOOLCHAIN_NAMES = Object.values(TOOLCHAIN_PACKAGES);
|
|
23
|
+
function dependsOnToolchain(dir) {
|
|
24
|
+
const manifestPath = path.join(dir, 'package.json');
|
|
25
|
+
if (!fs.existsSync(manifestPath))
|
|
26
|
+
return false;
|
|
27
|
+
const manifest = readManifest(manifestPath);
|
|
28
|
+
return ['dependencies', 'devDependencies'].some((field) => {
|
|
29
|
+
const deps = manifest[field];
|
|
30
|
+
return (typeof deps === 'object' &&
|
|
31
|
+
deps !== null &&
|
|
32
|
+
TOOLCHAIN_NAMES.some((name) => name in deps));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function planUpgrade(ctx, dir) {
|
|
36
|
+
const absolute = path.resolve(dir);
|
|
37
|
+
if (!isRepoRoot(absolute)) {
|
|
38
|
+
const loaded = loadProject(dir);
|
|
39
|
+
return {
|
|
40
|
+
baseDir: loaded.projectDir,
|
|
41
|
+
repo: false,
|
|
42
|
+
targets: [
|
|
43
|
+
{
|
|
44
|
+
dir: loaded.projectDir,
|
|
45
|
+
relDir: '',
|
|
46
|
+
slug: loaded.primary.slug,
|
|
47
|
+
validate: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const repo = discoverRepo(absolute);
|
|
53
|
+
for (const problem of repo.problems) {
|
|
54
|
+
ctx.warn(`${problem.where}: ${problem.message}`);
|
|
55
|
+
}
|
|
56
|
+
const targets = [
|
|
57
|
+
...(dependsOnToolchain(repo.root)
|
|
58
|
+
? [{ dir: repo.root, relDir: '', slug: null, validate: false }]
|
|
59
|
+
: []),
|
|
60
|
+
...repo.packages
|
|
61
|
+
.filter((pkg) => dependsOnToolchain(pkg.dir))
|
|
62
|
+
.map((pkg) => ({
|
|
63
|
+
dir: pkg.dir,
|
|
64
|
+
relDir: pkg.relDir,
|
|
65
|
+
slug: pkg.kind === 'integration' ? pkg.name : null,
|
|
66
|
+
validate: pkg.kind === 'integration',
|
|
67
|
+
})),
|
|
68
|
+
];
|
|
69
|
+
return { baseDir: repo.root, repo: true, targets };
|
|
70
|
+
}
|
|
21
71
|
export async function runUpgrade(ctx, args, deps = {}) {
|
|
22
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
23
|
-
const
|
|
24
|
-
const pm = detectPackageManager(
|
|
72
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
73
|
+
const plan = planUpgrade(ctx, args.dir);
|
|
74
|
+
const pm = detectPackageManager(plan.baseDir);
|
|
25
75
|
// Captured BEFORE anything installs: the templates the project's CURRENT
|
|
26
76
|
// (pre-upgrade) local `@ekanos/cli` ships, which is what actually
|
|
27
77
|
// generated the agent-context files on disk — not necessarily this
|
|
28
78
|
// process's own bundled templates, since `upgrade` is a machine-level verb
|
|
29
79
|
// that never delegates and is routinely invoked via a DIFFERENT (global)
|
|
30
80
|
// install. See `regenerateAgentContextFiles`.
|
|
31
|
-
const oldAgentTemplates =
|
|
32
|
-
|
|
81
|
+
const oldAgentTemplates = new Map(plan.targets
|
|
82
|
+
.filter((target) => target.slug !== null)
|
|
83
|
+
.map((target) => [
|
|
84
|
+
target.dir,
|
|
85
|
+
captureAgentTemplates(cliTemplatesDir(target.dir, pm.root)),
|
|
86
|
+
]));
|
|
87
|
+
const current = readCurrentToolchain(plan.baseDir, pm.root);
|
|
33
88
|
const session = resolveSessionQuietly(ctx, args);
|
|
34
89
|
const resolveTarget = (_a = deps.resolveTarget) !== null && _a !== void 0 ? _a : resolveToolchainTarget;
|
|
35
90
|
const resolution = await resolveTarget({
|
|
@@ -55,16 +110,19 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
55
110
|
}
|
|
56
111
|
const target = resolution.target;
|
|
57
112
|
const deltas = computeDeltas(current, target);
|
|
113
|
+
const scope = Object.assign({ packageManager: { name: pm.name, root: pm.root } }, (plan.repo
|
|
114
|
+
? { packages: plan.targets.map((t) => t.relDir || '.') }
|
|
115
|
+
: {}));
|
|
58
116
|
if (args.check) {
|
|
59
|
-
return ctx.succeed(Object.assign({ source: resolution.source, current,
|
|
117
|
+
return ctx.succeed(Object.assign(Object.assign({ source: resolution.source, current,
|
|
60
118
|
target,
|
|
61
|
-
deltas }, (resolution.source === 'host'
|
|
119
|
+
deltas }, scope), (resolution.source === 'host'
|
|
62
120
|
? { minimum: resolution.minimum, notes: resolution.notes }
|
|
63
121
|
: {})), checkSummary(ctx.ui, resolution.source, deltas));
|
|
64
122
|
}
|
|
65
123
|
const changedKeys = Object.keys(deltas).filter((key) => deltas[key].changed);
|
|
66
124
|
if (changedKeys.length === 0) {
|
|
67
|
-
return ctx.succeed({ source: resolution.source, current, target, deltas }, ctx.ui.result({
|
|
125
|
+
return ctx.succeed(Object.assign({ source: resolution.source, current, target, deltas }, scope), ctx.ui.result({
|
|
68
126
|
title: 'Already up to date',
|
|
69
127
|
rows: [['Target', `resolved from ${resolution.source}`]],
|
|
70
128
|
}));
|
|
@@ -86,11 +144,12 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
86
144
|
'modified.');
|
|
87
145
|
}
|
|
88
146
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
147
|
+
const work = plan.targets.map((t) => ({
|
|
148
|
+
target: t,
|
|
149
|
+
specs: buildInstallSpecs(readManifest(path.join(t.dir, 'package.json')), target, changedKeys),
|
|
150
|
+
}));
|
|
151
|
+
if (work.every((item) => item.specs.length === 0)) {
|
|
152
|
+
return ctx.succeed(Object.assign({ source: resolution.source, current, target, deltas }, scope), ctx.ui.result({
|
|
94
153
|
title: 'Nothing to install',
|
|
95
154
|
tone: 'warn',
|
|
96
155
|
notes: [
|
|
@@ -99,18 +158,27 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
99
158
|
],
|
|
100
159
|
}));
|
|
101
160
|
}
|
|
102
|
-
const snapshot =
|
|
161
|
+
const snapshot = snapshotFiles([
|
|
162
|
+
...plan.targets.map((t) => path.join(t.dir, 'package.json')),
|
|
163
|
+
...lockfileNames(pm).map((name) => path.join(pm.root, name)),
|
|
164
|
+
path.join(plan.baseDir, WORKSPACE_FILE),
|
|
165
|
+
path.join(pm.root, WORKSPACE_FILE),
|
|
166
|
+
]);
|
|
167
|
+
const hadLockfile = lockfileNames(pm).some((name) => snapshot.existed(path.join(pm.root, name)));
|
|
168
|
+
const runInstall = (_d = deps.runInstall) !== null && _d !== void 0 ? _d : defaultRunInstall;
|
|
169
|
+
const rollback = (failure) => failAndRollback(ctx, snapshot, pm, hadLockfile, runInstall, failure);
|
|
103
170
|
let answeredBuildScripts = [];
|
|
104
171
|
let buildScriptsSkipped;
|
|
105
172
|
try {
|
|
106
|
-
const runInstall = (_d = deps.runInstall) !== null && _d !== void 0 ? _d : defaultRunInstall;
|
|
107
173
|
// pnpm 10+ refuses dependency build scripts nobody has answered, and a
|
|
108
174
|
// new toolchain release can grow that list (0.4.0 added @swc/core and
|
|
109
175
|
// @parcel/watcher via next-intl). A project scaffolded under an older
|
|
110
176
|
// CLI has no line for them, so answer the closure BEFORE `pnpm add`, or
|
|
111
|
-
// the install writes placeholders and exits ERR_PNPM_IGNORED_BUILDS.
|
|
177
|
+
// the install writes placeholders and exits ERR_PNPM_IGNORED_BUILDS. A
|
|
178
|
+
// package inside someone else's workspace is told which file to edit
|
|
179
|
+
// rather than having that file edited for it.
|
|
112
180
|
const buildScripts = pm.name === 'pnpm'
|
|
113
|
-
? ensureBuildScriptsAnswered(
|
|
181
|
+
? ensureBuildScriptsAnswered(plan.baseDir)
|
|
114
182
|
: { changed: [] };
|
|
115
183
|
answeredBuildScripts = buildScripts.changed;
|
|
116
184
|
if (buildScripts.skipped) {
|
|
@@ -120,92 +188,147 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
120
188
|
if (answeredBuildScripts.length > 0) {
|
|
121
189
|
ctx.log(ctx.ui.dim(`Answered dependency build scripts in pnpm-workspace.yaml: ${answeredBuildScripts.join(', ')}.`));
|
|
122
190
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
:
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
const atWorkspaceRoot = isMultiPackageWorkspaceRoot(pm.root);
|
|
192
|
+
for (const { target: t, specs } of work) {
|
|
193
|
+
for (const group of [
|
|
194
|
+
specs.filter((s) => !s.dev),
|
|
195
|
+
specs.filter((s) => s.dev),
|
|
196
|
+
]) {
|
|
197
|
+
if (group.length === 0)
|
|
198
|
+
continue;
|
|
199
|
+
const argv = installExactArgs(pm, group.map((s) => `${s.name}@${s.version}`), group[0].dev, atWorkspaceRoot && t.dir === pm.root);
|
|
200
|
+
ctx.log(ctx.ui.dim(`Running "${pm.bin} ${argv.join(' ')}" in ${displayPath(t.dir, process.cwd())}…`));
|
|
201
|
+
const result = await runInstall(argv, pm.bin, t.dir);
|
|
202
|
+
if (result.code !== 0) {
|
|
203
|
+
return await rollback({
|
|
204
|
+
step: 'install',
|
|
205
|
+
exitCode: EXIT_CODES.PRECONDITION_FAILED,
|
|
206
|
+
message: isIgnoredBuildsFailure(result.output)
|
|
207
|
+
? `"${pm.bin} ${argv.join(' ')}" exited with code ${result.code}: ${ignoredBuildsHint(findIgnoredBuilds(result.output))}`
|
|
208
|
+
: `"${pm.bin} ${argv.join(' ')}" exited with code ${result.code}.`,
|
|
209
|
+
output: result.output,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
141
212
|
}
|
|
142
213
|
}
|
|
143
214
|
if (!args.noVerify) {
|
|
144
215
|
const runValidateStep = (_e = deps.runValidateStep) !== null && _e !== void 0 ? _e : defaultRunValidateStep;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
216
|
+
for (const t of plan.targets.filter((item) => item.validate)) {
|
|
217
|
+
const validateResult = await runValidateStep(t.dir);
|
|
218
|
+
if (!validateResult.ok) {
|
|
219
|
+
return await rollback({
|
|
220
|
+
step: 'validate',
|
|
221
|
+
exitCode: EXIT_CODES.VALIDATION,
|
|
222
|
+
message: 'The upgraded toolchain fails "ekanos validate" against ' +
|
|
223
|
+
(plan.repo
|
|
224
|
+
? `${t.relDir}'s integration definition.`
|
|
225
|
+
: "this project's own integration definition."),
|
|
226
|
+
output: JSON.stringify((_f = validateResult.findings) !== null && _f !== void 0 ? _f : [], null, 2),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
154
229
|
}
|
|
230
|
+
// A single project always runs its test script. In a repo, each
|
|
231
|
+
// package that declares one runs it; the root holds no tests.
|
|
232
|
+
const testTargets = plan.repo
|
|
233
|
+
? plan.targets.filter((t) => t.relDir !== '' && hasTestScript(t.dir))
|
|
234
|
+
: plan.targets;
|
|
155
235
|
const runTestStep = (_g = deps.runTestStep) !== null && _g !== void 0 ? _g : defaultRunTestStep;
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
for (const t of testTargets) {
|
|
237
|
+
const testResult = await runTestStep(t.dir, pm);
|
|
238
|
+
if (testResult.code !== 0) {
|
|
239
|
+
return await rollback({
|
|
240
|
+
step: 'test',
|
|
241
|
+
exitCode: testResult.code,
|
|
242
|
+
message: `${plan.repo ? `${t.relDir}: the` : "The project's"} test script exited with code ${testResult.code} on the upgraded toolchain.`,
|
|
243
|
+
output: testResult.output,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
164
246
|
}
|
|
165
247
|
}
|
|
166
248
|
}
|
|
167
249
|
finally {
|
|
168
250
|
snapshot.dispose();
|
|
169
251
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
252
|
+
const regenerated = {
|
|
253
|
+
updated: [],
|
|
254
|
+
unchanged: [],
|
|
255
|
+
needsAttention: [],
|
|
256
|
+
skipped: [],
|
|
257
|
+
};
|
|
258
|
+
const scaffolded = {
|
|
259
|
+
created: [],
|
|
260
|
+
needsAttention: [],
|
|
261
|
+
};
|
|
262
|
+
const under = (relDir, file) => relDir === '' ? file : `${relDir}/${file}`;
|
|
263
|
+
for (const t of plan.targets) {
|
|
264
|
+
if (t.slug === null)
|
|
265
|
+
continue;
|
|
266
|
+
const newTemplatesDir = cliTemplatesDir(t.dir, pm.root);
|
|
267
|
+
const report = regenerateAgentContextFiles({
|
|
268
|
+
projectDir: t.dir,
|
|
269
|
+
slug: t.slug,
|
|
270
|
+
oldTemplates: (_h = oldAgentTemplates.get(t.dir)) !== null && _h !== void 0 ? _h : new Map(),
|
|
271
|
+
newTemplatesDir,
|
|
272
|
+
});
|
|
273
|
+
regenerated.updated.push(...report.updated.map((f) => under(t.relDir, f)));
|
|
274
|
+
regenerated.unchanged.push(...report.unchanged.map((f) => under(t.relDir, f)));
|
|
275
|
+
regenerated.needsAttention.push(...report.needsAttention.map((f) => under(t.relDir, f)));
|
|
276
|
+
regenerated.skipped.push(...report.skipped.map((s) => (Object.assign(Object.assign({}, s), { file: under(t.relDir, s.file) }))));
|
|
277
|
+
// The regenerated AGENTS.md and skills may now REFERENCE artifacts this
|
|
278
|
+
// project predates (docs/api-notes.md, .mcp.json, bundled skill trees) —
|
|
279
|
+
// instructions pointing at files that don't exist are exactly the
|
|
280
|
+
// stale-docs failure the docs-grounding feature closes, so create what's
|
|
281
|
+
// missing. Create-if-missing ONLY: these files are partner/agent-authored
|
|
282
|
+
// after the scaffold and are never regenerated.
|
|
283
|
+
const docs = scaffoldMissingDocsArtifacts({
|
|
284
|
+
projectDir: t.dir,
|
|
285
|
+
slug: t.slug,
|
|
286
|
+
newTemplatesDir,
|
|
287
|
+
});
|
|
288
|
+
scaffolded.created.push(...docs.created.map((f) => under(t.relDir, f)));
|
|
289
|
+
scaffolded.needsAttention.push(...docs.needsAttention.map((f) => under(t.relDir, f)));
|
|
290
|
+
}
|
|
188
291
|
// `specs` only ever covers DIRECT dependencies (see `buildInstallSpecs`) —
|
|
189
292
|
// a changed key with no spec is transitive and was never actually
|
|
190
293
|
// installed here, so the report (and the human summary) must say so
|
|
191
294
|
// explicitly rather than implying every changed key was upgraded. This
|
|
192
|
-
// matters most for
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
295
|
+
// matters most for a globally-installed `ekanos` with no local
|
|
296
|
+
// `@ekanos/cli` devDependency, where `cli` changes in `deltas` but the
|
|
297
|
+
// running global binary itself is untouched by this command.
|
|
298
|
+
const installedKeys = [
|
|
299
|
+
...new Set(work.flatMap((item) => item.specs.map((spec) => spec.key))),
|
|
300
|
+
];
|
|
197
301
|
const skippedKeys = changedKeys.filter((key) => !installedKeys.includes(key));
|
|
198
|
-
return ctx.succeed({
|
|
199
|
-
source: resolution.source,
|
|
200
|
-
current,
|
|
302
|
+
return ctx.succeed(Object.assign(Object.assign({ source: resolution.source, current,
|
|
201
303
|
target,
|
|
202
|
-
deltas,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
304
|
+
deltas }, scope), { installed: installedKeys, skippedTransitive: skippedKeys, buildScripts: Object.assign({ answered: answeredBuildScripts }, (buildScriptsSkipped ? { skipped: buildScriptsSkipped } : {})), regenerated,
|
|
305
|
+
scaffolded }), successSummary(ctx.ui, installedKeys, skippedKeys, deltas, regenerated, scaffolded));
|
|
306
|
+
}
|
|
307
|
+
function hasTestScript(dir) {
|
|
308
|
+
const scripts = readManifest(path.join(dir, 'package.json')).scripts;
|
|
309
|
+
return typeof scripts === 'object' && scripts !== null && 'test' in scripts;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The nearest `node_modules/@ekanos/cli/templates` from `dir` up to `top`
|
|
313
|
+
* (the workspace root): pnpm links it into each package, npm and yarn hoist
|
|
314
|
+
* it to the root. Falls back to the package's own path, where a missing
|
|
315
|
+
* directory just means "cannot verify" to the regeneration below.
|
|
316
|
+
*/
|
|
317
|
+
function cliTemplatesDir(dir, top) {
|
|
318
|
+
const own = path.join(dir, 'node_modules', '@ekanos', 'cli', 'templates');
|
|
319
|
+
let current = path.resolve(dir);
|
|
320
|
+
const stop = path.resolve(top);
|
|
321
|
+
for (;;) {
|
|
322
|
+
const candidate = path.join(current, 'node_modules', '@ekanos', 'cli', 'templates');
|
|
323
|
+
if (fs.existsSync(candidate))
|
|
324
|
+
return candidate;
|
|
325
|
+
if (current === stop)
|
|
326
|
+
return own;
|
|
327
|
+
const parent = path.dirname(current);
|
|
328
|
+
if (parent === current)
|
|
329
|
+
return own;
|
|
330
|
+
current = parent;
|
|
331
|
+
}
|
|
209
332
|
}
|
|
210
333
|
/**
|
|
211
334
|
* Best-effort host/session resolution — NEVER throws. `upgrade` must work for
|
|
@@ -225,11 +348,24 @@ function resolveSessionQuietly(ctx, args) {
|
|
|
225
348
|
return null;
|
|
226
349
|
}
|
|
227
350
|
}
|
|
228
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The installed toolchain, read from the nearest `node_modules` holding each
|
|
353
|
+
* package between `projectDir` and `top` (the workspace root): npm and yarn
|
|
354
|
+
* hoist a workspace package's dependencies to the root.
|
|
355
|
+
*/
|
|
356
|
+
function readCurrentToolchain(projectDir, top) {
|
|
229
357
|
var _a;
|
|
230
358
|
const result = {};
|
|
359
|
+
const dirs = [];
|
|
360
|
+
for (let dir = path.resolve(projectDir);; dir = path.dirname(dir)) {
|
|
361
|
+
dirs.push(dir);
|
|
362
|
+
if (dir === path.resolve(top) || path.dirname(dir) === dir)
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
231
365
|
for (const [key, pkg] of Object.entries(TOOLCHAIN_PACKAGES)) {
|
|
232
|
-
const installed =
|
|
366
|
+
const installed = dirs
|
|
367
|
+
.map((dir) => readInstalledPackageManifest(dir, pkg))
|
|
368
|
+
.find((manifest) => manifest !== null);
|
|
233
369
|
result[key] = (_a = installed === null || installed === void 0 ? void 0 : installed.version) !== null && _a !== void 0 ? _a : (key === 'cli' ? cliVersion() : null);
|
|
234
370
|
}
|
|
235
371
|
return result;
|
|
@@ -289,45 +425,31 @@ function readManifest(packageJsonPath) {
|
|
|
289
425
|
return parsed;
|
|
290
426
|
}
|
|
291
427
|
/**
|
|
292
|
-
* Copy
|
|
293
|
-
* installs
|
|
294
|
-
*
|
|
295
|
-
*
|
|
428
|
+
* Copy every file in `files` that exists to a temp directory before anything
|
|
429
|
+
* installs: each target's `package.json`, the lockfile and
|
|
430
|
+
* `pnpm-workspace.yaml` at the package manager's root (the WORKSPACE root
|
|
431
|
+
* for a workspace package — that is where an install writes them).
|
|
432
|
+
* `restore()` copies them back and removes any of them the failed run
|
|
433
|
+
* created; `dispose()` cleans up the temp copies once the run is over,
|
|
434
|
+
* success or failure.
|
|
296
435
|
*/
|
|
297
|
-
function
|
|
436
|
+
function snapshotFiles(files) {
|
|
298
437
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-upgrade-'));
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
: path.join(projectDir, lockfileName(pm));
|
|
307
|
-
const lockfileExisted = lockName !== undefined;
|
|
308
|
-
const workspacePath = path.join(projectDir, WORKSPACE_FILE);
|
|
309
|
-
const workspaceExisted = fs.existsSync(workspacePath);
|
|
310
|
-
fs.copyFileSync(packageJsonPath, path.join(tmpDir, 'package.json'));
|
|
311
|
-
if (workspaceExisted) {
|
|
312
|
-
fs.copyFileSync(workspacePath, path.join(tmpDir, 'workspace.snapshot'));
|
|
313
|
-
}
|
|
314
|
-
if (lockfileExisted) {
|
|
315
|
-
fs.copyFileSync(lockfilePath, path.join(tmpDir, 'lockfile.snapshot'));
|
|
316
|
-
}
|
|
438
|
+
const entries = [...new Set(files.map((file) => path.resolve(file)))].map((file, index) => {
|
|
439
|
+
const copy = path.join(tmpDir, String(index));
|
|
440
|
+
const existed = fs.existsSync(file);
|
|
441
|
+
if (existed)
|
|
442
|
+
fs.copyFileSync(file, copy);
|
|
443
|
+
return { file, copy, existed };
|
|
444
|
+
});
|
|
317
445
|
return {
|
|
446
|
+
existed: (file) => { var _a; var _b; return (_b = (_a = entries.find((entry) => entry.file === path.resolve(file))) === null || _a === void 0 ? void 0 : _a.existed) !== null && _b !== void 0 ? _b : false; },
|
|
318
447
|
restore: () => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
fs.rmSync(workspacePath);
|
|
325
|
-
}
|
|
326
|
-
if (lockfileExisted) {
|
|
327
|
-
fs.copyFileSync(path.join(tmpDir, 'lockfile.snapshot'), lockfilePath);
|
|
328
|
-
}
|
|
329
|
-
else if (fs.existsSync(lockfilePath)) {
|
|
330
|
-
fs.rmSync(lockfilePath);
|
|
448
|
+
for (const { file, copy, existed } of entries) {
|
|
449
|
+
if (existed)
|
|
450
|
+
fs.copyFileSync(copy, file);
|
|
451
|
+
else if (fs.existsSync(file))
|
|
452
|
+
fs.rmSync(file);
|
|
331
453
|
}
|
|
332
454
|
},
|
|
333
455
|
dispose: () => {
|
|
@@ -337,35 +459,55 @@ function snapshotProject(projectDir, pm) {
|
|
|
337
459
|
}
|
|
338
460
|
/**
|
|
339
461
|
* The one rollback path every failure funnels through: restore the snapshot,
|
|
340
|
-
* reinstall
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
462
|
+
* then reinstall with the SAME package manager, frozen to the restored
|
|
463
|
+
* lockfile, at its root — so `node_modules` matches the restored
|
|
464
|
+
* `package.json`/lockfile again. npm run inside a pnpm workspace is what
|
|
465
|
+
* broke a project once; it is never used for a project another manager owns.
|
|
466
|
+
*
|
|
467
|
+
* The report says exactly which step failed and what it said, and states the
|
|
468
|
+
* project's real state: restored when the reinstall succeeded, and when it
|
|
469
|
+
* did not, that `node_modules` is NOT consistent, with the command to repair
|
|
470
|
+
* it by hand. Never a success claim the reinstall did not earn.
|
|
345
471
|
*/
|
|
346
|
-
async function failAndRollback(ctx, snapshot, pm,
|
|
472
|
+
async function failAndRollback(ctx, snapshot, pm, hadLockfile, runInstall, failure) {
|
|
347
473
|
ctx.warn(`ekanos: the "${failure.step}" step failed — rolling back package.json ` +
|
|
348
|
-
|
|
474
|
+
`and the lockfile, then reinstalling with ${pm.name} to restore node_modules…`);
|
|
349
475
|
snapshot.restore();
|
|
350
476
|
// The SAME injectable installer seam used for the forward install — a test
|
|
351
477
|
// that mocks it never shells out for the rollback path either.
|
|
352
|
-
const
|
|
478
|
+
const reinstallArgs = frozenInstallArgs(pm, hadLockfile);
|
|
479
|
+
const reinstall = await runInstall(reinstallArgs, pm.bin, pm.root);
|
|
480
|
+
const reinstallCommand = `${pm.bin} ${reinstallArgs.join(' ')}`;
|
|
481
|
+
const manual = `cd ${pm.root} && ${reinstallCommand}`;
|
|
482
|
+
const restored = reinstall.code === 0;
|
|
353
483
|
return ctx.failWith({
|
|
354
484
|
code: `upgrade_${failure.step}_failed`,
|
|
355
|
-
message:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'
|
|
485
|
+
message: restored
|
|
486
|
+
? failure.message
|
|
487
|
+
: `${failure.message} The rollback reinstall ("${reinstallCommand}") ALSO failed with code ${reinstall.code}.`,
|
|
488
|
+
hint: restored
|
|
489
|
+
? 'The project HAS BEEN RESTORED to its pre-upgrade state (package.json, ' +
|
|
490
|
+
'lockfile and node_modules all rolled back). Read the output in ' +
|
|
491
|
+
'data.output, fix the underlying issue, and re-run "ekanos upgrade".'
|
|
492
|
+
: 'package.json and the lockfile were restored, but node_modules was NOT ' +
|
|
493
|
+
'reinstalled and does not match them. Repair it by hand before ' +
|
|
494
|
+
`anything else: ${manual}. Then read data.output and data.rollback, ` +
|
|
495
|
+
'fix the underlying issue, and re-run "ekanos upgrade".',
|
|
363
496
|
exitCode: failure.exitCode,
|
|
364
|
-
data: {
|
|
497
|
+
data: {
|
|
498
|
+
step: failure.step,
|
|
499
|
+
output: failure.output,
|
|
500
|
+
rollback: Object.assign({ filesRestored: true, nodeModulesRestored: restored, command: reinstallCommand, cwd: pm.root, exitCode: reinstall.code }, (restored ? {} : { output: reinstall.output, manual })),
|
|
501
|
+
},
|
|
365
502
|
});
|
|
366
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Spawn the package manager. A spawn failure (the binary is missing) resolves
|
|
506
|
+
* as exit 127, the shell's "command not found", rather than rejecting: the
|
|
507
|
+
* caller must still roll back, and a rejection would skip straight past it.
|
|
508
|
+
*/
|
|
367
509
|
async function defaultRunInstall(argv, bin, cwd) {
|
|
368
|
-
return new Promise((resolve
|
|
510
|
+
return new Promise((resolve) => {
|
|
369
511
|
var _a, _b;
|
|
370
512
|
const child = spawn(bin, argv, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
371
513
|
let output = '';
|
|
@@ -375,7 +517,9 @@ async function defaultRunInstall(argv, bin, cwd) {
|
|
|
375
517
|
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (chunk) => {
|
|
376
518
|
output += chunk.toString();
|
|
377
519
|
});
|
|
378
|
-
child.on('error',
|
|
520
|
+
child.on('error', (error) => {
|
|
521
|
+
resolve({ code: 127, output: `${output}${error.message}` });
|
|
522
|
+
});
|
|
379
523
|
child.on('close', (code) => resolve({ code: code !== null && code !== void 0 ? code : 1, output }));
|
|
380
524
|
});
|
|
381
525
|
}
|
|
@@ -410,8 +554,7 @@ async function defaultRunTestStep(projectDir, pm) {
|
|
|
410
554
|
* just means every file below is treated conservatively — see
|
|
411
555
|
* `regenerateAgentContextFiles`.
|
|
412
556
|
*/
|
|
413
|
-
function captureAgentTemplates(
|
|
414
|
-
const templatesDir = path.join(projectDir, 'node_modules', '@ekanos', 'cli', 'templates');
|
|
557
|
+
function captureAgentTemplates(templatesDir) {
|
|
415
558
|
const captured = new Map();
|
|
416
559
|
for (const { template } of AGENT_CONTEXT_FILES) {
|
|
417
560
|
const templatePath = path.join(templatesDir, template);
|