@juangcarmona/lighthouse 0.2.1 → 0.3.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.
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Install the exact Lighthouse package artifact selected for the release and run `
|
|
|
8
8
|
|
|
9
9
|
Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an existing adoption. It does not write by default; add `--apply` only after reviewing the plan. Existing files whose content differs from the generated projection are conflicts and are never overwritten by reconciliation.
|
|
10
10
|
|
|
11
|
+
Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
|
|
12
|
+
|
|
11
13
|
The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes and `DESIGN.md`. Stack profiles own their source templates, guidance and checks.
|
|
12
14
|
|
|
13
15
|
ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration.
|
|
@@ -36,10 +38,12 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
|
|
|
36
38
|
|
|
37
39
|
## Current command scope
|
|
38
40
|
|
|
39
|
-
The command supports `init`, `authorities`, `reconcile` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
|
|
41
|
+
The command supports `init`, `authorities`, `reconcile`, `upgrade` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
|
|
40
42
|
|
|
41
43
|
Initialization installs `.husky/commit-msg`, `.husky/pre-push`, `scripts/lighthouse-guard.mjs` and `.github/workflows/lighthouse.yml` when those paths are free. Existing files remain project-owned and are reported as conflicts. Reconciliation can add missing managed paths after adoption, but it does not overwrite a path that has drifted or belongs to another tool.
|
|
42
44
|
|
|
45
|
+
The upgrade route records the ownership hashes in `.lighthouse/upgrade.json`. A missing or stale state file is an adoption gap; reconcile it first instead of allowing a full re-adoption. Upgrade plans report the source and target package and baseline, selected scopes, classifications, preserved paths, verification and recovery action.
|
|
46
|
+
|
|
43
47
|
The package does not contain the repository gallery, provider-owned integrations or reference source. Those remain with the source repository or the profile that owns them.
|
|
44
48
|
|
|
45
49
|
## Guardrail catalog
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"managed": {"anyOf": [
|
|
17
17
|
{"const": ["LIGHTHOUSE.md"]},
|
|
18
18
|
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
|
|
19
|
-
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]}
|
|
19
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]},
|
|
20
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]}
|
|
20
21
|
]},
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "object",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-upgrade.schema.json",
|
|
4
|
+
"title": "Lighthouse upgrade ownership state",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "package", "baseline", "capabilities", "projections", "lastAppliedMigration", "files"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {"type": "string"},
|
|
10
|
+
"schemaVersion": {"const": 1},
|
|
11
|
+
"package": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"required": ["name", "version"],
|
|
15
|
+
"properties": {"name": {"const": "@juangcarmona/lighthouse"}, "version": {"type": "string", "pattern": "^(?:\\d+\\.\\d+\\.\\d+|[a-f0-9]{40})$"}}
|
|
16
|
+
},
|
|
17
|
+
"baseline": {"type": "string", "minLength": 1},
|
|
18
|
+
"capabilities": {"$ref": "#/$defs/versions"},
|
|
19
|
+
"projections": {"$ref": "#/$defs/versions"},
|
|
20
|
+
"lastAppliedMigration": {"type": "string", "minLength": 1},
|
|
21
|
+
"files": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["hash", "scope"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"hash": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
29
|
+
"scope": {"enum": ["harness", "sdlc"]}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"$defs": {"versions": {"type": "object", "additionalProperties": {"type": ["string", "null"]}}}
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@arcshape/cli": "0.1.3",
|
|
24
24
|
"@prodshape/cli": "0.22.0",
|
|
25
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
25
|
+
"@juangcarmona/lighthouse-sdlc": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/lighthouse.mjs",
|
|
29
29
|
"src/guardrails.mjs",
|
|
30
30
|
"assets",
|
|
31
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
"upgrades"
|
|
32
33
|
],
|
|
33
34
|
"bin": {
|
|
34
35
|
"lighthouse": "./src/lighthouse.mjs"
|
package/src/lighthouse.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { access, chmod, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { access, chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { constants, readFileSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { createRequire } from 'node:module';
|
|
5
6
|
import { basename, dirname, resolve } from 'node:path';
|
|
6
7
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
@@ -8,10 +9,19 @@ import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
|
|
|
8
9
|
|
|
9
10
|
const root = process.cwd();
|
|
10
11
|
const packageRequire = createRequire(import.meta.url);
|
|
12
|
+
const cliPackage = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
|
+
function installedPackageVersion(name, entry) {
|
|
14
|
+
try {
|
|
15
|
+
const entryPath = packageRequire.resolve(`${name}/${entry}`);
|
|
16
|
+
return JSON.parse(readFileSync(resolve(dirname(entryPath), 'package.json'), 'utf8')).version;
|
|
17
|
+
} catch { return null; }
|
|
18
|
+
}
|
|
11
19
|
const manifestPath = resolve(root, 'lighthouse.json');
|
|
12
20
|
const projection = 'LIGHTHOUSE.md';
|
|
21
|
+
const upgradeStateRelative = '.lighthouse/upgrade.json';
|
|
22
|
+
const upgradeStatePath = resolve(root, upgradeStateRelative);
|
|
13
23
|
const layout = ['src', 'docs', 'infra', 'scripts'];
|
|
14
|
-
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: '0.1.0' };
|
|
24
|
+
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
|
|
15
25
|
const managedPaths = [
|
|
16
26
|
projection,
|
|
17
27
|
'scripts/lighthouse-guard.mjs',
|
|
@@ -19,11 +29,18 @@ const managedPaths = [
|
|
|
19
29
|
'.husky/pre-push',
|
|
20
30
|
'.github/workflows/lighthouse.yml',
|
|
21
31
|
'.lighthouse/sdlc.json',
|
|
32
|
+
upgradeStateRelative,
|
|
22
33
|
];
|
|
34
|
+
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
23
35
|
const legacyManagedPaths = [projection];
|
|
24
36
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
25
37
|
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
|
|
26
|
-
const
|
|
38
|
+
const upgradeScopes = {
|
|
39
|
+
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml'],
|
|
40
|
+
sdlc: ['.lighthouse/sdlc.json'],
|
|
41
|
+
};
|
|
42
|
+
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
43
|
+
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
27
44
|
|
|
28
45
|
const defaultRepository = () => ({
|
|
29
46
|
layout: [...layout],
|
|
@@ -198,6 +215,132 @@ function managedFiles(manifest) {
|
|
|
198
215
|
return files;
|
|
199
216
|
}
|
|
200
217
|
|
|
218
|
+
function contentHash(content) {
|
|
219
|
+
return createHash('sha256').update(content).digest('hex');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function upgradeState(manifest, files = managedFiles(manifest), previous = null) {
|
|
223
|
+
const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, {
|
|
224
|
+
hash: contentHash(content),
|
|
225
|
+
scope: Object.entries(upgradeScopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
|
|
226
|
+
}]));
|
|
227
|
+
return {
|
|
228
|
+
$schema: upgradeSchema,
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
package: { name: cliPackage.name, version: cliPackage.version },
|
|
231
|
+
baseline: manifest.baseline,
|
|
232
|
+
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
233
|
+
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
234
|
+
lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
|
|
235
|
+
files: entries,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function readUpgradeState() {
|
|
240
|
+
try { return JSON.parse(await readFile(upgradeStatePath, 'utf8')); }
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error.code === 'ENOENT') return null;
|
|
243
|
+
throw new Error(`Cannot read ${upgradeStateRelative}: ${error.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function writeUpgradeState(manifest, previous = null, files = managedFiles(manifest)) {
|
|
248
|
+
const state = upgradeState(manifest, files, previous);
|
|
249
|
+
await mkdir(dirname(upgradeStatePath), { recursive: true });
|
|
250
|
+
await writeFile(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`, { flag: 'w' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stateProblems(state) {
|
|
254
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) return [`${upgradeStateRelative} is missing; run lighthouse reconcile --apply to establish ownership evidence.`];
|
|
255
|
+
const problems = [];
|
|
256
|
+
if (state.schemaVersion !== 1) problems.push(`${upgradeStateRelative} has unsupported schemaVersion ${state.schemaVersion}.`);
|
|
257
|
+
if (!state.package || state.package.name !== cliPackage.name || typeof state.package.version !== 'string') problems.push(`${upgradeStateRelative} does not identify the installed Lighthouse package.`);
|
|
258
|
+
if (!state.files || typeof state.files !== 'object' || Array.isArray(state.files)) problems.push(`${upgradeStateRelative} does not contain file ownership evidence.`);
|
|
259
|
+
return problems;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scopesFromArgs(args) {
|
|
263
|
+
const selected = new Set();
|
|
264
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
265
|
+
if (args[index] !== '--scope' && !args[index].startsWith('--scope=')) continue;
|
|
266
|
+
const value = args[index] === '--scope' ? args[index + 1] : args[index].slice('--scope='.length);
|
|
267
|
+
if (args[index] === '--scope') index += 1;
|
|
268
|
+
if (!value) throw new Error('--scope requires harness, sdlc or all');
|
|
269
|
+
for (const scope of value.split(',')) {
|
|
270
|
+
if (scope === 'all') { selected.add('harness'); selected.add('sdlc'); }
|
|
271
|
+
else if (scope === 'harness' || scope === 'sdlc') selected.add(scope);
|
|
272
|
+
else throw new Error(`Unknown upgrade scope ${scope}; use harness, sdlc or all.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return selected.size ? selected : new Set(Object.keys(upgradeScopes));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function selectedPaths(scopes) {
|
|
279
|
+
return new Set([...scopes].flatMap((scope) => upgradeScopes[scope]));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function classifyUpgradeFile(path, current, expected, recordedHash) {
|
|
283
|
+
if (current === null) return { classification: 'additive', action: 'add' };
|
|
284
|
+
const currentHash = contentHash(current);
|
|
285
|
+
if (current === expected && currentHash === recordedHash) return { classification: 'unchanged', action: 'keep' };
|
|
286
|
+
if (current === expected && currentHash !== recordedHash) return { classification: 'adoptable', action: 'record' };
|
|
287
|
+
if (recordedHash && currentHash === recordedHash) return { classification: 'adoptable', action: 'update' };
|
|
288
|
+
if (recordedHash) return { classification: 'customized', action: 'preserve', reason: 'current content differs from the recorded projection' };
|
|
289
|
+
return { classification: 'conflicting', action: 'review', reason: 'ownership is not recorded for this path' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function planUpgrade(manifest, scopes) {
|
|
293
|
+
const state = await readUpgradeState();
|
|
294
|
+
const problems = stateProblems(state);
|
|
295
|
+
const files = managedFiles(manifest);
|
|
296
|
+
const paths = selectedPaths(scopes);
|
|
297
|
+
const changes = [];
|
|
298
|
+
for (const path of Object.keys(files).filter((candidate) => paths.has(candidate))) {
|
|
299
|
+
const target = resolve(root, path);
|
|
300
|
+
const current = await exists(target) ? await readFile(target, 'utf8') : null;
|
|
301
|
+
const expected = files[path];
|
|
302
|
+
const recordedHash = state?.files?.[path]?.hash;
|
|
303
|
+
const result = classifyUpgradeFile(path, current, expected, recordedHash);
|
|
304
|
+
changes.push({ path, scope: Object.entries(upgradeScopes).find(([, values]) => values.includes(path))?.[0], ...result });
|
|
305
|
+
}
|
|
306
|
+
if (state?.baseline && state.baseline !== manifest.baseline) {
|
|
307
|
+
changes.unshift({ path: 'baseline', scope: 'harness', classification: 'breaking', action: 'review', reason: `baseline ${state.baseline} is not compatible with ${manifest.baseline}` });
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
source: {
|
|
311
|
+
package: state?.package ?? null,
|
|
312
|
+
baseline: state?.baseline ?? null,
|
|
313
|
+
capabilities: state?.capabilities ?? null,
|
|
314
|
+
projections: state?.projections ?? null,
|
|
315
|
+
lastAppliedMigration: state?.lastAppliedMigration ?? null,
|
|
316
|
+
},
|
|
317
|
+
target: {
|
|
318
|
+
package: { name: cliPackage.name, version: cliPackage.version },
|
|
319
|
+
baseline: manifest.baseline,
|
|
320
|
+
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
321
|
+
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
322
|
+
},
|
|
323
|
+
scopes: [...scopes],
|
|
324
|
+
prerequisites: problems.length ? [{ status: 'failed', message: problems.join(' ') }] : [{ status: 'passed', message: 'Ownership metadata is readable.' }],
|
|
325
|
+
changes,
|
|
326
|
+
verification: ['lighthouse verify'],
|
|
327
|
+
recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
|
|
328
|
+
problems,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function printUpgradePlan(plan) {
|
|
333
|
+
console.log('Lighthouse upgrade plan:');
|
|
334
|
+
console.log(` package ${plan.source.package?.version ?? 'unknown'} -> ${plan.target.package.version}`);
|
|
335
|
+
console.log(` baseline ${plan.source.baseline ?? 'unknown'} -> ${plan.target.baseline}`);
|
|
336
|
+
console.log(` scopes ${plan.scopes.join(', ')}`);
|
|
337
|
+
for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
|
|
338
|
+
for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
|
|
339
|
+
if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
|
|
340
|
+
console.log(` verify ${plan.verification[0]}`);
|
|
341
|
+
console.log(` recovery ${plan.recovery}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
201
344
|
function validate(manifest) {
|
|
202
345
|
const problems = [];
|
|
203
346
|
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return ['manifest must be an object'];
|
|
@@ -244,8 +387,11 @@ function validate(manifest) {
|
|
|
244
387
|
const completeManaged = Array.isArray(manifest.managed)
|
|
245
388
|
&& manifest.managed.length === managedPaths.length
|
|
246
389
|
&& manifest.managed.every((entry, index) => entry === managedPaths[index]);
|
|
247
|
-
|
|
248
|
-
|
|
390
|
+
const previousCompleteManaged = Array.isArray(manifest.managed)
|
|
391
|
+
&& manifest.managed.length === previousManagedPaths.length
|
|
392
|
+
&& manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
|
|
393
|
+
if (!legacyManaged && !previousCompleteManaged && !completeManaged) {
|
|
394
|
+
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the current complete set`);
|
|
249
395
|
}
|
|
250
396
|
if ('repository' in manifest) {
|
|
251
397
|
const repository = manifest.repository;
|
|
@@ -317,6 +463,8 @@ async function planInitialization(manifest, manifestMissing) {
|
|
|
317
463
|
if (await exists(resolve(root, file))) conflicts.push(`${file} already exists; refusing to overwrite it`);
|
|
318
464
|
else operations.push({ path: file, action: 'create' });
|
|
319
465
|
}
|
|
466
|
+
if (await exists(upgradeStatePath)) conflicts.push(`${upgradeStateRelative} already exists; refusing to overwrite it`);
|
|
467
|
+
else operations.push({ path: upgradeStateRelative, action: 'create' });
|
|
320
468
|
return { operations, conflicts };
|
|
321
469
|
}
|
|
322
470
|
|
|
@@ -337,6 +485,9 @@ async function planReconciliation(manifest) {
|
|
|
337
485
|
conflicts.push(`${path} exists but is not a readable file; refusing to overwrite it`);
|
|
338
486
|
}
|
|
339
487
|
}
|
|
488
|
+
if (!(await exists(upgradeStatePath))) {
|
|
489
|
+
operations.push({ path: upgradeStateRelative, action: 'create', content: `${JSON.stringify(upgradeState(manifest), null, 2)}\n` });
|
|
490
|
+
}
|
|
340
491
|
return { operations, conflicts };
|
|
341
492
|
}
|
|
342
493
|
|
|
@@ -374,6 +525,7 @@ async function initialize(args) {
|
|
|
374
525
|
await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
|
|
375
526
|
if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
|
|
376
527
|
}
|
|
528
|
+
await writeUpgradeState(manifest, null, files);
|
|
377
529
|
try {
|
|
378
530
|
const configured = execFileSync('git', ['config', '--get', 'core.hooksPath'], { cwd: root, encoding: 'utf8' }).trim();
|
|
379
531
|
if (!configured) execFileSync('git', ['config', 'core.hooksPath', '.husky'], { cwd: root, stdio: 'ignore' });
|
|
@@ -413,6 +565,76 @@ async function reconcile(args) {
|
|
|
413
565
|
console.log('Reconciled missing Lighthouse-managed paths. Run lighthouse verify to check the complete projection.');
|
|
414
566
|
}
|
|
415
567
|
|
|
568
|
+
async function writeTextAtomically(path, content) {
|
|
569
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
570
|
+
await writeFile(temporary, content, { flag: 'wx' });
|
|
571
|
+
await rename(temporary, path);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function applyUpgrade(manifest, plan) {
|
|
575
|
+
if (plan.problems.length) throw new Error(`Upgrade stopped; ${plan.problems.join(' ')}`);
|
|
576
|
+
const blocked = plan.changes.filter((change) => ['conflicting', 'breaking'].includes(change.classification));
|
|
577
|
+
if (blocked.length) throw new Error(`Upgrade stopped; resolve ownership conflicts for: ${blocked.map((change) => change.path).join(', ')}.`);
|
|
578
|
+
const files = managedFiles(manifest);
|
|
579
|
+
const previous = await readUpgradeState();
|
|
580
|
+
const snapshots = new Map();
|
|
581
|
+
const updates = plan.changes.filter((change) => ['add', 'update', 'record'].includes(change.action));
|
|
582
|
+
for (const change of updates) {
|
|
583
|
+
const target = resolve(root, change.path);
|
|
584
|
+
snapshots.set(change.path, await exists(target) ? await readFile(target, 'utf8') : null);
|
|
585
|
+
}
|
|
586
|
+
const stateSnapshot = await readFile(upgradeStatePath, 'utf8');
|
|
587
|
+
try {
|
|
588
|
+
for (const change of updates) {
|
|
589
|
+
const target = resolve(root, change.path);
|
|
590
|
+
await mkdir(dirname(target), { recursive: true });
|
|
591
|
+
await writeTextAtomically(target, files[change.path]);
|
|
592
|
+
}
|
|
593
|
+
const next = upgradeState(manifest, files, { ...previous, lastAppliedMigration: `upgrade-${cliPackage.version}` });
|
|
594
|
+
for (const path of Object.keys(next.files)) {
|
|
595
|
+
const selected = plan.changes.find((change) => change.path === path);
|
|
596
|
+
if (!selected || selected.action === 'preserve' || selected.action === 'review') {
|
|
597
|
+
if (previous.files?.[path]) next.files[path] = previous.files[path];
|
|
598
|
+
else delete next.files[path];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
next.lastAppliedMigration = `upgrade-${cliPackage.version}`;
|
|
602
|
+
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
for (const [path, content] of snapshots) {
|
|
605
|
+
const target = resolve(root, path);
|
|
606
|
+
if (content === null) await rm(target, { force: true });
|
|
607
|
+
else await writeFile(target, content);
|
|
608
|
+
}
|
|
609
|
+
await writeFile(upgradeStatePath, stateSnapshot);
|
|
610
|
+
throw new Error(`Upgrade rolled back: ${error.message}`);
|
|
611
|
+
}
|
|
612
|
+
const preserved = plan.changes.filter((change) => change.classification === 'customized').map((change) => change.path);
|
|
613
|
+
console.log(`Applied Lighthouse upgrade ${cliPackage.version}. Run lighthouse verify to check the result.`);
|
|
614
|
+
if (preserved.length) console.log(`Preserved customized paths for review: ${preserved.join(', ')}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function upgrade(args) {
|
|
618
|
+
const manifest = await readManifest();
|
|
619
|
+
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
620
|
+
const problems = validate(manifest);
|
|
621
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
622
|
+
const plan = await planUpgrade(manifest, scopesFromArgs(args));
|
|
623
|
+
const format = formatFromArgs(args);
|
|
624
|
+
const apply = args.includes('--apply');
|
|
625
|
+
const dryRun = args.includes('--dry-run') || !apply;
|
|
626
|
+
if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
|
|
627
|
+
if (format === 'json' && apply) throw new Error('--format json is available for dry-run upgrade plans; use text when applying.');
|
|
628
|
+
if (format === 'json') console.log(JSON.stringify(plan, null, 2));
|
|
629
|
+
else printUpgradePlan(plan);
|
|
630
|
+
if (dryRun) {
|
|
631
|
+
if (format !== 'json') console.log('Dry run: nothing was changed. Re-run with --apply after reviewing the plan.');
|
|
632
|
+
if (plan.problems.length) process.exitCode = 1;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
await applyUpgrade(manifest, plan);
|
|
636
|
+
}
|
|
637
|
+
|
|
416
638
|
const authorityCommands = [
|
|
417
639
|
{
|
|
418
640
|
label: 'ProductShape',
|
|
@@ -587,21 +809,30 @@ let invalidArgument = false;
|
|
|
587
809
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
588
810
|
const arg = commandArgs[index];
|
|
589
811
|
if (arg === '--dry-run') continue;
|
|
590
|
-
if (arg === '--apply') { if (!['authorities', 'reconcile'].includes(command)) invalidArgument = true; continue; }
|
|
812
|
+
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
591
813
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
592
814
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
593
815
|
if (arg === '--format') {
|
|
594
|
-
if (
|
|
816
|
+
if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
595
817
|
else index += 1;
|
|
596
818
|
continue;
|
|
597
819
|
}
|
|
598
820
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
599
|
-
if (
|
|
821
|
+
if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (arg === '--scope') {
|
|
825
|
+
if (command !== 'upgrade' || !commandArgs[index + 1]) invalidArgument = true;
|
|
826
|
+
else index += 1;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (arg.startsWith('--scope=') && arg.length > '--scope='.length) {
|
|
830
|
+
if (command !== 'upgrade') invalidArgument = true;
|
|
600
831
|
continue;
|
|
601
832
|
}
|
|
602
833
|
invalidArgument = true;
|
|
603
834
|
}
|
|
604
|
-
if (!['init', 'authorities', 'reconcile', 'verify'].includes(command) || invalidArgument) {
|
|
835
|
+
if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
|
|
605
836
|
console.error(usage);
|
|
606
837
|
process.exit(2);
|
|
607
838
|
}
|
|
@@ -609,6 +840,7 @@ try {
|
|
|
609
840
|
if (command === 'init') await initialize(args.slice(1));
|
|
610
841
|
else if (command === 'authorities') await authorities(args.slice(1));
|
|
611
842
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
843
|
+
else if (command === 'upgrade') await upgrade(args.slice(1));
|
|
612
844
|
else {
|
|
613
845
|
const manifest = await readManifest();
|
|
614
846
|
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Lighthouse 0.3.0 upgrade guide
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- The repository has a valid `lighthouse.json`.
|
|
6
|
+
- The working tree is clean or the update is isolated in a reviewable branch.
|
|
7
|
+
- `.lighthouse/upgrade.json` exists and records the managed projection hashes.
|
|
8
|
+
- Node.js 22 or newer is available.
|
|
9
|
+
|
|
10
|
+
If `.lighthouse/upgrade.json` is missing, run `lighthouse reconcile --dry-run`, review the plan and then run `lighthouse reconcile --apply` to establish ownership evidence. Resolve any reported conflicts first.
|
|
11
|
+
|
|
12
|
+
## Plan the update
|
|
13
|
+
|
|
14
|
+
Run `lighthouse upgrade` to preview all Lighthouse-owned scopes. Use `lighthouse upgrade --scope harness` for `LIGHTHOUSE.md`, hooks, the guard script and the repository workflow, or use `lighthouse upgrade --scope sdlc` for the SDLC control-plane projection.
|
|
15
|
+
|
|
16
|
+
The plan reports the installed package and baseline, the selected scopes, prerequisites, file classifications, preserved content, verification command and recovery action. `lighthouse upgrade --format json` produces the same plan for a review tool or CI job.
|
|
17
|
+
|
|
18
|
+
## Apply the update
|
|
19
|
+
|
|
20
|
+
Review the plan and run `lighthouse upgrade --apply` with the same scope. Lighthouse writes only missing or provably managed projections, records the new ownership hashes and preserves customized files for review.
|
|
21
|
+
|
|
22
|
+
Run `lighthouse verify` after the update. A preserved customized projection may still report drift; resolve it through the normal repository change route before making the verification gate required.
|
|
23
|
+
|
|
24
|
+
## Recovery
|
|
25
|
+
|
|
26
|
+
If the update stops, keep the plan output and resolve the stated ownership or prerequisite issue. If a write fails, Lighthouse restores the files changed by that attempt; restore the branch or rerun the plan after correcting the reported condition.
|