@juangcarmona/lighthouse 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,12 +38,14 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
38
38
 
39
39
  ## Current command scope
40
40
 
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.
41
+ The command supports `init`, `authorities`, `reconcile`, `upgrade`, `capabilities` 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.
42
42
 
43
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.
44
44
 
45
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
46
 
47
+ The installed SDLC package also publishes a versioned capability catalog. `selected-by-default` capabilities appear as pending until the adopter selects or declines them; `opt-in` capabilities remain available without blocking `upgrade` or `verify`. `lighthouse capabilities select` and `lighthouse capabilities decline` record an explicit local choice, and `lighthouse verify --format json` exposes the adoption status and next command. For the GitHub labels slice, generate a local plan with `lighthouse capabilities plan --id labels --provider github --repo <owner/repo> --plan-file .lighthouse/reconciliation/labels.plan.json`, then apply it only with the plan hash returned by that command.
48
+
47
49
  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.
48
50
 
49
51
  ## Guardrail catalog
@@ -16,6 +16,24 @@
16
16
  },
17
17
  "baseline": {"type": "string", "minLength": 1},
18
18
  "capabilities": {"$ref": "#/$defs/versions"},
19
+ "adoption": {
20
+ "type": "object",
21
+ "additionalProperties": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["id", "status", "provider", "version", "catalogVersion", "nextAction"],
25
+ "properties": {
26
+ "id": {"type": "string"},
27
+ "status": {"enum": ["available", "pending-adoption", "adopted", "declined", "drifted", "blocked", "failed", "unavailable"]},
28
+ "selection": {"enum": ["selected-by-default", "opt-in"]},
29
+ "provider": {"type": "string"},
30
+ "version": {"type": "string"},
31
+ "catalogVersion": {"type": "string"},
32
+ "nextAction": {"type": ["string", "null"]},
33
+ "recordPath": {"type": "string"}
34
+ }
35
+ }
36
+ },
19
37
  "projections": {"$ref": "#/$defs/versions"},
20
38
  "lastAppliedMigration": {"type": "string", "minLength": 1},
21
39
  "files": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@arcshape/cli": "0.1.3",
24
24
  "@prodshape/cli": "0.22.0",
25
- "@juangcarmona/lighthouse-sdlc": "0.4.0"
25
+ "@juangcarmona/lighthouse-sdlc": "0.5.0"
26
26
  },
27
27
  "files": [
28
28
  "src/lighthouse.mjs",
@@ -6,6 +6,16 @@ import { createRequire } from 'node:module';
6
6
  import { basename, dirname, resolve } from 'node:path';
7
7
  import { execFileSync, spawnSync } from 'node:child_process';
8
8
  import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
9
+ let sdlcCapabilities;
10
+ let sdlcReconcile;
11
+ let sdlcGithub;
12
+ try {
13
+ [sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
14
+ import('@juangcarmona/lighthouse-sdlc/capabilities'),
15
+ import('@juangcarmona/lighthouse-sdlc/reconcile'),
16
+ import('@juangcarmona/lighthouse-sdlc/github'),
17
+ ]);
18
+ } catch { /* Standalone generated starters can run without the optional SDLC package. */ }
9
19
 
10
20
  const root = process.cwd();
11
21
  const packageRequire = createRequire(import.meta.url);
@@ -40,7 +50,7 @@ const upgradeScopes = {
40
50
  sdlc: ['.lighthouse/sdlc.json'],
41
51
  };
42
52
  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]';
53
+ const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
44
54
 
