create-cmp-cli 0.12.0 → 0.14.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/bin/create-cmp.mjs +3 -0
- package/package.json +6 -2
- package/packages/harness/package.json +38 -0
- package/packages/harness/src/approve.mjs +247 -0
- package/packages/harness/src/arch-doc.mjs +69 -0
- package/packages/harness/src/comment.mjs +76 -0
- package/packages/harness/src/lib/a11y.mjs +113 -0
- package/packages/harness/src/lib/affected-tests.mjs +147 -0
- package/packages/harness/src/lib/approvals.mjs +1403 -0
- package/packages/harness/src/lib/arch-doc.mjs +451 -0
- package/packages/harness/src/lib/audit-cadence.mjs +290 -0
- package/packages/harness/src/lib/comments.mjs +252 -0
- package/packages/harness/src/lib/component-stories.mjs +183 -0
- package/packages/harness/src/lib/determinism.mjs +179 -0
- package/packages/harness/src/lib/device-lease.mjs +249 -0
- package/packages/harness/src/lib/evidence-badge.mjs +158 -0
- package/packages/harness/src/lib/evidence-level.mjs +117 -0
- package/packages/harness/src/lib/feature-brief.mjs +324 -0
- package/packages/harness/src/lib/flight-recorder.mjs +332 -0
- package/packages/harness/src/lib/harness-lock.mjs +147 -0
- package/packages/harness/src/lib/harness-region.mjs +159 -0
- package/packages/harness/src/lib/inputs-hash.mjs +194 -0
- package/packages/harness/src/lib/reachability.mjs +211 -0
- package/packages/harness/src/lib/receipt-validate.mjs +234 -0
- package/packages/harness/src/lib/render.mjs +254 -0
- package/packages/harness/src/lib/spec-coverage.mjs +131 -0
- package/packages/harness/src/lib/step-cache.mjs +221 -0
- package/packages/harness/src/lib/token-drift.mjs +94 -0
- package/packages/harness/src/lib/tree.mjs +108 -0
- package/packages/harness/src/preview-gallery.mjs +122 -0
- package/packages/harness/src/receipt-check.mjs +96 -0
- package/packages/harness/src/record-audit.mjs +83 -0
- package/packages/harness/src/refusal-demo.mjs +498 -0
- package/packages/harness/src/retrospective.mjs +51 -0
- package/packages/harness/src/scaffold-feature.mjs +723 -0
- package/packages/harness/src/setup-hooks.mjs +33 -0
- package/packages/harness/src/verify.mjs +1709 -0
- package/packages/harness/src/walkthrough.mjs +499 -0
- package/packages/harness/src/watch.mjs +622 -0
- package/packages/receipts/package.json +36 -0
- package/packages/receipts/src/index.mjs +16 -0
- package/packages/receipts/src/inputs-hash.mjs +194 -0
- package/packages/receipts/src/receipt-validate.mjs +234 -0
- package/src/commands/upgrade.mjs +383 -0
- package/src/lib/harness-upgrade.mjs +521 -0
- package/src/scaffold.mjs +60 -1
- package/template/AGENTS.md +5 -0
- package/template/CLAUDE.md +34 -1
- package/template/README.md +4 -0
- package/template/gitignore +8 -0
- package/template/qa/lib/audit-cadence.mjs +290 -0
- package/template/qa/lib/determinism.mjs +179 -0
- package/template/qa/lib/evidence-badge.mjs +158 -0
- package/template/qa/lib/flight-recorder.mjs +332 -0
- package/template/qa/lib/harness-lock.mjs +147 -0
- package/template/qa/lib/harness-region.mjs +159 -0
- package/template/qa/lib/inputs-hash.mjs +17 -2
- package/template/qa/lib/receipt-validate.mjs +1 -1
- package/template/qa/preview-gallery.mjs +17 -2
- package/template/qa/record-audit.mjs +83 -0
- package/template/qa/retrospective.mjs +51 -0
- package/template/qa/verify.mjs +400 -10
- package/template/qa/watch.mjs +2 -2
package/src/commands/upgrade.mjs
CHANGED
|
@@ -17,15 +17,87 @@
|
|
|
17
17
|
// `<kotlin>-…`.
|
|
18
18
|
// - Works on ANY project with a libs.versions.toml — template markers only
|
|
19
19
|
// soften/strengthen messaging, never refuse.
|
|
20
|
+
//
|
|
21
|
+
// Second mode — `create-cmp upgrade --harness`:
|
|
22
|
+
//
|
|
23
|
+
// create-cmp upgrade --harness [--target-dir .] [--base-dir <path>] [--dry-run] [--yes]
|
|
24
|
+
//
|
|
25
|
+
// Refreshes the ENGINE-OWNED files of a stamped app via a three-way merge
|
|
26
|
+
// (base = old engine's stamp, new = current engine's stamp, theirs = the
|
|
27
|
+
// app's tree — both stamps use the app's own recorded config from
|
|
28
|
+
// create-cmp.json so every diff is pure engine change). Conflicts never
|
|
29
|
+
// clobber: the app's file stays put and a `.cmp-new` sidecar carries the
|
|
30
|
+
// new engine content. Decision logic lives in src/lib/harness-upgrade.mjs;
|
|
31
|
+
// this file does the filesystem/CLI orchestration (npm pack of the base
|
|
32
|
+
// version, temp-dir stamps, consent, backups, report, exit code).
|
|
20
33
|
|
|
21
34
|
import fs from "node:fs";
|
|
35
|
+
import os from "node:os";
|
|
22
36
|
import path from "node:path";
|
|
37
|
+
import { fileURLToPath } from "node:url";
|
|
38
|
+
import { spawnSync } from "node:child_process";
|
|
23
39
|
|
|
24
40
|
import { flagBool } from "../lib/args.mjs";
|
|
25
41
|
import { colors, ok, warn, fail, step } from "../lib/log.mjs";
|
|
26
42
|
import { consent } from "../bootstrap/exec.mjs";
|
|
27
43
|
import { loadRegistry, latestSet, getSet } from "../lib/registry.mjs";
|
|
28
44
|
import { planUpgrade, BACKUP_SUFFIX } from "../lib/upgrade.mjs";
|
|
45
|
+
import { writeHarnessLock, checkHarnessIntegrity, describeIntegrity } from "../../packages/harness/src/lib/harness-lock.mjs";
|
|
46
|
+
import { LOCAL_PATCH_PATH } from "../lib/harness-upgrade.mjs";
|
|
47
|
+
import {
|
|
48
|
+
planHarnessUpgrade,
|
|
49
|
+
applyHarnessPlan,
|
|
50
|
+
configFromSpecRecord,
|
|
51
|
+
SIDECAR_SUFFIX,
|
|
52
|
+
} from "../lib/harness-upgrade.mjs";
|
|
53
|
+
|
|
54
|
+
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The harness version this engine ships. Read from the package that owns the
|
|
58
|
+
* lane, NOT the engine's own package.json — they version independently, which
|
|
59
|
+
* is the point: the lane changes far more often than the template's app shape,
|
|
60
|
+
* and fusing them forced an app-shape merge every time a lane fix shipped.
|
|
61
|
+
* @returns {string|null} semver, or null when unreadable
|
|
62
|
+
*/
|
|
63
|
+
function shippedHarnessVersion() {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(
|
|
66
|
+
fs.readFileSync(path.join(REPO_ROOT, "packages/harness/package.json"), "utf8")
|
|
67
|
+
).version;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Advance create-cmp.json's engineVersion to what this engine just applied.
|
|
75
|
+
*
|
|
76
|
+
* Nothing used to do this, and the omission compounded: the next
|
|
77
|
+
* `upgrade --harness` re-fetched the STALE version as its merge base and
|
|
78
|
+
* re-presented every conflict already resolved by hand. Fuelled still read
|
|
79
|
+
* 0.9.0 after being upgraded to 0.13.0.
|
|
80
|
+
*
|
|
81
|
+
* Only called when the sweep landed COMPLETELY. With conflicts outstanding the
|
|
82
|
+
* app is genuinely part-way between two engine versions, and claiming the new
|
|
83
|
+
* one would be the same lie in the other direction.
|
|
84
|
+
* @param {string} projectDir
|
|
85
|
+
* @param {string} version
|
|
86
|
+
* @returns {boolean} whether the record was updated
|
|
87
|
+
*/
|
|
88
|
+
function writeBackEngineVersion(projectDir, version) {
|
|
89
|
+
const specPath = path.join(projectDir, "create-cmp.json");
|
|
90
|
+
try {
|
|
91
|
+
const record = JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
92
|
+
if (record.engineVersion === version) return false;
|
|
93
|
+
record.engineVersion = version;
|
|
94
|
+
record.upgradedAt = new Date().toISOString();
|
|
95
|
+
fs.writeFileSync(specPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false; // best-effort — never fail an applied upgrade over metadata
|
|
99
|
+
}
|
|
100
|
+
}
|
|
29
101
|
|
|
30
102
|
function readIfExists(p) {
|
|
31
103
|
try {
|
|
@@ -48,11 +120,322 @@ function printDiffTable(changes) {
|
|
|
48
120
|
}
|
|
49
121
|
}
|
|
50
122
|
|
|
123
|
+
// --- harness mode helpers ----------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** Current engine version, from this checkout's package.json. */
|
|
126
|
+
function currentEngineVersion() {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version;
|
|
129
|
+
} catch {
|
|
130
|
+
return "unknown";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a --base-dir argument to a template root (the dir holding
|
|
136
|
+
* manifest.json). Accepts the template root itself, a repo/package checkout
|
|
137
|
+
* containing `template/`, or an extracted npm tarball (`package/template/`).
|
|
138
|
+
* Throws (never exits) so the caller's temp-dir cleanup always runs.
|
|
139
|
+
* @param {string} p
|
|
140
|
+
* @returns {string} absolute template root
|
|
141
|
+
*/
|
|
142
|
+
function resolveBaseTemplateDir(p) {
|
|
143
|
+
const abs = path.resolve(p);
|
|
144
|
+
for (const candidate of [abs, path.join(abs, "template"), path.join(abs, "package", "template")]) {
|
|
145
|
+
if (fs.existsSync(path.join(candidate, "manifest.json"))) return candidate;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
`--base-dir ${abs} does not look like a create-cmp template (no manifest.json at ` +
|
|
149
|
+
`<dir>, <dir>/template, or <dir>/package/template).`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fetch the OLD engine's template by version: `npm pack create-cmp-cli@<v>`
|
|
155
|
+
* into a temp dir, then extract `package/template/` from the tarball.
|
|
156
|
+
* Fails loudly (throws — never exits, so the caller's temp-dir cleanup always
|
|
157
|
+
* runs), naming the exact command that failed and the --base-dir escape hatch
|
|
158
|
+
* (offline / local checkout / testing).
|
|
159
|
+
* @param {string} engineVersion version recorded in create-cmp.json
|
|
160
|
+
* @param {string} tmpRoot session temp dir (cleaned up by the caller)
|
|
161
|
+
* @returns {string} absolute path of the extracted template root
|
|
162
|
+
*/
|
|
163
|
+
function fetchBaseTemplate(engineVersion, tmpRoot) {
|
|
164
|
+
if (!engineVersion || engineVersion === "unknown" || !/^[0-9A-Za-z.+-]+$/.test(engineVersion)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`create-cmp.json does not record a usable engineVersion (got ${JSON.stringify(engineVersion)}). ` +
|
|
167
|
+
`Pass --base-dir <path> pointing at the template this app was stamped from.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const packDir = path.join(tmpRoot, "pack");
|
|
171
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
172
|
+
const packCmd = `npm pack create-cmp-cli@${engineVersion}`;
|
|
173
|
+
const r = spawnSync("npm", ["pack", `create-cmp-cli@${engineVersion}`], {
|
|
174
|
+
cwd: packDir,
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 120000,
|
|
177
|
+
});
|
|
178
|
+
if (r.error || r.status !== 0) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Could not fetch the base engine template: \`${packCmd}\` failed` +
|
|
181
|
+
(r.stderr ? ` — ${r.stderr.trim().split("\n").pop()}` : "") +
|
|
182
|
+
`.\n Offline or unpublished version? Pass --base-dir <path> to an already-extracted template.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// npm pack prints the tarball filename as the last stdout line.
|
|
186
|
+
const tgz = (r.stdout || "").trim().split("\n").pop().trim();
|
|
187
|
+
const tgzPath = path.join(packDir, tgz);
|
|
188
|
+
const tarCmd = `tar -xzf ${tgzPath} -C ${packDir}`;
|
|
189
|
+
const rt = spawnSync("tar", ["-xzf", tgzPath, "-C", packDir], { timeout: 120000 });
|
|
190
|
+
if (rt.error || rt.status !== 0 || !fs.existsSync(path.join(packDir, "package", "template", "manifest.json"))) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Could not extract the base engine template: \`${tarCmd}\` failed or the tarball ` +
|
|
193
|
+
`carries no package/template/. Pass --base-dir <path> to an already-extracted template.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return path.join(packDir, "package", "template");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Print the grouped harness report; returns whether anything is actionable. */
|
|
200
|
+
function printHarnessReport(plan) {
|
|
201
|
+
const c = plan.counts;
|
|
202
|
+
const list = (bucket) => plan.entries.filter((e) => e.bucket === bucket).map((e) => e.relPath);
|
|
203
|
+
|
|
204
|
+
process.stdout.write(
|
|
205
|
+
`\n${colors.bold("Engine-owned files")} — ` +
|
|
206
|
+
`${colors.dim(`unchanged ${c.unchanged} · already current ${c.current} · excluded state/secrets ${c.excluded}`)}\n`
|
|
207
|
+
);
|
|
208
|
+
const groups = [
|
|
209
|
+
// The machine-owned lane first: it is the bulk of the sweep and the part
|
|
210
|
+
// that needs no human judgement at all.
|
|
211
|
+
["region-clean", "lane refreshed (machine-owned, untouched since install)", ok],
|
|
212
|
+
["region-absorbed", "lane refreshed (local edit already carried by the new engine)", ok],
|
|
213
|
+
["region-restored", "lane restored (files the app had deleted)", ok],
|
|
214
|
+
["region-removed", "lane files retired by the engine", ok],
|
|
215
|
+
[
|
|
216
|
+
"region-patched",
|
|
217
|
+
`lane refreshed, LOCAL FORK preserved (not re-applied — see ${LOCAL_PATCH_PATH})`,
|
|
218
|
+
warn,
|
|
219
|
+
],
|
|
220
|
+
["applied", "applied (engine changed, app never touched — will take the new content)", ok],
|
|
221
|
+
["merged", "merged (both changed different regions — both edits survive)", ok],
|
|
222
|
+
["added", "added (new engine files absent from the app)", ok],
|
|
223
|
+
["removed", "removed (engine deleted, app never touched — will be deleted)", warn],
|
|
224
|
+
["orphaned", "orphaned (engine deleted these but the app modified them — left in place)", warn],
|
|
225
|
+
["conflicted", `conflicted (NEVER clobbered — new engine content lands beside as *${SIDECAR_SUFFIX})`, fail],
|
|
226
|
+
];
|
|
227
|
+
let actionable = 0;
|
|
228
|
+
for (const [bucket, label, log] of groups) {
|
|
229
|
+
const files = list(bucket);
|
|
230
|
+
if (files.length === 0) continue;
|
|
231
|
+
// `orphaned` needs no action (the app's file is kept as-is), and neither do
|
|
232
|
+
// the clean lane buckets — they apply themselves. Only a preserved local
|
|
233
|
+
// fork earns a human's attention among the region buckets.
|
|
234
|
+
if (bucket !== "orphaned") actionable += files.length;
|
|
235
|
+
log(`${colors.bold(String(files.length))} ${label}`);
|
|
236
|
+
for (const f of files) process.stdout.write(` ${f}\n`);
|
|
237
|
+
}
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
colors.dim(
|
|
240
|
+
`\n Not considered: ${c.excluded} excluded app-state/secret files ` +
|
|
241
|
+
`(evidence, approvals, goldens, keystores, Firebase configs), and any app-authored ` +
|
|
242
|
+
`files the engine never stamped.\n`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
return actionable > 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `create-cmp upgrade --harness` — refresh engine-owned files of a stamped app.
|
|
250
|
+
* @param {Record<string,string|boolean>} flags
|
|
251
|
+
* @param {string|undefined} positional optional target dir positional
|
|
252
|
+
*/
|
|
253
|
+
async function runHarnessUpgrade(flags, positional) {
|
|
254
|
+
const targetDir =
|
|
255
|
+
(typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
|
|
256
|
+
const projectDir = path.resolve(targetDir);
|
|
257
|
+
|
|
258
|
+
const specPath = path.join(projectDir, "create-cmp.json");
|
|
259
|
+
const specRaw = readIfExists(specPath);
|
|
260
|
+
if (specRaw === null) {
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
`Error: no create-cmp.json under ${projectDir}.\n` +
|
|
263
|
+
`\`create-cmp upgrade --harness\` refreshes the engine-owned files of a create-cmp-stamped ` +
|
|
264
|
+
`project, and needs the spec-of-record the stamp wrote. Run it from the project root or ` +
|
|
265
|
+
`pass --target-dir. (For a plain version-catalog upgrade, drop --harness.)\n`
|
|
266
|
+
);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
let record;
|
|
270
|
+
try {
|
|
271
|
+
record = JSON.parse(specRaw);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
process.stderr.write(`Error: ${specPath} is not valid JSON (${e.message}).\n`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const baseVersion = record.engineVersion;
|
|
278
|
+
const currentVersion = currentEngineVersion();
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`\n${colors.bold("create-cmp upgrade --harness")} — refresh engine-owned files\n` +
|
|
281
|
+
` project: ${colors.cyan(projectDir)}\n` +
|
|
282
|
+
` stamped by engine ${colors.yellow(String(baseVersion))} ${colors.dim("→")} current engine ${colors.green(currentVersion)}\n\n`
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// NOTE: `process.exit` skips `finally`, so the body below RETURNS an exit
|
|
286
|
+
// code and the temp trees are cleaned up before the process actually exits.
|
|
287
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "create-cmp-harness-"));
|
|
288
|
+
let code = 1;
|
|
289
|
+
try {
|
|
290
|
+
code = await harnessPlanAndApply({
|
|
291
|
+
flags,
|
|
292
|
+
record,
|
|
293
|
+
projectDir,
|
|
294
|
+
targetDir,
|
|
295
|
+
tmpRoot,
|
|
296
|
+
baseVersion,
|
|
297
|
+
currentVersion,
|
|
298
|
+
});
|
|
299
|
+
} catch (e) {
|
|
300
|
+
fail(e.message);
|
|
301
|
+
code = 1;
|
|
302
|
+
} finally {
|
|
303
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
process.exit(code);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The harness mode's plan/report/apply body. Returns the process exit code
|
|
310
|
+
* (0 = clean apply or dry run, 1 = conflicts produced) and throws on
|
|
311
|
+
* environment failures — it never calls process.exit itself, so the caller's
|
|
312
|
+
* temp-dir cleanup always runs.
|
|
313
|
+
* @returns {Promise<number>}
|
|
314
|
+
*/
|
|
315
|
+
async function harnessPlanAndApply({ flags, record, projectDir, targetDir, tmpRoot, baseVersion, currentVersion }) {
|
|
316
|
+
// Base template: --base-dir wins (offline / local checkout / testing);
|
|
317
|
+
// same-version needs no fetch (the local template IS the base); otherwise
|
|
318
|
+
// npm pack the recorded version.
|
|
319
|
+
let baseTemplateDir;
|
|
320
|
+
if (typeof flags["base-dir"] === "string") {
|
|
321
|
+
baseTemplateDir = resolveBaseTemplateDir(flags["base-dir"]);
|
|
322
|
+
step(`Base template: ${colors.cyan(baseTemplateDir)} (--base-dir)`);
|
|
323
|
+
} else if (baseVersion === currentVersion) {
|
|
324
|
+
baseTemplateDir = path.join(REPO_ROOT, "template");
|
|
325
|
+
step(`App was stamped by THIS engine version — base is the local template.`);
|
|
326
|
+
} else {
|
|
327
|
+
step(`Fetching base engine ${baseVersion} via npm pack…`);
|
|
328
|
+
baseTemplateDir = fetchBaseTemplate(baseVersion, tmpRoot);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Stamp both sides with the app's OWN recorded config, so tokens resolve
|
|
332
|
+
// identically and base→new diffs are pure engine change. `verify:false`
|
|
333
|
+
// keeps this filesystem-only — no Gradle, no device.
|
|
334
|
+
const { scaffold } = await import("../scaffold.mjs");
|
|
335
|
+
const newDir = path.join(tmpRoot, "new");
|
|
336
|
+
const baseDir = path.join(tmpRoot, "base");
|
|
337
|
+
step("Stamping the CURRENT engine with the app's recorded config…");
|
|
338
|
+
await scaffold(configFromSpecRecord(record, newDir), { verify: false });
|
|
339
|
+
step(`Stamping the BASE engine (${baseVersion}) with the same config…`);
|
|
340
|
+
await scaffold(configFromSpecRecord(record, baseDir), {
|
|
341
|
+
templateDir: baseTemplateDir,
|
|
342
|
+
verify: false,
|
|
343
|
+
});
|
|
344
|
+
const plan = planHarnessUpgrade({ baseDir, newDir, projectDir });
|
|
345
|
+
|
|
346
|
+
const anythingToDo = printHarnessReport(plan);
|
|
347
|
+
if (!anythingToDo) {
|
|
348
|
+
ok("Engine-owned files are fully up to date — nothing to apply.");
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (flags["dry-run"] === true) {
|
|
353
|
+
process.stdout.write(`\n${colors.yellow("Dry run")} — nothing written. Re-run with --yes to apply.\n`);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
const approved = await consent(
|
|
357
|
+
`\nApply these changes (backups written as *${BACKUP_SUFFIX}; conflicts only get *${SIDECAR_SUFFIX} sidecars)?`,
|
|
358
|
+
{ assumeYes: flags.yes === true }
|
|
359
|
+
);
|
|
360
|
+
if (!approved) {
|
|
361
|
+
process.stdout.write(`${colors.yellow("Not applied")} — dry run only. Re-run with --yes to apply.\n`);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const actionable = plan.entries.filter(
|
|
366
|
+
(e) => e.write !== null || e.sidecar !== null || e.remove
|
|
367
|
+
);
|
|
368
|
+
const result = applyHarnessPlan(projectDir, actionable);
|
|
369
|
+
for (const f of result.written) ok(`wrote ${f} ${colors.dim(`(backup: ${f}${BACKUP_SUFFIX})`)}`);
|
|
370
|
+
for (const f of result.created) ok(`created ${f}`);
|
|
371
|
+
for (const f of result.deleted) ok(`deleted ${f} ${colors.dim(`(backup: ${f}${BACKUP_SUFFIX})`)}`);
|
|
372
|
+
for (const f of result.sidecars) warn(`conflict sidecar ${f} — resolve by hand, then delete it`);
|
|
373
|
+
|
|
374
|
+
// ── Re-lock the lane ──────────────────────────────────────────────────────
|
|
375
|
+
// The machine-owned region always lands on the new engine's content, whether
|
|
376
|
+
// or not app-shaped files conflicted — that is what the two independent
|
|
377
|
+
// version numbers buy. So the lock is rewritten on its own schedule.
|
|
378
|
+
const harnessVersion = shippedHarnessVersion();
|
|
379
|
+
if (harnessVersion) {
|
|
380
|
+
writeHarnessLock(projectDir, { version: harnessVersion });
|
|
381
|
+
ok(`lane locked at ${colors.bold(harnessVersion)} — ${describeIntegrity(checkHarnessIntegrity(projectDir))}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (result.patched.length > 0) {
|
|
385
|
+
warn(
|
|
386
|
+
`${result.patched.length} lane file(s) carried a LOCAL change that the new engine does not. ` +
|
|
387
|
+
`The lane was refreshed and your edits were preserved — NOT re-applied — in ${LOCAL_PATCH_PATH}.`
|
|
388
|
+
);
|
|
389
|
+
for (const f of result.patched) process.stdout.write(` ${f}\n`);
|
|
390
|
+
process.stdout.write(
|
|
391
|
+
colors.dim(
|
|
392
|
+
` The diff is against the lane you WERE on, so it may not apply cleanly to the new one —\n` +
|
|
393
|
+
` that is the merge this tool declined to do behind your back, not a broken patch.\n` +
|
|
394
|
+
` Attempt it with: git apply --reject ${LOCAL_PATCH_PATH} (or upstream the change)\n`
|
|
395
|
+
)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (result.backups.length > 0 || result.created.length > 0) {
|
|
400
|
+
process.stdout.write(`\n${colors.bold("To revert")}\n`);
|
|
401
|
+
for (const f of result.backups) {
|
|
402
|
+
process.stdout.write(` mv "${path.join(projectDir, f)}${BACKUP_SUFFIX}" "${path.join(projectDir, f)}"\n`);
|
|
403
|
+
}
|
|
404
|
+
for (const f of result.created) {
|
|
405
|
+
process.stdout.write(` rm "${path.join(projectDir, f)}"\n`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (result.sidecars.length > 0) {
|
|
410
|
+
fail(
|
|
411
|
+
`${result.sidecars.length} conflict(s) need a human: the app's files were left untouched; ` +
|
|
412
|
+
`each *${SIDECAR_SUFFIX} sidecar carries the new engine content.`
|
|
413
|
+
);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
// ── Advance the recorded engine version ───────────────────────────────────
|
|
417
|
+
// Only now, with nothing conflicted: the sweep landed completely, so the
|
|
418
|
+
// next upgrade's merge base is genuinely this version. Skipping this is what
|
|
419
|
+
// made repeat upgrades compound — the base stayed stale and every
|
|
420
|
+
// already-resolved conflict came back.
|
|
421
|
+
if (writeBackEngineVersion(projectDir, currentVersion)) {
|
|
422
|
+
ok(`create-cmp.json engineVersion → ${colors.bold(currentVersion)}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
process.stdout.write(
|
|
426
|
+
`\n${colors.green("Applied.")} Prove the build: ${colors.cyan(`create-cmp verify --target-dir ${targetDir}`)}\n`
|
|
427
|
+
);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
51
431
|
/**
|
|
52
432
|
* @param {Record<string,string|boolean>} flags
|
|
53
433
|
* @param {string|undefined} positional optional target dir positional
|
|
54
434
|
*/
|
|
55
435
|
export async function runUpgrade(flags, positional) {
|
|
436
|
+
if (flags.harness === true) {
|
|
437
|
+
return runHarnessUpgrade(flags, positional);
|
|
438
|
+
}
|
|
56
439
|
const targetDir =
|
|
57
440
|
(typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
|
|
58
441
|
const projectDir = path.resolve(targetDir);
|