@juangcarmona/lighthouse 0.1.0 → 0.3.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.
@@ -1,60 +1,852 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, writeFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
2
+ import { access, chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { constants, readFileSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { createRequire } from 'node:module';
6
+ import { basename, dirname, resolve } from 'node:path';
7
+ import { execFileSync, spawnSync } from 'node:child_process';
8
+ import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
4
9
 
5
10
  const root = process.cwd();
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
+ }
6
19
  const manifestPath = resolve(root, 'lighthouse.json');
7
20
  const projection = 'LIGHTHOUSE.md';
8
- const usage = 'Usage: lighthouse <init|verify>';
21
+ const upgradeStateRelative = '.lighthouse/upgrade.json';
22
+ const upgradeStatePath = resolve(root, upgradeStateRelative);
23
+ const layout = ['src', 'docs', 'infra', 'scripts'];
24
+ const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
25
+ const managedPaths = [
26
+ projection,
27
+ 'scripts/lighthouse-guard.mjs',
28
+ '.husky/commit-msg',
29
+ '.husky/pre-push',
30
+ '.github/workflows/lighthouse.yml',
31
+ '.lighthouse/sdlc.json',
32
+ upgradeStateRelative,
33
+ ];
34
+ const previousManagedPaths = managedPaths.slice(0, -1);
35
+ const legacyManagedPaths = [projection];
36
+ const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
37
+ const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
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]';
44
+
45
+ const defaultRepository = () => ({
46
+ layout: [...layout],
47
+ publicSite: { enabled: true },
48
+ conventions: { commits: 'conventional', branchPattern },
49
+ });
50
+
51
+ function defaultManifest(product) {
52
+ return {
53
+ $schema: './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-manifest.schema.json',
54
+ schemaVersion: 1,
55
+ baseline: '0.1.0',
56
+ product,
57
+ profiles: [],
58
+ integrations: { productShape: null, arcShape: null },
59
+ packages: { sdlc: { ...sdlcPackage } },
60
+ managed: [...managedPaths],
61
+ repository: defaultRepository(),
62
+ };
63
+ }
64
+
65
+ function normalizeRepository(repository) {
66
+ const value = repository && typeof repository === 'object' && !Array.isArray(repository) ? repository : {};
67
+ return {
68
+ layout: Array.isArray(value.layout) ? value.layout : [...layout],
69
+ publicSite: value.publicSite && typeof value.publicSite === 'object'
70
+ ? { enabled: value.publicSite.enabled !== false }
71
+ : { enabled: true },
72
+ conventions: value.conventions && typeof value.conventions === 'object'
73
+ ? {
74
+ commits: value.conventions.commits ?? 'conventional',
75
+ branchPattern: value.conventions.branchPattern ?? branchPattern,
76
+ }
77
+ : { commits: 'conventional', branchPattern },
78
+ };
79
+ }
80
+
81
+ function normalizeSdlcPackage(manifest) {
82
+ const value = manifest?.packages?.sdlc;
83
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
84
+ return { name: value.name, version: value.version };
85
+ }
86
+
87
+ function sdlcContract() {
88
+ try {
89
+ const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/contract.json');
90
+ return JSON.parse(readFileSync(path, 'utf8'));
91
+ } catch {
92
+ return {
93
+ routes: ['supervised', 'plan-only', 'unattended', 'hotfix', 'stacked'],
94
+ gates: ['triaged', 'ready', 'planned', 'done'],
95
+ };
96
+ }
97
+ }
98
+
99
+ function sdlcControlPlane(manifest) {
100
+ const selected = normalizeSdlcPackage(manifest);
101
+ if (!selected) return null;
102
+ const contract = sdlcContract();
103
+ return `${JSON.stringify({
104
+ $schema: './node_modules/@juangcarmona/lighthouse-sdlc/schemas/control-plane.schema.json',
105
+ schemaVersion: 1,
106
+ package: selected,
107
+ routes: contract.routes,
108
+ gates: contract.gates,
109
+ state: { provider: 'agent-host' },
110
+ memory: { provider: 'agent-host', scopes: ['run', 'project'] },
111
+ roles: {},
112
+ adapters: {},
113
+ }, null, 2)}\n`;
114
+ }
9
115
 