45
55
  const defaultRepository = () => ({
46
56
  layout: [...layout],
@@ -96,6 +106,45 @@ function sdlcContract() {
96
106
  }
97
107
  }
98
108
 
109
+ function sdlcCapabilityCatalog() {
110
+ if (!sdlcCapabilities) return null;
111
+ try {
112
+ const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/capabilities.json');
113
+ const catalog = JSON.parse(readFileSync(path, 'utf8'));
114
+ const errors = sdlcCapabilities.validateCapabilityCatalog(catalog);
115
+ if (errors.length) throw new Error(errors.map((error) => error.message).join('; '));
116
+ return catalog;
117
+ } catch (error) {
118
+ throw new Error(`Cannot read the installed SDLC capability catalog: ${error.message}`);
119
+ }
120
+ }
121
+
122
+ function capabilityAdoption(previous = null) {
123
+ const catalog = sdlcCapabilityCatalog();
124
+ if (!catalog) return {};
125
+ return Object.fromEntries(catalog.capabilities.map((capability) => {
126
+ const current = previous?.adoption?.[capability.id];
127
+ const status = current?.catalogVersion === catalog.package.version
128
+ ? current.status
129
+ : sdlcCapabilities.initialAdoptionStatus?.(capability) ?? 'pending-adoption';
130
+ const nextAction = status === 'available'
131
+ ? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
132
+ : ['adopted', 'declined'].includes(status)
133
+ ? null
134
+ : `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
135
+ return [capability.id, {
136
+ id: capability.id,
137
+ status,
138
+ selection: capability.selection,
139
+ provider: capability.provider,
140
+ version: capability.version,
141
+ catalogVersion: catalog.package.version,
142
+ nextAction,
143
+ ...(current?.recordPath ? { recordPath: current.recordPath } : {}),
144
+ }];
145
+ }));
146
+ }
147
+
99
148
  function sdlcControlPlane(manifest) {
100
149
  const selected = normalizeSdlcPackage(manifest);
101
150
  if (!selected) return null;
@@ -230,6 +279,7 @@ function upgradeState(manifest, files = managedFiles(manifest), previous = null)
230
279
  package: { name: cliPackage.name, version: cliPackage.version },
231
280
  baseline: manifest.baseline,
232
281
  capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
282
+ adoption: capabilityAdoption(previous),
233
283
  projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
234
284
  lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
235
285
  files: entries,
@@ -311,6 +361,7 @@ async function planUpgrade(manifest, scopes) {
311
361
  package: state?.package ?? null,
312
362
  baseline: state?.baseline ?? null,
313
363
  capabilities: state?.capabilities ?? null,
364
+ adoption: state?.adoption ?? null,
314
365
  projections: state?.projections ?? null,
315
366
  lastAppliedMigration: state?.lastAppliedMigration ?? null,
316
367
  },
@@ -318,6 +369,7 @@ async function planUpgrade(manifest, scopes) {
318
369
  package: { name: cliPackage.name, version: cliPackage.version },
319
370
  baseline: manifest.baseline,
320
371
  capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
372
+ adoption: capabilityAdoption(state),
321
373
  projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
322
374
  },
323
375
  scopes: [...scopes],
@@ -325,6 +377,11 @@ async function planUpgrade(manifest, scopes) {
325
377
  changes,
326
378
  verification: ['lighthouse verify'],
327
379
  recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
380
+ adoption: {
381
+ status: Object.values(capabilityAdoption(state)).some((item) => !['adopted', 'declined', 'available'].includes(item.status)) ? 'pending' : 'complete',
382
+ capabilities: capabilityAdoption(state),
383
+ externalWrites: 'none',
384
+ },
328
385
  problems,
329
386
  };
330
387
  }
@@ -336,6 +393,12 @@ function printUpgradePlan(plan) {
336
393
  console.log(` scopes ${plan.scopes.join(', ')}`);
337
394
  for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
338
395
  for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
396
+ if (plan.adoption) {
397
+ console.log(` capability adoption ${plan.adoption.status} (external writes: ${plan.adoption.externalWrites})`);
398
+ for (const capability of Object.values(plan.adoption.capabilities)) {
399
+ console.log(` capability ${capability.id ?? capability.provider} ${capability.status}${capability.nextAction ? `; next: ${capability.nextAction}` : ''}`);
400
+ }
401
+ }
339
402
  if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
340
403
  console.log(` verify ${plan.verification[0]}`);
341
404
  console.log(` recovery ${plan.recovery}`);
@@ -787,6 +850,30 @@ async function verify(manifest, format = 'text') {
787
850
  validateGitConventions,
788
851
  branch: gitBranch(),
789
852
  });
853
+ const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
854
+ for (const item of adoption) {
855
+ if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
856
+ item.status = 'failed';
857
+ item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
858
+ }
859
+ if (adoption.length) {
860
+ const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
861
+ const available = adoption.some((item) => item.status === 'available');
862
+ const declined = adoption.some((item) => item.status === 'declined');
863
+ report.results.push({
864
+ id: 'foundation.capability-adoption',
865
+ owner: '@juangcarmona/lighthouse-sdlc',
866
+ level: 'recommended',
867
+ kind: 'hard',
868
+ verification: 'deterministic',
869
+ status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
870
+ adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
871
+ evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
872
+ problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
873
+ nextAction: pending[0]?.nextAction ?? null,
874
+ });
875
+ report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
876
+ }
790
877
  if (format === 'json') {
791
878
  console.log(JSON.stringify(report, null, 2));
792
879
  if (report.problems.length) process.exitCode = 1;
@@ -796,6 +883,158 @@ async function verify(manifest, format = 'text') {
796
883
  console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
797
884
  }
798
885
 
886
+ function planHash(plan) {
887
+ const { authorization, ...payload } = plan;
888
+ return contentHash(JSON.stringify(payload));
889
+ }
890
+
891
+ function capabilityPlanText(plan) {
892
+ const lines = [
893
+ `Capability adoption: ${plan.status}`,
894
+ `Capability: ${plan.capability.id}@${plan.capability.version} (${plan.capability.provider})`,
895
+ `Repository: ${plan.repository.repo}`,
896
+ `Remote writes: none`,
897
+ `Authorization: ${plan.authorization.planHash}`,
898
+ ];
899
+ for (const change of plan.reconciliation.changes) lines.push(` ${change.status} ${change.name}`);
900
+ if (plan.nextAction) lines.push(`Next: ${plan.nextAction}`);
901
+ return `${lines.join('\n')}\n`;
902
+ }
903
+
904
+ async function writeCapabilityPlan(path, plan) {
905
+ const target = resolve(root, path);
906
+ await mkdir(dirname(target), { recursive: true });
907
+ await writeTextAtomically(target, `${JSON.stringify(plan, null, 2)}\n`);
908
+ }
909
+
910
+ async function writeCapabilityEvidence(plan, status, extra = {}) {
911
+ const path = resolve(root, '.lighthouse/reconciliation', `${plan.capability.id}.json`);
912
+ await mkdir(dirname(path), { recursive: true });
913
+ const evidence = {
914
+ schemaVersion: 1,
915
+ capability: plan.capability,
916
+ catalogVersion: plan.catalogVersion,
917
+ provider: plan.repository,
918
+ desiredHash: plan.reconciliation.desiredHash,
919
+ observedHash: extra.observedHash ?? plan.reconciliation.observedHash,
920
+ targetHash: extra.targetHash ?? plan.reconciliation.targetHash,
921
+ operations: extra.operations ?? plan.reconciliation.operations,
922
+ operationPolicy: plan.reconciliation.operationPolicy ?? plan.operationPolicy ?? null,
923
+ blockedOperations: extra.blockedOperations ?? plan.reconciliation.blockedOperations ?? [],
924
+ conflicts: extra.conflicts ?? plan.reconciliation.changes.filter((change) => change.status === 'conflict'),
925
+ status,
926
+ verified: status === 'adopted',
927
+ };
928
+ await writeTextAtomically(path, `${JSON.stringify(evidence, null, 2)}\n`);
929
+ return path;
930
+ }
931
+
932
+ async function capabilities(args) {
933
+ const action = args[0];
934
+ const id = args.includes('--id') ? args[args.indexOf('--id') + 1] : args.find((arg) => arg.startsWith('--id='))?.slice(5) ?? 'labels';
935
+ const provider = args.includes('--provider') ? args[args.indexOf('--provider') + 1] : args.find((arg) => arg.startsWith('--provider='))?.slice(11) ?? 'github';
936
+ const repo = args.includes('--repo') ? args[args.indexOf('--repo') + 1] : args.find((arg) => arg.startsWith('--repo='))?.slice(7);
937
+ const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
938
+ const format = formatFromArgs(args);
939
+ const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
940
+ if (['select', 'decline'].includes(action)) {
941
+ const catalog = sdlcCapabilityCatalog();
942
+ if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
943
+ const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
944
+ if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
945
+ const state = await readUpgradeState();
946
+ if (!state) throw new Error('Cannot select a capability before Lighthouse ownership evidence exists; run lighthouse init or reconcile --apply.');
947
+ state.adoption = state.adoption ?? {};
948
+ const status = action === 'decline' ? 'declined' : 'pending-adoption';
949
+ state.adoption[id] = {
950
+ id,
951
+ status,
952
+ selection: capability.selection,
953
+ provider: capability.provider,
954
+ version: capability.version,
955
+ catalogVersion: catalog.package.version,
956
+ nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${id}.plan.json`,
957
+ };
958
+ await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
959
+ const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
960
+ if (format === 'json') console.log(JSON.stringify(result, null, 2)); else console.log(`Capability ${id}: ${status}.`);
961
+ return;
962
+ }
963
+ if (action === 'plan') {
964
+ if (!repo || !planFile) throw new Error('Usage: lighthouse capabilities plan --id <capability> --provider github --repo <owner/repo> --plan-file <path> [--input <capabilities.json>] [--format <text|json>]');
965
+ const catalog = sdlcCapabilityCatalog();
966
+ if (!catalog || !sdlcCapabilities || !sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before planning adoption.');
967
+ const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
968
+ if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
969
+ const declaration = inputPath ? JSON.parse(await readFile(resolve(root, inputPath), 'utf8')) : sdlcCapabilities.declarationFromCatalog(catalog, id);
970
+ const labels = await sdlcGithub.readGithubLabels(repo);
971
+ const reconciliation = sdlcReconcile.buildLabelReconciliationPlan(declaration, labels, await (async () => {
972
+ const path = resolve(root, '.lighthouse/reconciliation', `${id}.json`);
973
+ try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
974
+ })());
975
+ const plan = {
976
+ schemaVersion: 1,
977
+ status: reconciliation.status,
978
+ capability: { id: capability.id, provider: capability.provider, selection: capability.selection, version: capability.version },
979
+ catalogVersion: catalog.package.version,
980
+ operationPolicy: capability.operations,
981
+ repository: { provider, repo },
982
+ declaration,
983
+ reconciliation,
984
+ authorization: { required: true, planHash: '' },
985
+ nextAction: reconciliation.canApply ? `lighthouse capabilities apply --plan-file ${planFile} --authorize <plan-hash>` : 'Resolve the reported conflicts before authorizing this plan.',
986
+ };
987
+ plan.authorization.planHash = planHash(plan);
988
+ await writeCapabilityPlan(planFile, plan);
989
+ if (format === 'json') console.log(JSON.stringify(plan, null, 2)); else process.stdout.write(capabilityPlanText(plan));
990
+ if (!reconciliation.valid || !reconciliation.canApply) process.exitCode = 1;
991
+ return;
992
+ }
993
+ if (action === 'apply') {
994
+ const authorization = args.includes('--authorize') ? args[args.indexOf('--authorize') + 1] : args.find((arg) => arg.startsWith('--authorize='))?.slice(12);
995
+ if (!planFile || !authorization) throw new Error('Usage: lighthouse capabilities apply --plan-file <path> --authorize <plan-hash> [--format <text|json>]');
996
+ const plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8'));
997
+ if (authorization !== plan.authorization?.planHash || planHash(plan) !== authorization) throw new Error('Capability plan authorization does not match the plan file; generate a new plan.');
998
+ if (!plan.reconciliation?.canApply) throw new Error('Capability plan is blocked; resolve its conflicts and generate a new plan.');
999
+ if (!sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before applying adoption.');
1000
+ const current = await sdlcGithub.readGithubLabels(plan.repository.repo);
1001
+ let reconciliation = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, current, plan.reconciliation.record);
1002
+ const originalOperations = new Set(plan.reconciliation.operations.map((operation) => operation.name));
1003
+ const plannedTarget = new Map((plan.reconciliation.target ?? []).map((label) => [label.name, JSON.stringify(label)]));
1004
+ const outsidePlanDrift = current.some((label) => !plannedTarget.has(label.name) || (plannedTarget.has(label.name) && !originalOperations.has(label.name) && JSON.stringify(label) !== plannedTarget.get(label.name)));
1005
+ if (reconciliation.observedHash !== plan.reconciliation.observedHash && (outsidePlanDrift || reconciliation.operations.some((operation) => !originalOperations.has(operation.name)))) {
1006
+ throw new Error('GitHub labels changed outside this plan; refusing to apply. Generate a new plan.');
1007
+ }
1008
+ const completed = [];
1009
+ try {
1010
+ for (const operation of reconciliation.operations) {
1011
+ await sdlcGithub.createGithubLabel(plan.repository.repo, operation, { dryRun: false });
1012
+ completed.push(operation);
1013
+ }
1014
+ } catch (error) {
1015
+ await writeCapabilityEvidence(plan, 'failed', { operations: completed, observedHash: reconciliation.observedHash });
1016
+ throw new Error(`Capability adoption stopped after ${completed.length} operation(s): ${error.message}`);
1017
+ }
1018
+ const finalLabels = await sdlcGithub.readGithubLabels(plan.repository.repo);
1019
+ const final = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, finalLabels, plan.reconciliation.record);
1020
+ if (!final.valid || final.operations.length || final.changes.some((change) => change.status === 'conflict')) {
1021
+ await writeCapabilityEvidence(plan, 'blocked', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash, conflicts: final.changes.filter((change) => change.status === 'conflict') });
1022
+ throw new Error('Capability adoption did not reach the declared state; inspect the evidence and generate a new plan.');
1023
+ }
1024
+ const evidencePath = await writeCapabilityEvidence(plan, 'adopted', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash });
1025
+ const state = await readUpgradeState();
1026
+ if (state) {
1027
+ state.adoption = state.adoption ?? {};
1028
+ state.adoption[id] = { id, status: 'adopted', selection: plan.capability.selection, provider: plan.capability.provider, version: plan.capability.version, catalogVersion: plan.catalogVersion, nextAction: null, recordPath: '.lighthouse/reconciliation/labels.json' };
1029
+ await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
1030
+ }
1031
+ const result = { ...plan, status: 'adopted', evidencePath, reconciliation: final, applied: true };
1032
+ if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
1033
+ return;
1034
+ }
1035
+ throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
1036
+ }
1037
+
799
1038
  function formatFromArgs(args) {
800
1039
  const index = args.indexOf('--format');
801
1040
  if (index !== -1) return args[index + 1];
@@ -808,17 +1047,18 @@ const commandArgs = args.slice(1);
808
1047
  let invalidArgument = false;
809
1048
  for (let index = 0; index < commandArgs.length; index += 1) {
810
1049
  const arg = commandArgs[index];
1050
+ if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
811
1051
  if (arg === '--dry-run') continue;
812
1052
  if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
813
1053
  if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
814
1054
  if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
815
1055
  if (arg === '--format') {
816
- if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1056
+ if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
817
1057
  else index += 1;
818
1058
  continue;
819
1059
  }
820
1060
  if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
821
- if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
1061
+ if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
822
1062
  continue;
823
1063
  }
824
1064
  if (arg === '--scope') {
@@ -830,9 +1070,15 @@ for (let index = 0; index < commandArgs.length; index += 1) {
830
1070
  if (command !== 'upgrade') invalidArgument = true;
831
1071
  continue;
832
1072
  }
1073
+ if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
1074
+ if (!commandArgs[index + 1]) invalidArgument = true;
1075
+ else index += 1;
1076
+ continue;
1077
+ }
1078
+ if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
833
1079
  invalidArgument = true;
834
1080
  }
835
- if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
1081
+ if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
836
1082
  console.error(usage);
837
1083
  process.exit(2);
838
1084
  }
@@ -841,6 +1087,7 @@ try {
841
1087
  else if (command === 'authorities') await authorities(args.slice(1));
842
1088
  else if (command === 'reconcile') await reconcile(args.slice(1));
843
1089
  else if (command === 'upgrade') await upgrade(args.slice(1));
1090
+ else if (command === 'capabilities') await capabilities(args.slice(1));
844
1091
  else {
845
1092
  const manifest = await readManifest();
846
1093
  if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
@@ -0,0 +1,7 @@
1
+ # Lighthouse 0.4.0
2
+
3
+ This release discovers the installed SDLC capability catalog during `lighthouse upgrade` and records pending adoption in `.lighthouse/upgrade.json`.
4
+
5
+ The upgrade remains local and dry-run by default. Review the plan, generate a provider plan with `lighthouse capabilities plan`, then authorize its application explicitly with the returned plan hash.
6
+
7
+ No package installation, local upgrade or implicit hook writes to GitHub.