@juangcarmona/lighthouse 0.7.0 → 0.8.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 +7 -4
- package/assets/command-aliases.json +11 -0
- package/assets/schemas/command-aliases.schema.json +35 -0
- package/assets/schemas/init-plan.schema.json +40 -0
- package/assets/skills/init/SKILL.md +30 -0
- package/package.json +5 -3
- package/src/aliases.mjs +56 -0
- package/src/create-dotnet.mjs +5 -1
- package/src/init-plan.mjs +62 -0
- package/src/lighthouse.mjs +397 -13
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
|
|
|
6
6
|
|
|
7
7
|
Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Add `--profile visual-theme` when the project wants the Lighthouse UI contract. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
|
|
8
8
|
|
|
9
|
+
When the request is expressed in product terms, use the projected `.agents/skills/lighthouse-init/SKILL.md` to prepare a JSON plan that follows `assets/schemas/init-plan.schema.json`. A reference project is optional; the agent asks only the questions needed to resolve the application shape, stacks and profiles. `lighthouse init --plan-file <path>` previews the local operations by default, `--format json` emits the machine-readable plan and `--apply` is required to write it. The CLI does not inspect or copy a reference and performs no remote writes.
|
|
10
|
+
|
|
9
11
|
For a Lighthouse-owned .NET backend, run `lighthouse create-dotnet <empty-target> --name <PascalCaseProduct>`. The creator seeds the modular backend, Aspire AppHost, CQRS/analyzer sources, tests, profile guidance and deterministic verifier into a new or empty directory. It refuses populated targets and never contacts a remote repository. Run `dotnet restore <Product>.slnx`, then `npm run verify` in the generated product.
|
|
10
12
|
|
|
11
13
|
The manifest can declare `application.shape` and `application.stacks` when the project has enough information to select compatible profiles. `lighthouse profiles --format json` reports selected profiles, applicability, owned surfaces and verification layers without changing the repository.
|
|
@@ -16,7 +18,7 @@ Use `lighthouse upgrade` to preview a versioned foundation update. The command i
|
|
|
16
18
|
|
|
17
19
|
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, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks; the .NET creator is the packaged entry point for the selected backend profile.
|
|
18
20
|
|
|
19
|
-
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.
|
|
21
|
+
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. In an existing repository, run `lighthouse authorities --adopt-existing` to validate the current ProductShape and ArcShape surfaces without running either initializer; review the dry-run plan and add `--apply` only to record both exact versions in Lighthouse-owned configuration.
|
|
20
22
|
|
|
21
23
|
Use `lighthouse authorities` to prepare the two baseline authorities through their own CLIs. The command previews both plans by default; `--apply` runs ProductShape's full model initialization and ArcShape's architecture initialization without installing agent capabilities. Provider-owned files remain owned by the provider, and Lighthouse records the exact adopted versions in `lighthouse.json` only after both applications succeed.
|
|
22
24
|
|
|
@@ -37,12 +39,13 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
|
|
|
37
39
|
1. Inventory the current source layout, tools, managed files and CI before editing.
|
|
38
40
|
2. Map existing files to the selected Lighthouse contracts and identify collisions.
|
|
39
41
|
3. Adopt the manifest and verifier first; the initializer reports conflicts and preserves product-owned guidance and generated outputs.
|
|
40
|
-
4.
|
|
41
|
-
5.
|
|
42
|
+
4. If ProductShape or ArcShape already exists, run `lighthouse authorities --adopt-existing` and repair any provider validation result before applying it.
|
|
43
|
+
5. Add profiles one at a time and measure their checks before making a workflow status required.
|
|
44
|
+
6. Record each exception in the project-owned documentation and remove it when the product is ready.
|
|
42
45
|
|
|
43
46
|
## Current command scope
|
|
44
47
|
|
|
45
|
-
The command supports `init`, `create-dotnet`, `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.
|
|
48
|
+
The command supports `init`, `aliases`, `create-dotnet`, `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. `lighthouse aliases --format json` exposes the installed `/lh:*` aliases and their canonical `/lighthouse:*` commands. `lighthouse authorities --adopt-existing` validates existing ProductShape and ArcShape authorities without writing provider-owned files. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
|
|
46
49
|
|
|
47
50
|
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.
|
|
48
51
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schemas/command-aliases.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"contractVersion": 1,
|
|
5
|
+
"package": { "name": "@juangcarmona/lighthouse" },
|
|
6
|
+
"canonicalNamespace": "/lighthouse",
|
|
7
|
+
"aliasNamespace": "/lh",
|
|
8
|
+
"aliases": [
|
|
9
|
+
{ "alias": "/lh:init", "canonical": "/lighthouse:init", "asset": "skills/init/SKILL.md" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-command-aliases.schema.json",
|
|
4
|
+
"title": "Lighthouse command aliases",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "contractVersion", "package", "canonicalNamespace", "aliasNamespace", "aliases"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {"type": "string"},
|
|
10
|
+
"schemaVersion": {"const": 1},
|
|
11
|
+
"contractVersion": {"const": 1},
|
|
12
|
+
"package": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["name"],
|
|
16
|
+
"properties": {"name": {"const": "@juangcarmona/lighthouse"}}
|
|
17
|
+
},
|
|
18
|
+
"canonicalNamespace": {"const": "/lighthouse"},
|
|
19
|
+
"aliasNamespace": {"const": "/lh"},
|
|
20
|
+
"aliases": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["alias", "canonical", "asset"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"alias": {"type": "string", "pattern": "^/lh:[a-z][a-z0-9-]*$"},
|
|
29
|
+
"canonical": {"type": "string", "pattern": "^/lighthouse:[a-z][a-z0-9-]*$"},
|
|
30
|
+
"asset": {"type": "string", "pattern": "^skills/[a-z0-9-]+/SKILL\\.md$"}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/init-plan.schema.json",
|
|
4
|
+
"title": "Lighthouse semantic initialization plan",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "product", "intent", "application", "profiles", "openQuestions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {"type": "string"},
|
|
10
|
+
"schemaVersion": {"const": 1},
|
|
11
|
+
"product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
|
|
12
|
+
"intent": {"type": "string", "minLength": 1},
|
|
13
|
+
"application": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["shape", "stacks"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"shape": {"enum": ["unknown", "frontend", "backend", "full-stack", "service", "library", "tooling", "cli", "worker", "mobile"]},
|
|
19
|
+
"stacks": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"profiles": {"type": "array", "items": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"}, "uniqueItems": true},
|
|
23
|
+
"reference": {
|
|
24
|
+
"oneOf": [
|
|
25
|
+
{"type": "null"},
|
|
26
|
+
{
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["kind"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"kind": {"enum": ["none", "local", "repository"]},
|
|
32
|
+
"location": {"type": "string", "minLength": 1},
|
|
33
|
+
"purpose": {"type": "string", "minLength": 1}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"openQuestions": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lighthouse-init
|
|
3
|
+
description: Turn a natural-language project request into a reviewable Lighthouse initialization plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lighthouse semantic initialization
|
|
7
|
+
|
|
8
|
+
Use this skill when an adopter describes a project in product terms and wants Lighthouse to prepare its foundation.
|
|
9
|
+
|
|
10
|
+
## Inputs
|
|
11
|
+
|
|
12
|
+
1. Read the installed profile catalog and the repository's current Lighthouse manifest when one exists.
|
|
13
|
+
2. Treat a reference project as optional. If the adopter names one, record its location and purpose; do not assume it exists or copy it.
|
|
14
|
+
3. Ask only the questions needed to resolve the application shape, stacks, profiles and material profile combinations. Keep unresolved questions in the plan instead of guessing.
|
|
15
|
+
|
|
16
|
+
## Plan
|
|
17
|
+
|
|
18
|
+
Write a JSON plan that validates against `assets/schemas/init-plan.schema.json` with `schemaVersion: 1`, `product`, `intent`, `application`, `profiles`, optional `reference` metadata and `openQuestions`.
|
|
19
|
+
|
|
20
|
+
Select profiles from the installed catalog. Do not invent a profile, make a stack mandatory, or turn a reference into a requirement. An absent reference is represented by an omitted `reference` field or `{"kind":"none"}`.
|
|
21
|
+
|
|
22
|
+
## Apply
|
|
23
|
+
|
|
24
|
+
Review the generated plan with the adopter, then run `lighthouse init --plan-file <path>` for a dry run. Use `--format json` for agent consumption. Apply only after the adopter explicitly authorizes `lighthouse init --plan-file <path> --apply`.
|
|
25
|
+
|
|
26
|
+
The CLI owns deterministic validation, profile compatibility, conflict detection and local writes. It does not inspect or copy the reference and it performs no remote writes. A plan with `openQuestions` is not ready to apply.
|
|
27
|
+
|
|
28
|
+
Use `lighthouse aliases --format json` to discover the installed `/lh:*` aliases and their canonical `/lighthouse:*` commands. The generated `.lighthouse/commands.json` is the host-readable projection; aliases reference the package-owned command or skill asset and never create a second implementation.
|
|
29
|
+
|
|
30
|
+
After applying, run `lighthouse verify` and report the created, preserved and conflicting paths. Keep product and architecture work in ProductShape and ArcShape's own routes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"node": ">=22"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@arcshape/cli": "0.1.
|
|
23
|
+
"@arcshape/cli": "0.1.4",
|
|
24
24
|
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
25
25
|
"@prodshape/cli": "0.22.0",
|
|
26
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
26
|
+
"@juangcarmona/lighthouse-sdlc": "0.7.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/lighthouse.mjs",
|
|
30
|
+
"src/aliases.mjs",
|
|
31
|
+
"src/init-plan.mjs",
|
|
30
32
|
"src/guardrails.mjs",
|
|
31
33
|
"assets",
|
|
32
34
|
"README.md",
|
package/src/aliases.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const contractPath = resolve(packageRoot, 'assets/command-aliases.json');
|
|
8
|
+
const packageName = '@juangcarmona/lighthouse';
|
|
9
|
+
const aliasPattern = /^\/lh:[a-z][a-z0-9-]*$/u;
|
|
10
|
+
const canonicalPattern = /^\/lighthouse:[a-z][a-z0-9-]*$/u;
|
|
11
|
+
const assetPattern = /^skills\/[a-z0-9-]+\/SKILL\.md$/u;
|
|
12
|
+
|
|
13
|
+
async function exists(path) {
|
|
14
|
+
try { await access(path, constants.F_OK); return true; } catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readCommandAliases(path = contractPath) {
|
|
18
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateCommandAliases(contract) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) return [{ code: 'aliases.invalid', message: 'command alias contract must be an object' }];
|
|
24
|
+
if (contract.schemaVersion !== 1) errors.push({ code: 'aliases.schema-version', message: 'command alias schemaVersion must be 1' });
|
|
25
|
+
if (contract.contractVersion !== 1) errors.push({ code: 'aliases.contract-version', message: 'command alias contractVersion must be 1' });
|
|
26
|
+
if (contract.package?.name !== packageName) errors.push({ code: 'aliases.package', message: `command alias contract package must be ${packageName}` });
|
|
27
|
+
if (contract.canonicalNamespace !== '/lighthouse') errors.push({ code: 'aliases.canonical-namespace', message: 'canonical namespace must be /lighthouse' });
|
|
28
|
+
if (contract.aliasNamespace !== '/lh') errors.push({ code: 'aliases.alias-namespace', message: 'alias namespace must be /lh' });
|
|
29
|
+
if (!Array.isArray(contract.aliases) || contract.aliases.length === 0) return [...errors, { code: 'aliases.list', message: 'command alias contract must declare at least one alias' }];
|
|
30
|
+
const aliases = new Set();
|
|
31
|
+
const canonicals = new Set();
|
|
32
|
+
for (const [index, item] of contract.aliases.entries()) {
|
|
33
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
34
|
+
errors.push({ code: 'aliases.entry', message: `alias ${index} must be an object` });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (typeof item.alias !== 'string' || !aliasPattern.test(item.alias)) errors.push({ code: 'aliases.alias', message: `alias ${index} must use the /lh:<verb> form` });
|
|
38
|
+
if (typeof item.canonical !== 'string' || !canonicalPattern.test(item.canonical)) errors.push({ code: 'aliases.canonical', message: `alias ${index} must target a /lighthouse:<verb> command` });
|
|
39
|
+
if (typeof item.asset !== 'string' || !assetPattern.test(item.asset)) errors.push({ code: 'aliases.asset', message: `alias ${index} must reference a packaged skill asset` });
|
|
40
|
+
if (aliases.has(item.alias)) errors.push({ code: 'aliases.duplicate', message: `alias ${item.alias} is declared more than once` });
|
|
41
|
+
if (canonicals.has(item.canonical)) errors.push({ code: 'aliases.duplicate-target', message: `canonical command ${item.canonical} has more than one alias` });
|
|
42
|
+
aliases.add(item.alias);
|
|
43
|
+
canonicals.add(item.canonical);
|
|
44
|
+
}
|
|
45
|
+
return errors;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function verifyCommandAliases(contract = readCommandAliases()) {
|
|
49
|
+
const errors = validateCommandAliases(contract);
|
|
50
|
+
for (const item of contract.aliases ?? []) {
|
|
51
|
+
if (typeof item.asset === 'string' && !(await exists(resolve(packageRoot, 'assets', item.asset)))) errors.push({ code: 'aliases.missing-asset', message: `alias ${item.alias} references missing asset ${item.asset}` });
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { contractPath };
|
package/src/create-dotnet.mjs
CHANGED
|
@@ -74,19 +74,23 @@ try {
|
|
|
74
74
|
await writeFile(solutionFile, `<Solution>\n${projects.map((project) => ` ${project}`).join('\n')}\n</Solution>\n`);
|
|
75
75
|
|
|
76
76
|
await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
|
|
77
|
+
await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
|
|
78
|
+
await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
|
|
77
79
|
await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
|
|
78
80
|
await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
|
|
79
81
|
await copy(resolve(packageRoot, 'src/verify-dotnet.mjs'), resolve(stage, 'scripts/verify-dotnet.mjs'));
|
|
80
82
|
await copy(resolve(packageRoot, 'assets/guardrails'), resolve(stage, 'assets/guardrails'));
|
|
81
83
|
await copy(resolve(packageRoot, 'assets/profiles'), resolve(stage, 'assets/profiles'));
|
|
82
84
|
await copy(resolve(packageRoot, 'assets/schemas'), resolve(stage, 'schemas'));
|
|
85
|
+
await copy(resolve(packageRoot, 'assets/command-aliases.json'), resolve(stage, 'assets/command-aliases.json'));
|
|
86
|
+
await copy(resolve(packageRoot, 'assets/skills/init'), resolve(stage, 'assets/skills/init'));
|
|
83
87
|
|
|
84
88
|
await writeFile(resolve(stage, 'lighthouse.json'), `${JSON.stringify({
|
|
85
89
|
$schema: './schemas/lighthouse-manifest.schema.json', schemaVersion: 1, baseline: '0.1.0',
|
|
86
90
|
product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
|
|
87
91
|
integrations: { productShape: null, arcShape: null },
|
|
88
92
|
packages: { sdlc: { name: '@juangcarmona/lighthouse-sdlc', version: sdlcContract.version } },
|
|
89
|
-
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.lighthouse/upgrade.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
|
|
93
|
+
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.agents/skills/lighthouse-init/SKILL.md', '.lighthouse/upgrade.json', '.lighthouse/commands.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
|
|
90
94
|
repository: {
|
|
91
95
|
layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
|
|
92
96
|
conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const productNamePattern = /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/u;
|
|
2
|
+
const shapes = new Set(['unknown', 'frontend', 'backend', 'full-stack', 'service', 'library', 'tooling', 'cli', 'worker', 'mobile']);
|
|
3
|
+
const referenceKinds = new Set(['none', 'local', 'repository']);
|
|
4
|
+
|
|
5
|
+
function isObject(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function uniqueStrings(value, name, { required = false } = {}) {
|
|
10
|
+
if (!Array.isArray(value) || (required && value.length === 0) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
11
|
+
return [`${name} must contain non-empty strings`];
|
|
12
|
+
}
|
|
13
|
+
return new Set(value).size === value.length ? [] : [`${name} must be unique`];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function referenceProblems(reference) {
|
|
17
|
+
if (reference === undefined || reference === null) return [];
|
|
18
|
+
if (!isObject(reference)) return ['reference must be null or an object'];
|
|
19
|
+
const problems = [];
|
|
20
|
+
if (!referenceKinds.has(reference.kind)) problems.push('reference.kind must be none, local or repository');
|
|
21
|
+
if (reference.kind !== 'none' && (typeof reference.location !== 'string' || reference.location.length === 0)) {
|
|
22
|
+
problems.push('reference.location is required for local and repository references');
|
|
23
|
+
}
|
|
24
|
+
if (reference.kind === 'none' && 'location' in reference) problems.push('reference.location is not allowed when reference.kind is none');
|
|
25
|
+
if ('purpose' in reference && (typeof reference.purpose !== 'string' || reference.purpose.length === 0)) problems.push('reference.purpose must be a non-empty string');
|
|
26
|
+
return problems;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateInitPlan(plan, supportedProfiles) {
|
|
30
|
+
if (!isObject(plan)) return ['plan must be an object'];
|
|
31
|
+
const problems = [];
|
|
32
|
+
const allowed = new Set(['$schema', 'schemaVersion', 'product', 'intent', 'application', 'profiles', 'reference', 'openQuestions']);
|
|
33
|
+
for (const key of Object.keys(plan)) if (!allowed.has(key)) problems.push(`unknown field ${key}`);
|
|
34
|
+
if (plan.schemaVersion !== 1) problems.push('schemaVersion must be 1');
|
|
35
|
+
if (typeof plan.product !== 'string' || !productNamePattern.test(plan.product)) problems.push('product must be a readable name of 1–64 characters');
|
|
36
|
+
if (typeof plan.intent !== 'string' || plan.intent.trim().length === 0) problems.push('intent must be a non-empty string');
|
|
37
|
+
if (!isObject(plan.application)) problems.push('application must be an object');
|
|
38
|
+
else {
|
|
39
|
+
if (!shapes.has(plan.application.shape)) problems.push(`application.shape must be one of ${[...shapes].join(', ')}`);
|
|
40
|
+
problems.push(...uniqueStrings(plan.application.stacks, 'application.stacks'));
|
|
41
|
+
}
|
|
42
|
+
problems.push(...uniqueStrings(plan.profiles, 'profiles'));
|
|
43
|
+
for (const profile of plan.profiles ?? []) {
|
|
44
|
+
if (!supportedProfiles.includes(profile)) problems.push(`unknown profile ${profile}; use ${supportedProfiles.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
problems.push(...referenceProblems(plan.reference));
|
|
47
|
+
problems.push(...uniqueStrings(plan.openQuestions, 'openQuestions'));
|
|
48
|
+
return problems;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hasUnresolvedQuestions(plan) {
|
|
52
|
+
return Array.isArray(plan.openQuestions) && plan.openQuestions.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function manifestFromInitPlan(plan, createManifest) {
|
|
56
|
+
const manifest = createManifest(plan.product, plan.profiles);
|
|
57
|
+
manifest.application = {
|
|
58
|
+
shape: plan.application.shape,
|
|
59
|
+
stacks: [...plan.application.stacks],
|
|
60
|
+
};
|
|
61
|
+
return manifest;
|
|
62
|
+
}
|
package/src/lighthouse.mjs
CHANGED
|
@@ -7,9 +7,12 @@ 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
9
|
import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
|
|
10
|
+
import { hasUnresolvedQuestions, manifestFromInitPlan, validateInitPlan } from './init-plan.mjs';
|
|
11
|
+
import { readCommandAliases, verifyCommandAliases } from './aliases.mjs';
|
|
10
12
|
let sdlcCapabilities;
|
|
11
13
|
let sdlcReconcile;
|
|
12
14
|
let sdlcGithub;
|
|
15
|
+
let sdlcAliases;
|
|
13
16
|
try {
|
|
14
17
|
[sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
|
|
15
18
|
import('@juangcarmona/lighthouse-sdlc/capabilities'),
|
|
@@ -17,6 +20,7 @@ try {
|
|
|
17
20
|
import('@juangcarmona/lighthouse-sdlc/github'),
|
|
18
21
|
]);
|
|
19
22
|
} catch { /* Standalone generated starters can run without the optional SDLC package. */ }
|
|
23
|
+
try { sdlcAliases = await import('@juangcarmona/lighthouse-sdlc/aliases'); } catch { /* Older standalone installations may not publish aliases. */ }
|
|
20
24
|
|
|
21
25
|
const root = process.cwd();
|
|
22
26
|
const packageRequire = createRequire(import.meta.url);
|
|
@@ -41,7 +45,9 @@ const managedPaths = [
|
|
|
41
45
|
'.husky/pre-push',
|
|
42
46
|
'.github/workflows/lighthouse.yml',
|
|
43
47
|
'.lighthouse/sdlc.json',
|
|
48
|
+
'.agents/skills/lighthouse-init/SKILL.md',
|
|
44
49
|
upgradeStateRelative,
|
|
50
|
+
'.lighthouse/commands.json',
|
|
45
51
|
];
|
|
46
52
|
const profileManagedPaths = Object.freeze({
|
|
47
53
|
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
@@ -54,11 +60,13 @@ const profileManagedPaths = Object.freeze({
|
|
|
54
60
|
});
|
|
55
61
|
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
56
62
|
const legacyManagedPaths = [projection];
|
|
63
|
+
const preSemanticManagedPaths = [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', upgradeStateRelative];
|
|
64
|
+
const preSemanticWithoutUpgradePaths = preSemanticManagedPaths.slice(0, -1);
|
|
57
65
|
const supportedProfiles = readProfileCatalog().profiles.map(({ id }) => id);
|
|
58
66
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
59
|
-
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.
|
|
67
|
+
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.4' };
|
|
60
68
|
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
61
|
-
const usage = 'Usage: lighthouse <init|create-dotnet|profiles|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
69
|
+
const usage = 'Usage: lighthouse <init|aliases|create-dotnet|profiles|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--plan-file <path>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--adopt-existing] [--dry-run] [--apply]';
|
|
62
70
|
|
|
63
71
|
const defaultRepository = () => ({
|
|
64
72
|
layout: [...layout],
|
|
@@ -73,8 +81,8 @@ function managedPathsFor(manifest) {
|
|
|
73
81
|
|
|
74
82
|
function upgradeScopesFor(manifest) {
|
|
75
83
|
return {
|
|
76
|
-
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
77
|
-
sdlc: ['.lighthouse/sdlc.json'],
|
|
84
|
+
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.agents/skills/lighthouse-init/SKILL.md', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
85
|
+
sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json'],
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -270,7 +278,7 @@ function render(manifest) {
|
|
|
270
278
|
const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
|
|
271
279
|
const selectedUi = manifest.packages?.ui;
|
|
272
280
|
const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
|
|
273
|
-
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}${uiLine}- 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`;
|
|
281
|
+
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}${uiLine}- 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, command aliases and host adapters without storing agent runtime data. Use \`lighthouse aliases --format json\` or \`.lighthouse/commands.json\` to discover canonical and short agent commands.\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`;
|
|
274
282
|
}
|
|
275
283
|
|
|
276
284
|
function guardrailScript(manifest) {
|
|
@@ -354,6 +362,59 @@ jobs:
|
|
|
354
362
|
`;
|
|
355
363
|
}
|
|
356
364
|
|
|
365
|
+
function readSdlcCommandAliases() {
|
|
366
|
+
try {
|
|
367
|
+
const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/command-aliases.json');
|
|
368
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
369
|
+
} catch { return null; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function commandAliasesProjection() {
|
|
373
|
+
const contracts = [{ contract: readCommandAliases(), packageVersion: cliPackage.version }];
|
|
374
|
+
const sdlc = readSdlcCommandAliases();
|
|
375
|
+
if (sdlc) contracts.push({ contract: sdlc, packageVersion: sdlcPackage.version });
|
|
376
|
+
const aliases = contracts.flatMap(({ contract, packageVersion }) => (contract.aliases ?? []).map((alias) => ({
|
|
377
|
+
...alias,
|
|
378
|
+
owner: contract.package.name,
|
|
379
|
+
packageVersion,
|
|
380
|
+
})));
|
|
381
|
+
return {
|
|
382
|
+
schemaVersion: 1,
|
|
383
|
+
contractVersion: 1,
|
|
384
|
+
canonicalNamespace: '/lighthouse',
|
|
385
|
+
aliasNamespace: '/lh',
|
|
386
|
+
packages: contracts.map(({ contract, packageVersion }) => ({
|
|
387
|
+
name: contract.package.name,
|
|
388
|
+
version: packageVersion,
|
|
389
|
+
contractVersion: contract.contractVersion,
|
|
390
|
+
aliases: contract.aliases,
|
|
391
|
+
})),
|
|
392
|
+
aliases,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function commandAliasProjectionText(projection) {
|
|
397
|
+
return `${JSON.stringify(projection, null, 2)}\n`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function commandAliases(format = 'text') {
|
|
401
|
+
const harness = readCommandAliases();
|
|
402
|
+
const errors = await verifyCommandAliases(harness);
|
|
403
|
+
const sdlc = readSdlcCommandAliases();
|
|
404
|
+
if (!sdlc) errors.push({ code: 'aliases.sdlc-unavailable', message: 'The installed SDLC package does not publish command aliases; upgrade it before using the SDLC alias namespace.' });
|
|
405
|
+
else if (sdlcAliases) errors.push(...(await sdlcAliases.verifyCommandAliases(sdlc)));
|
|
406
|
+
else errors.push({ code: 'aliases.sdlc-validator-unavailable', message: 'The installed SDLC package does not publish its alias validator.' });
|
|
407
|
+
const projection = commandAliasesProjection();
|
|
408
|
+
const report = { ...projection, status: errors.length ? 'invalid' : 'valid', valid: errors.length === 0, errors };
|
|
409
|
+
if (format === 'json') console.log(JSON.stringify(report, null, 2));
|
|
410
|
+
else {
|
|
411
|
+
console.log(`Lighthouse command aliases: ${report.status}`);
|
|
412
|
+
for (const alias of report.aliases) console.log(` ${alias.alias} -> ${alias.canonical}`);
|
|
413
|
+
for (const error of report.errors) console.error(` ${error.code}: ${error.message}`);
|
|
414
|
+
}
|
|
415
|
+
if (errors.length) process.exitCode = 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
357
418
|
function managedFiles(manifest) {
|
|
358
419
|
const files = {
|
|
359
420
|
[projection]: render(manifest),
|
|
@@ -361,10 +422,12 @@ function managedFiles(manifest) {
|
|
|
361
422
|
'.husky/commit-msg': commitHook(),
|
|
362
423
|
'.husky/pre-push': prePushHook(),
|
|
363
424
|
'.github/workflows/lighthouse.yml': workflow(),
|
|
425
|
+
'.agents/skills/lighthouse-init/SKILL.md': readFileSync(new URL('../assets/skills/init/SKILL.md', import.meta.url), 'utf8'),
|
|
364
426
|
};
|
|
365
427
|
Object.assign(files, profileAssets(manifest));
|
|
366
428
|
const controlPlane = sdlcControlPlane(manifest);
|
|
367
429
|
if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
|
|
430
|
+
files['.lighthouse/commands.json'] = commandAliasProjectionText(commandAliasesProjection());
|
|
368
431
|
return files;
|
|
369
432
|
}
|
|
370
433
|
|
|
@@ -586,7 +649,13 @@ function validate(manifest) {
|
|
|
586
649
|
const previousCompleteManaged = Array.isArray(manifest.managed)
|
|
587
650
|
&& manifest.managed.length === previousManagedPaths.length
|
|
588
651
|
&& manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
|
|
589
|
-
|
|
652
|
+
const preSemanticManaged = Array.isArray(manifest.managed)
|
|
653
|
+
&& manifest.managed.length === preSemanticManagedPaths.length
|
|
654
|
+
&& manifest.managed.every((entry, index) => entry === preSemanticManagedPaths[index]);
|
|
655
|
+
const preSemanticWithoutUpgrade = Array.isArray(manifest.managed)
|
|
656
|
+
&& manifest.managed.length === preSemanticWithoutUpgradePaths.length
|
|
657
|
+
&& manifest.managed.every((entry, index) => entry === preSemanticWithoutUpgradePaths[index]);
|
|
658
|
+
if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
|
|
590
659
|
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
|
|
591
660
|
}
|
|
592
661
|
if ('repository' in manifest) {
|
|
@@ -637,6 +706,103 @@ function productNameFromArgs(args) {
|
|
|
637
706
|
return value;
|
|
638
707
|
}
|
|
639
708
|
|
|
709
|
+
function valueFromArgs(args, name) {
|
|
710
|
+
const index = args.indexOf(name);
|
|
711
|
+
if (index !== -1) return args[index + 1];
|
|
712
|
+
return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function readInitPlan(planFile) {
|
|
716
|
+
let plan;
|
|
717
|
+
try { plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8')); }
|
|
718
|
+
catch (error) { throw new Error(`Cannot read initialization plan ${planFile}: ${error.message}`); }
|
|
719
|
+
const problems = validateInitPlan(plan, supportedProfiles);
|
|
720
|
+
if (problems.length) throw new Error(`Invalid initialization plan:\n${problems.map((problem) => ` ${problem}`).join('\n')}`);
|
|
721
|
+
return plan;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function printInitializationPlan(plan, result, format) {
|
|
725
|
+
const report = {
|
|
726
|
+
schemaVersion: 1,
|
|
727
|
+
intent: plan.intent,
|
|
728
|
+
product: plan.product,
|
|
729
|
+
application: plan.application,
|
|
730
|
+
profiles: plan.profiles,
|
|
731
|
+
reference: plan.reference ?? null,
|
|
732
|
+
openQuestions: plan.openQuestions,
|
|
733
|
+
status: result.plan.conflicts.length || plan.openQuestions.length ? 'blocked' : 'ready',
|
|
734
|
+
manifest: {
|
|
735
|
+
product: result.manifest.product,
|
|
736
|
+
application: result.manifest.application,
|
|
737
|
+
profiles: result.manifest.profiles,
|
|
738
|
+
},
|
|
739
|
+
operations: result.plan.operations.map(({ path, action }) => ({ path, action })),
|
|
740
|
+
conflicts: result.plan.conflicts,
|
|
741
|
+
apply: 'lighthouse init --plan-file <path> --apply',
|
|
742
|
+
};
|
|
743
|
+
if (format === 'json') {
|
|
744
|
+
console.log(JSON.stringify(report, null, 2));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
console.log('Lighthouse semantic initialization plan:');
|
|
748
|
+
console.log(` product ${plan.product}`);
|
|
749
|
+
console.log(` intent ${plan.intent}`);
|
|
750
|
+
console.log(` application ${plan.application.shape} (${plan.application.stacks.join(', ') || 'no stack'})`);
|
|
751
|
+
console.log(` profiles ${plan.profiles.join(', ') || 'none'}`);
|
|
752
|
+
console.log(` reference ${plan.reference?.kind ?? 'none'}`);
|
|
753
|
+
if (plan.openQuestions.length) for (const question of plan.openQuestions) console.error(` question ${question}`);
|
|
754
|
+
for (const operation of result.plan.operations) console.log(` ${operation.action} ${operation.path}`);
|
|
755
|
+
for (const conflict of result.plan.conflicts) console.error(` conflict ${conflict}`);
|
|
756
|
+
console.log(' default is dry-run; apply only after review with --apply');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function initializeFromPlan(args, planFile) {
|
|
760
|
+
const plan = await readInitPlan(planFile);
|
|
761
|
+
const format = formatFromArgs(args);
|
|
762
|
+
const apply = args.includes('--apply');
|
|
763
|
+
const dryRun = args.includes('--dry-run') || !apply;
|
|
764
|
+
if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
|
|
765
|
+
if (format === 'json' && apply) throw new Error('--format json is available for semantic initialization dry runs; omit it when applying.');
|
|
766
|
+
const existing = await readManifest();
|
|
767
|
+
let manifest;
|
|
768
|
+
if (existing) {
|
|
769
|
+
const problems = validate(existing);
|
|
770
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
771
|
+
if (existing.product !== plan.product) throw new Error(`Initialization plan targets ${plan.product}, but lighthouse.json belongs to ${existing.product}.`);
|
|
772
|
+
if (JSON.stringify(existing.profiles ?? []) !== JSON.stringify(plan.profiles)) throw new Error('Initialization plan profiles differ from lighthouse.json; resolve the selection before applying.');
|
|
773
|
+
if (JSON.stringify(normalizeApplication(existing.application)) !== JSON.stringify(plan.application)) throw new Error('Initialization plan application differs from lighthouse.json; resolve the selection before applying.');
|
|
774
|
+
manifest = existing;
|
|
775
|
+
} else {
|
|
776
|
+
manifest = manifestFromInitPlan(plan, defaultManifest);
|
|
777
|
+
}
|
|
778
|
+
const problems = validate(manifest);
|
|
779
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
780
|
+
const composition = composeProfiles(readProfileCatalog(), manifest);
|
|
781
|
+
if (composition.status === 'failed') throw new Error(`Initialization plan has incompatible profiles:\n${composition.selected.flatMap((profile) => profile.problems).map((problem) => ` ${problem}`).join('\n')}`);
|
|
782
|
+
const initialization = { manifest, plan: await planInitialization(manifest, !existing) };
|
|
783
|
+
printInitializationPlan(plan, initialization, format);
|
|
784
|
+
if (initialization.plan.conflicts.length) {
|
|
785
|
+
if (dryRun) process.exitCode = 1;
|
|
786
|
+
else throw new Error('Initialization stopped; resolve the reported conflicts and run the plan again.');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (hasUnresolvedQuestions(plan)) {
|
|
790
|
+
if (dryRun) process.exitCode = 1;
|
|
791
|
+
else throw new Error('Initialization stopped; answer the open questions before using --apply.');
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (dryRun) return;
|
|
795
|
+
if (!existing) await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
796
|
+
for (const operation of initialization.plan.operations.filter((item) => item.path.endsWith('/'))) await mkdir(resolve(root, operation.path), { recursive: true });
|
|
797
|
+
const files = managedFiles(manifest);
|
|
798
|
+
for (const [file, content] of Object.entries(files)) {
|
|
799
|
+
await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
|
|
800
|
+
if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
|
|
801
|
+
}
|
|
802
|
+
await writeUpgradeState(manifest, null, files);
|
|
803
|
+
console.log(`Initialized ${manifest.product} from the semantic plan. Run lighthouse verify.`);
|
|
804
|
+
}
|
|
805
|
+
|
|
640
806
|
async function planInitialization(manifest, manifestMissing) {
|
|
641
807
|
const operations = [];
|
|
642
808
|
const conflicts = [];
|
|
@@ -694,6 +860,8 @@ function printReconciliationPlan(plan) {
|
|
|
694
860
|
}
|
|
695
861
|
|
|
696
862
|
async function initialize(args) {
|
|
863
|
+
const planFile = valueFromArgs(args, '--plan-file');
|
|
864
|
+
if (planFile) return initializeFromPlan(args, planFile);
|
|
697
865
|
let manifest = await readManifest();
|
|
698
866
|
const manifestMissing = !manifest;
|
|
699
867
|
const selectedProfiles = profileArguments(args);
|
|
@@ -854,6 +1022,25 @@ const authorityCommands = [
|
|
|
854
1022
|
},
|
|
855
1023
|
];
|
|
856
1024
|
|
|
1025
|
+
const authorityAdoptionCommands = [
|
|
1026
|
+
{
|
|
1027
|
+
id: 'productShape',
|
|
1028
|
+
label: 'ProductShape',
|
|
1029
|
+
packageName: '@prodshape/cli',
|
|
1030
|
+
command: 'prodshape',
|
|
1031
|
+
path: 'docs/product/model',
|
|
1032
|
+
validateArgs: ['validate', '--format', 'json'],
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'arcShape',
|
|
1036
|
+
label: 'ArcShape',
|
|
1037
|
+
packageName: '@arcshape/cli',
|
|
1038
|
+
command: 'arcshape',
|
|
1039
|
+
path: 'docs/architecture',
|
|
1040
|
+
validateArgs: ['validate', '--docs', 'docs/architecture', '--links', '--citations', '--format', 'json'],
|
|
1041
|
+
},
|
|
1042
|
+
];
|
|
1043
|
+
|
|
857
1044
|
function providerExecutable(authority) {
|
|
858
1045
|
const packageJson = packageRequire.resolve(`${authority.packageName}/package.json`);
|
|
859
1046
|
const metadata = JSON.parse(readFileSync(packageJson, 'utf8'));
|
|
@@ -887,6 +1074,132 @@ function runAuthorityCommand(authority, dryRun) {
|
|
|
887
1074
|
};
|
|
888
1075
|
}
|
|
889
1076
|
|
|
1077
|
+
function parseProviderReport(output) {
|
|
1078
|
+
const text = output.trim();
|
|
1079
|
+
try { return JSON.parse(text); } catch { /* The provider may prefix JSON with a diagnostic line. */ }
|
|
1080
|
+
const start = text.indexOf('{');
|
|
1081
|
+
const end = text.lastIndexOf('}');
|
|
1082
|
+
if (start === -1 || end <= start) return null;
|
|
1083
|
+
try { return JSON.parse(text.slice(start, end + 1)); } catch { return null; }
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function authorityStatus(present, validation, recordedVersion, providerVersion) {
|
|
1087
|
+
if (!present) return 'absent';
|
|
1088
|
+
if (validation.status === 'unavailable') return 'unavailable';
|
|
1089
|
+
if (!validation.valid) return 'invalid';
|
|
1090
|
+
if (recordedVersion && recordedVersion !== providerVersion) return 'drifted';
|
|
1091
|
+
return recordedVersion ? 'adopted' : 'ready';
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async function runAuthorityValidation(authority, manifest) {
|
|
1095
|
+
const providerVersion = installedPackageVersion(authority.packageName, 'package.json');
|
|
1096
|
+
const present = await isDirectory(resolve(root, authority.path));
|
|
1097
|
+
const command = `${authority.command} ${authority.validateArgs.join(' ')}`;
|
|
1098
|
+
if (!present) {
|
|
1099
|
+
return {
|
|
1100
|
+
id: authority.id,
|
|
1101
|
+
label: authority.label,
|
|
1102
|
+
provider: authority.packageName,
|
|
1103
|
+
version: providerVersion,
|
|
1104
|
+
path: authority.path,
|
|
1105
|
+
command,
|
|
1106
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1107
|
+
status: 'absent',
|
|
1108
|
+
valid: false,
|
|
1109
|
+
diagnostics: [`${authority.path} is missing`],
|
|
1110
|
+
nextAction: `Create the ${authority.label} authority through its supported initializer, then rerun lighthouse authorities --adopt-existing.`,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
if (!providerVersion) {
|
|
1114
|
+
return {
|
|
1115
|
+
id: authority.id,
|
|
1116
|
+
label: authority.label,
|
|
1117
|
+
provider: authority.packageName,
|
|
1118
|
+
version: null,
|
|
1119
|
+
path: authority.path,
|
|
1120
|
+
command,
|
|
1121
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1122
|
+
status: 'unavailable',
|
|
1123
|
+
valid: false,
|
|
1124
|
+
diagnostics: [`${authority.packageName} is not installed`],
|
|
1125
|
+
nextAction: `Install ${authority.packageName}, then rerun lighthouse authorities --adopt-existing.`,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
let result;
|
|
1129
|
+
try {
|
|
1130
|
+
result = spawnSync(process.execPath, [providerExecutable(authority), ...authority.validateArgs], {
|
|
1131
|
+
cwd: root,
|
|
1132
|
+
encoding: 'utf8',
|
|
1133
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1134
|
+
});
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
return {
|
|
1137
|
+
id: authority.id,
|
|
1138
|
+
label: authority.label,
|
|
1139
|
+
provider: authority.packageName,
|
|
1140
|
+
version: providerVersion,
|
|
1141
|
+
path: authority.path,
|
|
1142
|
+
command,
|
|
1143
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1144
|
+
status: 'unavailable',
|
|
1145
|
+
valid: false,
|
|
1146
|
+
diagnostics: [error.message],
|
|
1147
|
+
nextAction: `Run ${command} directly, repair the provider and rerun lighthouse authorities --adopt-existing.`,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
1151
|
+
const report = parseProviderReport(output);
|
|
1152
|
+
const valid = result.status === 0 && report?.valid !== false;
|
|
1153
|
+
const status = authorityStatus(present, { status: result.status === null ? 'unavailable' : 'completed', valid }, manifest.integrations[authority.id], providerVersion);
|
|
1154
|
+
return {
|
|
1155
|
+
id: authority.id,
|
|
1156
|
+
label: authority.label,
|
|
1157
|
+
provider: authority.packageName,
|
|
1158
|
+
version: providerVersion,
|
|
1159
|
+
path: authority.path,
|
|
1160
|
+
command,
|
|
1161
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1162
|
+
status,
|
|
1163
|
+
valid,
|
|
1164
|
+
diagnostics: report?.diagnostics ?? (valid ? [] : [output.trim().split('\n').filter(Boolean).slice(-3).join(' ') || `${authority.label} validation failed`]),
|
|
1165
|
+
nextAction: status === 'invalid' ? `Run ${command} directly, repair the reported authority and rerun lighthouse authorities --adopt-existing.` : null,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function authorityAdoptionPlan(manifest) {
|
|
1170
|
+
const authorities = await Promise.all(authorityAdoptionCommands.map((authority) => runAuthorityValidation(authority, manifest)));
|
|
1171
|
+
const blocking = authorities.filter((authority) => ['absent', 'invalid', 'unavailable'].includes(authority.status));
|
|
1172
|
+
const changed = authorities.some((authority) => ['ready', 'drifted'].includes(authority.status));
|
|
1173
|
+
const status = blocking.length ? 'blocked' : changed ? 'ready' : 'adopted';
|
|
1174
|
+
return {
|
|
1175
|
+
schemaVersion: 1,
|
|
1176
|
+
command: 'lighthouse authorities --adopt-existing',
|
|
1177
|
+
status,
|
|
1178
|
+
remoteWrites: 'none',
|
|
1179
|
+
providerWrites: 'none',
|
|
1180
|
+
authorities,
|
|
1181
|
+
operations: changed ? [
|
|
1182
|
+
{ action: 'record', path: 'lighthouse.json', values: Object.fromEntries(authorities.map((authority) => [authority.id, authority.version])) },
|
|
1183
|
+
{ action: 'regenerate', path: 'LIGHTHOUSE.md' },
|
|
1184
|
+
] : [],
|
|
1185
|
+
apply: 'lighthouse authorities --adopt-existing --apply',
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function printAuthorityAdoptionPlan(plan) {
|
|
1190
|
+
console.log(`Lighthouse existing-authority adoption plan: ${plan.status}`);
|
|
1191
|
+
console.log(' ProductShape and ArcShape provider files remain untouched.');
|
|
1192
|
+
console.log(' Remote writes: none.');
|
|
1193
|
+
for (const authority of plan.authorities) {
|
|
1194
|
+
const version = authority.version ?? 'unavailable';
|
|
1195
|
+
console.log(` ${authority.label}: ${authority.status} (${version}) ${authority.path}`);
|
|
1196
|
+
for (const diagnostic of authority.diagnostics) console.error(` ${diagnostic}`);
|
|
1197
|
+
if (authority.nextAction) console.error(` next: ${authority.nextAction}`);
|
|
1198
|
+
}
|
|
1199
|
+
for (const operation of plan.operations) console.log(` ${operation.action} ${operation.path}`);
|
|
1200
|
+
console.log(' default is dry-run; apply only after review with --apply');
|
|
1201
|
+
}
|
|
1202
|
+
|
|
890
1203
|
async function writeManifestAtomically(manifest) {
|
|
891
1204
|
const temporary = `${manifestPath}.tmp-${process.pid}`;
|
|
892
1205
|
await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
@@ -907,7 +1220,7 @@ async function ensureProjectionReady(manifest) {
|
|
|
907
1220
|
|
|
908
1221
|
async function writeProjection(manifest) {
|
|
909
1222
|
const target = resolve(root, projection);
|
|
910
|
-
await
|
|
1223
|
+
await writeTextAtomically(target, render(manifest));
|
|
911
1224
|
}
|
|
912
1225
|
|
|
913
1226
|
async function authorities(args) {
|
|
@@ -945,6 +1258,37 @@ async function authorities(args) {
|
|
|
945
1258
|
console.log(`Recorded ProductShape ${authorityVersions.productShape} and ArcShape ${authorityVersions.arcShape} in lighthouse.json.`);
|
|
946
1259
|
}
|
|
947
1260
|
|
|
1261
|
+
async function adoptExistingAuthorities(args) {
|
|
1262
|
+
const manifest = await readManifest();
|
|
1263
|
+
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
1264
|
+
const problems = validate(manifest);
|
|
1265
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
1266
|
+
const apply = args.includes('--apply');
|
|
1267
|
+
const dryRun = args.includes('--dry-run') || !apply;
|
|
1268
|
+
const format = formatFromArgs(args);
|
|
1269
|
+
if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
|
|
1270
|
+
if (format === 'json' && apply) throw new Error('--format json is available for existing-authority adoption dry runs; omit it when applying.');
|
|
1271
|
+
const plan = await authorityAdoptionPlan(manifest);
|
|
1272
|
+
if (format === 'json') console.log(JSON.stringify(plan, null, 2));
|
|
1273
|
+
else printAuthorityAdoptionPlan(plan);
|
|
1274
|
+
if (dryRun) {
|
|
1275
|
+
if (plan.status === 'blocked') process.exitCode = 1;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (plan.status === 'blocked') throw new Error('Existing-authority adoption stopped; resolve the reported provider conditions and rerun the plan.');
|
|
1279
|
+
await ensureProjectionReady(manifest);
|
|
1280
|
+
const updated = {
|
|
1281
|
+
...manifest,
|
|
1282
|
+
integrations: Object.fromEntries(plan.authorities.map((authority) => [authority.id, authority.version])),
|
|
1283
|
+
};
|
|
1284
|
+
if (JSON.stringify(updated) !== JSON.stringify(manifest)) await writeManifestAtomically(updated);
|
|
1285
|
+
if (!await exists(resolve(root, projection))) await writeProjection(updated);
|
|
1286
|
+
else if (await readFile(resolve(root, projection), 'utf8') !== render(updated)) await writeProjection(updated);
|
|
1287
|
+
const verified = await authorityAdoptionPlan(updated);
|
|
1288
|
+
if (verified.status !== 'adopted') throw new Error('Existing-authority adoption was written but did not verify; rerun lighthouse authorities --adopt-existing and inspect the provider report.');
|
|
1289
|
+
console.log('Recorded and verified the existing ProductShape and ArcShape authorities. Provider-owned files were not changed.');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
948
1292
|
function gitBranch() {
|
|
949
1293
|
try {
|
|
950
1294
|
return execFileSync('git', ['branch', '--show-current'], {
|
|
@@ -994,6 +1338,40 @@ async function verify(manifest, format = 'text') {
|
|
|
994
1338
|
const profileResult = profileCompositionResult(profileComposition);
|
|
995
1339
|
report.results.push(profileResult);
|
|
996
1340
|
report.summary[profileResult.status] = (report.summary[profileResult.status] ?? 0) + 1;
|
|
1341
|
+
if (validManifest) {
|
|
1342
|
+
const recordedAuthorities = [manifest.integrations.productShape, manifest.integrations.arcShape];
|
|
1343
|
+
const anyRecorded = recordedAuthorities.some((version) => version !== null);
|
|
1344
|
+
const authorityPlan = anyRecorded ? await authorityAdoptionPlan(manifest) : null;
|
|
1345
|
+
const authorityStatuses = authorityPlan?.authorities.map((authority) => authority.status) ?? [];
|
|
1346
|
+
const authorityStatus = !anyRecorded ? 'unconfigured'
|
|
1347
|
+
: authorityStatuses.includes('unavailable') ? 'unavailable'
|
|
1348
|
+
: authorityStatuses.some((status) => ['absent', 'invalid'].includes(status)) ? 'failed'
|
|
1349
|
+
: authorityStatuses.includes('drifted') ? 'drift'
|
|
1350
|
+
: 'passed';
|
|
1351
|
+
const authorityProblems = !anyRecorded
|
|
1352
|
+
? ['ProductShape and ArcShape are not recorded; run lighthouse authorities --adopt-existing after reviewing its dry-run plan.']
|
|
1353
|
+
: authorityPlan.authorities.filter((authority) => authority.status !== 'adopted').flatMap((authority) => [
|
|
1354
|
+
`${authority.label} is ${authority.status}${authority.diagnostics.length ? `: ${authority.diagnostics.join('; ')}` : ''}`,
|
|
1355
|
+
...(authority.nextAction ? [authority.nextAction] : []),
|
|
1356
|
+
]);
|
|
1357
|
+
const authorityResult = {
|
|
1358
|
+
id: 'foundation.authorities',
|
|
1359
|
+
owner: 'lighthouse',
|
|
1360
|
+
level: anyRecorded ? 'required' : 'recommended',
|
|
1361
|
+
kind: 'hard',
|
|
1362
|
+
verification: 'deterministic',
|
|
1363
|
+
status: authorityStatus,
|
|
1364
|
+
evidence: authorityPlan?.authorities.map((authority) => ({ id: authority.id, path: authority.path, version: authority.version, recordedVersion: authority.recordedVersion, command: authority.command })) ?? [],
|
|
1365
|
+
problems: authorityProblems,
|
|
1366
|
+
nextAction: authorityProblems.length ? 'lighthouse authorities --adopt-existing' : null,
|
|
1367
|
+
};
|
|
1368
|
+
report.results.push(authorityResult);
|
|
1369
|
+
report.summary[authorityStatus] = (report.summary[authorityStatus] ?? 0) + 1;
|
|
1370
|
+
if (authorityResult.level === 'required' && !['passed', 'not-applicable'].includes(authorityStatus)) {
|
|
1371
|
+
report.blocking.push(authorityResult.id);
|
|
1372
|
+
report.problems.push(...authorityProblems);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
997
1375
|
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
998
1376
|
for (const item of adoption) {
|
|
999
1377
|
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
@@ -1212,7 +1590,8 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1212
1590
|
const arg = commandArgs[index];
|
|
1213
1591
|
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
1214
1592
|
if (arg === '--dry-run') continue;
|
|
1215
|
-
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1593
|
+
if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1594
|
+
if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }
|
|
1216
1595
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
1217
1596
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
1218
1597
|
if (arg === '--profile') {
|
|
@@ -1225,12 +1604,12 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1225
1604
|
continue;
|
|
1226
1605
|
}
|
|
1227
1606
|
if (arg === '--format') {
|
|
1228
|
-
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1607
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1229
1608
|
else index += 1;
|
|
1230
1609
|
continue;
|
|
1231
1610
|
}
|
|
1232
1611
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
1233
|
-
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1612
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1234
1613
|
continue;
|
|
1235
1614
|
}
|
|
1236
1615
|
if (arg === '--scope') {
|
|
@@ -1243,19 +1622,24 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1243
1622
|
continue;
|
|
1244
1623
|
}
|
|
1245
1624
|
if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
|
|
1246
|
-
if (!commandArgs[index + 1]) invalidArgument = true;
|
|
1625
|
+
if (!commandArgs[index + 1] || (arg === '--plan-file' && !['init', 'capabilities'].includes(command))) invalidArgument = true;
|
|
1247
1626
|
else index += 1;
|
|
1248
1627
|
continue;
|
|
1249
1628
|
}
|
|
1250
|
-
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length))
|
|
1629
|
+
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) {
|
|
1630
|
+
if (arg.startsWith('--plan-file=') && !['init', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1251
1633
|
invalidArgument = true;
|
|
1252
1634
|
}
|
|
1253
|
-
if (!['init', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1635
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1254
1636
|
console.error(usage);
|
|
1255
1637
|
process.exit(2);
|
|
1256
1638
|
}
|
|
1257
1639
|
try {
|
|
1258
1640
|
if (command === 'init') await initialize(args.slice(1));
|
|
1641
|
+
else if (command === 'aliases') await commandAliases(formatFromArgs(commandArgs));
|
|
1642
|
+
else if (command === 'authorities' && commandArgs.includes('--adopt-existing')) await adoptExistingAuthorities(args.slice(1));
|
|
1259
1643
|
else if (command === 'authorities') await authorities(args.slice(1));
|
|
1260
1644
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
1261
1645
|
else if (command === 'upgrade') await upgrade(args.slice(1));
|