10
116
  function render(manifest) {
11
- return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Agentic SDLC: ${manifest.integrations.agenticSdlc ?? 'not adopted'}\n\n## How delivery works\n\nUse ProductShape for product intent and Product Changes, and use ArcShape for applicable architecture decisions; they can advance independently. Lighthouse carries the selected authority context through the delivery stages.\n\nA greenfield project initializes both authorities through their supported provider routes. An existing project can adopt them incrementally after its ownership and managed paths are assessed. The adoption contract includes a check of the external CodeGraph MCP service, its current git-root binding and the canonical repository and active worktrees; its index and credentials stay outside the checkout.\n\n1. \`triage\` classifies the work item, estimates complexity and blast radius, records urgency, impact, dependencies and risk, and sets configured workflow labels.\n2. \`refine\` defines the problem and stops at Ready.\n3. \`propose\` creates the solution and stops at Planned.\n4. \`implement\` applies the accepted plan and stops at Done.\n5. \`integrate\` updates the tracker, maintains the PR stack and closes the repository loop.\n6. \`review\` records learning after completion.\n\nThese are lifecycle capabilities. The baseline CLI currently provides \`init\` and \`verify\`; a configured OpenSpec, tracker or agent-host integration supplies the stage commands.\n\nChoose supervised work when a person steers each stage, plan-only work when only the proposal is needed, \`ff\` for a bounded unattended route, \`hotfix\` for a confirmed defect or \`decompose\` for a large item that needs ordered child issues and stacked PRs.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
117
+ const repository = normalizeRepository(manifest.repository);
118
+ const publicSite = repository.publicSite.enabled ? 'enabled' : 'disabled';
119
+ const selectedSdlc = normalizeSdlcPackage(manifest);
120
+ const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
121
+ return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package and host adapters without storing agent runtime data.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
122
+ }
123
+
124
+ function guardrailScript(manifest) {
125
+ const pattern = JSON.stringify(normalizeRepository(manifest.repository).conventions.branchPattern);
126
+ return `#!/usr/bin/env node
127
+ import { readFileSync } from 'node:fs';
128
+ import { execFileSync } from 'node:child_process';
129
+
130
+ const mode = process.argv[2];
131
+ const branchPattern = new RegExp(${pattern}, 'u');
132
+ function fail(message) { console.error(message); process.exit(1); }
133
+ function branch() {
134
+ try { return execFileSync('git', ['branch', '--show-current'], { encoding: 'utf8' }).trim(); }
135
+ catch { return ''; }
136
+ }
137
+ if (mode === 'commit-msg') {
138
+ const file = process.argv[3];
139
+ if (!file) fail('Usage: lighthouse-guard commit-msg <message-file>');
140
+ const message = readFileSync(file, 'utf8').split('\\n').find((line) => line.trim() && !line.startsWith('#'))?.trim() ?? '';
141
+ if (!/^(?:feat|fix|docs|refactor|test|chore|build|ci|perf|revert)(?:\\([^)]+\\))?!?: .+/u.test(message)) fail('Commit message must follow Conventional Commits.');
142
+ } else if (mode === 'branch' || mode === 'verify') {
143
+ const current = branch();
144
+ if (current && !['main', 'master'].includes(current) && !branchPattern.test(current)) fail('Branch ' + current + ' does not match ' + branchPattern.source + '.');
145
+ } else fail('Usage: lighthouse-guard <commit-msg|branch|verify>');
146
+ `;
147
+ }
148
+
149
+ function commitHook() {
150
+ return '#!/usr/bin/env sh\nset -eu\nnode scripts/lighthouse-guard.mjs commit-msg "$1"\n';
151
+ }
152
+
153
+ function prePushHook() {
154
+ return '#!/usr/bin/env sh\nset -eu\nnode scripts/lighthouse-guard.mjs branch\n';
155
+ }
156
+
157
+ function workflow() {
158
+ return `name: Lighthouse repository guardrails
159
+
160
+ on:
161
+ pull_request:
162
+ paths:
163
+ - "lighthouse.json"
164
+ - "LIGHTHOUSE.md"
165
+ - "src/**"
166
+ - "docs/**"
167
+ - "infra/**"
168
+ - "scripts/**"
169
+ - ".husky/**"
170
+ - ".github/workflows/lighthouse.yml"
171
+ push:
172
+ branches: [main]
173
+ paths:
174
+ - "lighthouse.json"
175
+ - "LIGHTHOUSE.md"
176
+ - "src/**"
177
+ - "docs/**"
178
+ - "infra/**"
179
+ - "scripts/**"
180
+ - ".husky/**"
181
+ - ".github/workflows/lighthouse.yml"
182
+ workflow_dispatch:
183
+
184
+ permissions:
185
+ contents: read
186
+
187
+ jobs:
188
+ verify:
189
+ name: Verify Lighthouse foundation
190
+ runs-on: ubuntu-latest
191
+ timeout-minutes: 10
192
+ steps:
193
+ - uses: actions/checkout@v4
194
+ - uses: actions/setup-node@v4
195
+ with:
196
+ node-version: 22
197
+ cache: npm
198
+ - run: npm ci --ignore-scripts
199
+ - run: |
200
+ if [ -f scripts/lighthouse.mjs ]; then node scripts/lighthouse.mjs verify; else npx --no-install lighthouse verify; fi
201
+ - run: node scripts/lighthouse-guard.mjs verify
202
+ `;
203
+ }
204
+
205
+ function managedFiles(manifest) {
206
+ const files = {
207
+ [projection]: render(manifest),
208
+ 'scripts/lighthouse-guard.mjs': guardrailScript(manifest),
209
+ '.husky/commit-msg': commitHook(),
210
+ '.husky/pre-push': prePushHook(),
211
+ '.github/workflows/lighthouse.yml': workflow(),
212
+ };
213
+ const controlPlane = sdlcControlPlane(manifest);
214
+ if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
215
+ return files;
216
+ }
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
+ }
12
245
  }
13
246
 
14
- function validate(m) {
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
+
344
+ function validate(manifest) {
15
345
  const problems = [];
16
- if (!m || typeof m !== 'object' || Array.isArray(m)) return ['manifest must be an object'];
17
- const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'managed'];
18
- for (const key of Object.keys(m)) if (!allowed.includes(key)) problems.push(`unknown field ${key}`);
19
- for (const key of ['schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'managed']) if (!(key in m)) problems.push(`missing ${key}`);
20
- if (m.schemaVersion !== 1) problems.push('schemaVersion must be 1');
21
- if (m.baseline !== '0.1.0') problems.push('this verifier supports baseline 0.1.0');
22
- if ('$schema' in m && typeof m.$schema !== 'string') problems.push('$schema must be a string');
23
- if (typeof m.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(m.product)) problems.push('product must be a readable name of 1–64 characters');
346
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return ['manifest must be an object'];
347
+ const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'packages', 'managed', 'repository'];
348
+ for (const key of Object.keys(manifest)) if (!allowed.includes(key)) problems.push(`unknown field ${key}`);
349
+ for (const key of ['schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'managed']) {
350
+ if (!(key in manifest)) problems.push(`missing ${key}`);
351
+ }
352
+ if (manifest.schemaVersion !== 1) problems.push('schemaVersion must be 1');
353
+ if (manifest.baseline !== '0.1.0') problems.push('this verifier supports baseline 0.1.0');
354
+ if ('$schema' in manifest && typeof manifest.$schema !== 'string') problems.push('$schema must be a string');
355
+ if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
356
+ problems.push('product must be a readable name of 1–64 characters');
357
+ }
24
358
  const profiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
25
- if (!Array.isArray(m.profiles) || m.profiles.some((p) => !profiles.includes(p)) || new Set(m.profiles).size !== m.profiles.length) problems.push('profiles must be unique supported profile names');
26
- const integrations = m.integrations;
27
- const keys = ['productShape', 'arcShape', 'agenticSdlc'];
359
+ if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
360
+ || new Set(manifest.profiles).size !== manifest.profiles.length) {
361
+ problems.push('profiles must be unique supported profile names');
362
+ }
363
+ const integrations = manifest.integrations;
364
+ const keys = ['productShape', 'arcShape'];
28
365
  const pin = /^(?:\d+\.\d+\.\d+|[a-f0-9]{40})$/;
29
366
  if (!integrations || typeof integrations !== 'object' || Array.isArray(integrations)
30
367
  || Object.keys(integrations).length !== keys.length
31
- || keys.some((k) => !(k in integrations) || (integrations[k] !== null && (typeof integrations[k] !== 'string' || !pin.test(integrations[k])))))
32
- problems.push('integrations must declare productShape, arcShape and agenticSdlc as exact versions, commit hashes or null');
33
- if (!Array.isArray(m.managed) || m.managed.length !== 1 || m.managed[0] !== projection) problems.push(`only ${projection} is managed by this version`);
368
+ || keys.some((key) => !(key in integrations)
369
+ || (integrations[key] !== null && (typeof integrations[key] !== 'string' || !pin.test(integrations[key]))))) {
370
+ problems.push('integrations must declare productShape and arcShape as exact versions, commit hashes or null');
371
+ }
372
+ if ('packages' in manifest) {
373
+ const packages = manifest.packages;
374
+ const selected = packages?.sdlc;
375
+ if (!packages || typeof packages !== 'object' || Array.isArray(packages)
376
+ || Object.keys(packages).length !== 1
377
+ || !selected || typeof selected !== 'object' || Array.isArray(selected)
378
+ || Object.keys(selected).length !== 2
379
+ || selected.name !== sdlcPackage.name
380
+ || typeof selected.version !== 'string' || !pin.test(selected.version)) {
381
+ problems.push(`packages.sdlc must declare ${sdlcPackage.name} and an exact version or commit hash`);
382
+ }
383
+ }
384
+ const legacyManaged = Array.isArray(manifest.managed)
385
+ && manifest.managed.length === legacyManagedPaths.length
386
+ && manifest.managed.every((entry, index) => entry === legacyManagedPaths[index]);
387
+ const completeManaged = Array.isArray(manifest.managed)
388
+ && manifest.managed.length === managedPaths.length
389
+ && manifest.managed.every((entry, index) => entry === managedPaths[index]);
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`);
395
+ }
396
+ if ('repository' in manifest) {
397
+ const repository = manifest.repository;
398
+ if (!repository || typeof repository !== 'object' || Array.isArray(repository)) {
399
+ problems.push('repository must be an object');
400
+ } else {
401
+ if (!Array.isArray(repository.layout) || repository.layout.length !== layout.length
402
+ || repository.layout.some((entry, index) => entry !== layout[index])) {
403
+ problems.push(`repository.layout must be ${layout.map((entry) => `'${entry}'`).join(', ')}`);
404
+ }
405
+ if (!repository.publicSite || typeof repository.publicSite.enabled !== 'boolean') {
406
+ problems.push('repository.publicSite.enabled must be a boolean');
407
+ }
408
+ if (!repository.conventions || repository.conventions.commits !== 'conventional'
409
+ || typeof repository.conventions.branchPattern !== 'string') {
410
+ problems.push('repository.conventions must declare conventional commits and a branchPattern');
411
+ } else {
412
+ try { new RegExp(repository.conventions.branchPattern, 'u'); }
413
+ catch { problems.push('repository.conventions.branchPattern must be a valid regular expression'); }
414
+ }
415
+ }
416
+ }
34
417
  return problems;
35
418
  }
36
419
 
37
- const command = process.argv[2];
38
- if (!['init', 'verify'].includes(command)) { console.error(usage); process.exit(2); }
39
- let manifest;
40
- try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')); }
41
- catch (error) { console.error(`Cannot read lighthouse.json: ${error.message}`); process.exit(1); }
42
- const problems = validate(manifest);
43
- if (problems.length) { console.error(problems.map((p) => `Invalid lighthouse.json: ${p}`).join('\n')); process.exit(1); }
44
- const expected = render(manifest);
45
- const target = resolve(root, projection);
46
- if (command === 'init') {
47
- try { await readFile(target); console.error(`${projection} already exists; refusing to overwrite it. Review the manifest and manage the file explicitly.`); process.exit(1); }
48
- catch (error) { if (error.code !== 'ENOENT') throw error; }
49
- await writeFile(target, expected, { flag: 'wx' });
50
- console.log(`Created ${projection} from lighthouse.json.`);
51
- } else {
420
+ async function exists(path) {
421
+ try { await access(path, constants.F_OK); return true; } catch { return false; }
422
+ }
423
+
424
+ async function isDirectory(path) {
425
+ try { return (await stat(path)).isDirectory(); } catch { return false; }
426
+ }
427
+
428
+ async function readManifest() {
429
+ try { return JSON.parse(await readFile(manifestPath, 'utf8')); }
430
+ catch (error) {
431
+ if (error.code === 'ENOENT') return null;
432
+ throw new Error(`Cannot read lighthouse.json: ${error.message}`);
433
+ }
434
+ }
435
+
436
+ function productNameFromArgs(args) {
437
+ const index = args.indexOf('--name');
438
+ const value = index === -1 ? args.find((arg) => arg.startsWith('--name='))?.slice('--name='.length) : args[index + 1];
439
+ if (index === -1 && !value) return basename(root).replace(/[^A-Za-z0-9 ._-]/gu, '').slice(0, 64) || 'adopter';
440
+ if (!value) throw new Error('--name requires a product name');
441
+ return value;
442
+ }
443
+
444
+ async function planInitialization(manifest, manifestMissing) {
445
+ const operations = [];
446
+ const conflicts = [];
447
+ if (manifestMissing) operations.push({ path: 'lighthouse.json', action: 'create' });
448
+ const files = managedFiles(manifest);
449
+ const directories = new Set(normalizeRepository(manifest.repository).layout);
450
+ for (const file of Object.keys(files)) {
451
+ let directory = dirname(file);
452
+ while (directory !== '.') {
453
+ directories.add(directory);
454
+ directory = dirname(directory);
455
+ }
456
+ }
457
+ for (const directory of [...directories].sort((left, right) => left.split('/').length - right.split('/').length)) {
458
+ const path = resolve(root, directory);
459
+ if (!(await exists(path))) operations.push({ path: `${directory}/`, action: 'create' });
460
+ else if (!(await isDirectory(path))) conflicts.push(`${directory} exists but is not a directory`);
461
+ }
462
+ for (const file of Object.keys(files)) {
463
+ if (await exists(resolve(root, file))) conflicts.push(`${file} already exists; refusing to overwrite it`);
464
+ else operations.push({ path: file, action: 'create' });
465
+ }
466
+ if (await exists(upgradeStatePath)) conflicts.push(`${upgradeStateRelative} already exists; refusing to overwrite it`);
467
+ else operations.push({ path: upgradeStateRelative, action: 'create' });
468
+ return { operations, conflicts };
469
+ }
470
+
471
+ async function planReconciliation(manifest) {
472
+ const operations = [];
473
+ const conflicts = [];
474
+ for (const [path, expected] of Object.entries(managedFiles(manifest))) {
475
+ const target = resolve(root, path);
476
+ if (!(await exists(target))) {
477
+ operations.push({ path, action: 'create', content: expected });
478
+ continue;
479
+ }
480
+ try {
481
+ const current = await readFile(target, 'utf8');
482
+ if (current === expected) operations.push({ path, action: 'unchanged' });
483
+ else conflicts.push(`${path} exists with different content; refusing to overwrite it`);
484
+ } catch {
485
+ conflicts.push(`${path} exists but is not a readable file; refusing to overwrite it`);
486
+ }
487
+ }
488
+ if (!(await exists(upgradeStatePath))) {
489
+ operations.push({ path: upgradeStateRelative, action: 'create', content: `${JSON.stringify(upgradeState(manifest), null, 2)}\n` });
490
+ }
491
+ return { operations, conflicts };
492
+ }
493
+
494
+ function printReconciliationPlan(plan) {
495
+ console.log('Lighthouse reconciliation plan:');
496
+ for (const operation of plan.operations) console.log(` ${operation.action} ${operation.path}`);
497
+ for (const conflict of plan.conflicts) console.error(` conflict ${conflict}`);
498
+ }
499
+
500
+ async function initialize(args) {
501
+ let manifest = await readManifest();
502
+ const manifestMissing = !manifest;
503
+ if (!manifest) manifest = defaultManifest(productNameFromArgs(args));
504
+ const problems = validate(manifest);
505
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
506
+ const plan = await planInitialization(manifest, manifestMissing);
507
+ console.log('Lighthouse initialization plan:');
508
+ for (const operation of plan.operations) console.log(` ${operation.action} ${operation.path}`);
509
+ for (const conflict of plan.conflicts) console.error(` conflict ${conflict}`);
510
+ if (normalizeRepository(manifest.repository).publicSite.enabled) {
511
+ if (manifest.integrations.productShape === null) console.log(' adoption gap ProductShape: review lighthouse authorities --dry-run, then run lighthouse authorities --apply');
512
+ if (manifest.integrations.arcShape === null) console.log(' adoption gap ArcShape: review lighthouse authorities --dry-run, then run lighthouse authorities --apply');
513
+ }
514
+ if (args.includes('--dry-run')) {
515
+ if (plan.conflicts.length) process.exitCode = 1;
516
+ return;
517
+ }
518
+ if (plan.conflicts.length) throw new Error('Initialization stopped; resolve the reported conflicts and run lighthouse init again.');
519
+ if (manifestMissing) await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
520
+ for (const operation of plan.operations.filter((item) => item.path.endsWith('/'))) {
521
+ await mkdir(resolve(root, operation.path), { recursive: true });
522
+ }
523
+ const files = managedFiles(manifest);
524
+ for (const [file, content] of Object.entries(files)) {
525
+ await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
526
+ if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
527
+ }
528
+ await writeUpgradeState(manifest, null, files);
52
529
  try {
53
- const actual = await readFile(target, 'utf8');
54
- if (actual !== expected) { console.error(`${projection} has drifted. Regenerate it from lighthouse.json or restore the generated content.`); process.exit(1); }
530
+ const configured = execFileSync('git', ['config', '--get', 'core.hooksPath'], { cwd: root, encoding: 'utf8' }).trim();
531
+ if (!configured) execFileSync('git', ['config', 'core.hooksPath', '.husky'], { cwd: root, stdio: 'ignore' });
532
+ } catch { /* A non-git target can adopt the hooks when it creates its repository. */ }
533
+ console.log(`Initialized ${manifest.product}. Review lighthouse.json and the managed guardrails, then run lighthouse verify.`);
534
+ }
535
+
536
+ async function reconcile(args) {
537
+ const manifest = await readManifest();
538
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
539
+ const problems = validate(manifest);
540
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
541
+ const plan = await planReconciliation(manifest);
542
+ printReconciliationPlan(plan);
543
+ const apply = args.includes('--apply');
544
+ const dryRun = args.includes('--dry-run') || !apply;
545
+ if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
546
+ if (plan.conflicts.length) {
547
+ if (dryRun) process.exitCode = 1;
548
+ else throw new Error('Reconciliation stopped; resolve the reported conflicts and run lighthouse reconcile again.');
549
+ return;
550
+ }
551
+ if (dryRun) {
552
+ console.log('Dry run: nothing was changed. Re-run with --apply to create missing managed paths.');
553
+ return;
554
+ }
555
+ for (const operation of plan.operations.filter((item) => item.action === 'create')) {
556
+ await mkdir(dirname(resolve(root, operation.path)), { recursive: true });
557
+ try {
558
+ await writeFile(resolve(root, operation.path), operation.content, { flag: 'wx', mode: operation.path.startsWith('.husky/') ? 0o755 : 0o644 });
559
+ if (operation.path.startsWith('.husky/')) await chmod(resolve(root, operation.path), 0o755);
560
+ } catch (error) {
561
+ if (error.code === 'EEXIST') throw new Error(`Reconciliation stopped; ${operation.path} appeared during the plan.`);
562
+ throw error;
563
+ }
564
+ }
565
+ console.log('Reconciled missing Lighthouse-managed paths. Run lighthouse verify to check the complete projection.');
566
+ }
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`);
55
603
  } catch (error) {
56
- if (error.code === 'ENOENT') { console.error(`${projection} is missing; run lighthouse init after reviewing the manifest.`); process.exit(1); }
57
- throw 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
+
638
+ const authorityCommands = [
639
+ {
640
+ label: 'ProductShape',
641
+ packageName: '@prodshape/cli',
642
+ command: 'prodshape',
643
+ initArgs: ['init', '--full', '--sdd', 'none', '--gitignore'],
644
+ repair: 'prodshape init --full --sdd none --gitignore',
645
+ },
646
+ {
647
+ label: 'ArcShape',
648
+ packageName: '@arcshape/cli',
649
+ command: 'arcshape',
650
+ initArgs: ['init', '--no-agents'],
651
+ applyArgs: ['init', '--no-agents', '--yes'],
652
+ repair: 'arcshape init --no-agents --yes',
653
+ },
654
+ ];
655
+
656
+ function providerExecutable(authority) {
657
+ const packageJson = packageRequire.resolve(`${authority.packageName}/package.json`);
658
+ const metadata = JSON.parse(readFileSync(packageJson, 'utf8'));
659
+ const binary = typeof metadata.bin === 'string' ? metadata.bin : metadata.bin?.[authority.command];
660
+ if (!binary) throw new Error(`${authority.packageName} does not declare a ${authority.command} executable`);
661
+ return resolve(dirname(packageJson), binary);
662
+ }
663
+
664
+ function providerHasConflict(output) {
665
+ return output.split('\n').some((line) => /^\s*conflict\b/i.test(line))
666
+ || /Conflicts\s*\(\s*[1-9]\d*\s*\)/i.test(output);
667
+ }
668
+
669
+ function runAuthorityCommand(authority, dryRun) {
670
+ const initArgs = dryRun ? authority.initArgs : (authority.applyArgs ?? authority.initArgs);
671
+ const args = [providerExecutable(authority), ...initArgs];
672
+ if (dryRun) args.push('--dry-run');
673
+ const result = spawnSync(process.execPath, args, {
674
+ cwd: root,
675
+ encoding: 'utf8',
676
+ stdio: ['pipe', 'pipe', 'pipe'],
677
+ });
678
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
679
+ console.log(`\n${authority.label} ${dryRun ? 'preflight' : 'apply'}:`);
680
+ if (result.stdout) process.stdout.write(result.stdout);
681
+ if (result.stderr) process.stderr.write(result.stderr);
682
+ return {
683
+ ok: result.status === 0 && !providerHasConflict(output),
684
+ output,
685
+ status: result.status,
686
+ };
687
+ }
688
+
689
+ async function writeManifestAtomically(manifest) {
690
+ const temporary = `${manifestPath}.tmp-${process.pid}`;
691
+ await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
692
+ await rename(temporary, manifestPath);
693
+ }
694
+
695
+ async function ensureProjectionReady(manifest) {
696
+ const target = resolve(root, projection);
697
+ try {
698
+ const current = await readFile(target, 'utf8');
699
+ if (current !== render(manifest)) {
700
+ throw new Error(`${projection} has drifted; regenerate it from lighthouse.json before running lighthouse authorities --apply.`);
701
+ }
702
+ } catch (error) {
703
+ if (error.code !== 'ENOENT') throw error;
704
+ }
705
+ }
706
+
707
+ async function writeProjection(manifest) {
708
+ const target = resolve(root, projection);
709
+ await writeFile(target, render(manifest), { flag: 'w' });
710
+ }
711
+
712
+ async function authorities(args) {
713
+ const manifest = await readManifest();
714
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
715
+ const problems = validate(manifest);
716
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
717
+ const apply = args.includes('--apply');
718
+ const dryRun = args.includes('--dry-run') || !apply;
719
+ if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
720
+ console.log('Lighthouse authority plan:');
721
+ console.log(' ProductShape: initialize the full ProductShape layout without an SDD integration');
722
+ console.log(' ArcShape: initialize arc42 documentation without installing agent capabilities');
723
+ console.log(' Provider-owned files remain under provider control; Lighthouse records only exact versions.');
724
+ const preflights = authorityCommands.map((authority) => ({ authority, result: runAuthorityCommand(authority, true) }));
725
+ const failed = preflights.filter(({ result }) => !result.ok);
726
+ if (failed.length) {
727
+ throw new Error(failed.map(({ authority }) => `${authority.label} preflight failed; repair with: ${authority.repair}`).join('\n'));
728
+ }
729
+ if (dryRun) {
730
+ console.log('Dry run: nothing was changed. Re-run with --apply to apply both provider plans.');
731
+ return;
732
+ }
733
+ await ensureProjectionReady(manifest);
734
+ for (const authority of authorityCommands) {
735
+ const result = runAuthorityCommand(authority, false);
736
+ if (!result.ok) throw new Error(`${authority.label} apply failed; repair with: ${authority.repair}`);
737
+ }
738
+ const updated = {
739
+ ...manifest,
740
+ integrations: { ...manifest.integrations, ...authorityVersions },
741
+ };
742
+ await writeManifestAtomically(updated);
743
+ await writeProjection(updated);
744
+ console.log(`Recorded ProductShape ${authorityVersions.productShape} and ArcShape ${authorityVersions.arcShape} in lighthouse.json.`);
745
+ }
746
+
747
+ function gitBranch() {
748
+ try {
749
+ return execFileSync('git', ['branch', '--show-current'], {
750
+ cwd: root,
751
+ encoding: 'utf8',
752
+ stdio: ['ignore', 'pipe', 'ignore'],
753
+ }).trim();
754
+ }
755
+ catch { return ''; }
756
+ }
757
+
758
+ function validateGitConventions(repository) {
759
+ const problems = [];
760
+ const branch = gitBranch();
761
+ if (branch && !['main', 'master'].includes(branch) && !new RegExp(repository.conventions.branchPattern, 'u').test(branch)) {
762
+ problems.push(`branch ${branch} does not match ${repository.conventions.branchPattern}`);
763
+ }
764
+ const message = process.env.LIGHTHOUSE_COMMIT_MESSAGE;
765
+ if (message && repository.conventions.commits === 'conventional'
766
+ && !/^(?:feat|fix|docs|refactor|test|chore|build|ci|perf|revert)(?:\([^)]+\))?!?: .+/u.test(message.trim())) {
767
+ problems.push('commit message does not follow Conventional Commits');
768
+ }
769
+ return problems;
770
+ }
771
+
772
+ async function verify(manifest, format = 'text') {
773
+ const manifestProblems = validate(manifest);
774
+ const validManifest = manifestProblems.length === 0;
775
+ const repository = normalizeRepository(manifest?.repository);
776
+ const managed = validManifest ? managedFiles(manifest) : {};
777
+ const report = await evaluateGuardrails({
778
+ catalog: readGuardrailCatalog(),
779
+ manifest,
780
+ manifestProblems,
781
+ repository,
782
+ managedFiles: managed,
783
+ resolve: (path) => resolve(root, path),
784
+ exists,
785
+ isDirectory,
786
+ readFile,
787
+ validateGitConventions,
788
+ branch: gitBranch(),
789
+ });
790
+ if (format === 'json') {
791
+ console.log(JSON.stringify(report, null, 2));
792
+ if (report.problems.length) process.exitCode = 1;
793
+ return;
794
+ }
795
+ if (report.problems.length) throw new Error(report.problems.join('\n'));
796
+ console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
797
+ }
798
+
799
+ function formatFromArgs(args) {
800
+ const index = args.indexOf('--format');
801
+ if (index !== -1) return args[index + 1];
802
+ return args.find((arg) => arg.startsWith('--format='))?.slice('--format='.length) ?? 'text';
803
+ }
804
+
805
+ const args = process.argv.slice(2);
806
+ const command = args[0];
807
+ const commandArgs = args.slice(1);
808
+ let invalidArgument = false;
809
+ for (let index = 0; index < commandArgs.length; index += 1) {
810
+ const arg = commandArgs[index];
811
+ if (arg === '--dry-run') continue;
812
+ if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
813
+ if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
814
+ if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
815
+ if (arg === '--format') {
816
+ if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
817
+ else index += 1;
818
+ continue;
819
+ }
820
+ if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
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;
831
+ continue;
832
+ }
833
+ invalidArgument = true;
834
+ }
835
+ if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
836
+ console.error(usage);
837
+ process.exit(2);
838
+ }
839
+ try {
840
+ if (command === 'init') await initialize(args.slice(1));
841
+ else if (command === 'authorities') await authorities(args.slice(1));
842
+ else if (command === 'reconcile') await reconcile(args.slice(1));
843
+ else if (command === 'upgrade') await upgrade(args.slice(1));
844
+ else {
845
+ const manifest = await readManifest();
846
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
847
+ await verify(manifest, formatFromArgs(commandArgs));
58
848
  }
59
- console.log('Lighthouse manifest and managed projection are valid.');
849
+ } catch (error) {
850
+ console.error(error.message);
851
+ process.exitCode = 1;
60
852
  }