@juangcarmona/lighthouse 0.4.0 → 0.5.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 +2 -2
- package/assets/guardrails/catalog.json +10 -0
- package/assets/schemas/lighthouse-manifest.schema.json +3 -2
- package/package.json +2 -1
- package/src/guardrails.mjs +17 -2
- package/src/lighthouse.mjs +102 -20
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
|
|
|
4
4
|
|
|
5
5
|
## Initialize a repository
|
|
6
6
|
|
|
7
|
-
Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. 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.
|
|
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
9
|
Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an existing adoption. It does not write by default; add `--apply` only after reviewing the plan. Existing files whose content differs from the generated projection are conflicts and are never overwritten by reconciliation.
|
|
10
10
|
|
|
11
11
|
Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
|
|
12
12
|
|
|
13
|
-
The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes
|
|
13
|
+
The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes, `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.
|
|
14
14
|
|
|
15
15
|
ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration.
|
|
16
16
|
|
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
"verification": "deterministic",
|
|
42
42
|
"check": "branch",
|
|
43
43
|
"failureHint": "Use the branch pattern declared in lighthouse.json."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "visual.design-contract",
|
|
47
|
+
"owner": "@juangcarmona/lighthouse-ui",
|
|
48
|
+
"appliesTo": { "baseline": false, "profiles": ["visual-theme"] },
|
|
49
|
+
"level": "required",
|
|
50
|
+
"kind": "hard",
|
|
51
|
+
"verification": "deterministic",
|
|
52
|
+
"check": "ui-design",
|
|
53
|
+
"failureHint": "Run lighthouse reconcile after reviewing the visual-theme profile plan, then run lighthouse verify again."
|
|
44
54
|
}
|
|
45
55
|
]
|
|
46
56
|
}
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
"product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
|
|
13
13
|
"profiles": {"type": "array", "items": {"enum": ["node-tooling", "dotnet-backend", "visual-theme"]}, "uniqueItems": true},
|
|
14
14
|
"integrations": {"type": "object", "additionalProperties": false, "required": ["productShape", "arcShape"], "properties": {"productShape": {"$ref": "#/$defs/pin"}, "arcShape": {"$ref": "#/$defs/pin"}}},
|
|
15
|
-
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
15
|
+
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}, "ui": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-ui"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
16
16
|
"managed": {"anyOf": [
|
|
17
17
|
{"const": ["LIGHTHOUSE.md"]},
|
|
18
18
|
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
|
|
19
19
|
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]},
|
|
20
|
-
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]}
|
|
20
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]},
|
|
21
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json", "docs/design/DESIGN.md", ".agents/skills/lighthouse-ui/SKILL.md"]}
|
|
21
22
|
]},
|
|
22
23
|
"repository": {
|
|
23
24
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@arcshape/cli": "0.1.3",
|
|
24
|
+
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
24
25
|
"@prodshape/cli": "0.22.0",
|
|
25
26
|
"@juangcarmona/lighthouse-sdlc": "0.5.0"
|
|
26
27
|
},
|
package/src/guardrails.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const allowedLevels = new Set(['required', 'recommended', 'optional']);
|
|
|
7
7
|
const allowedKinds = new Set(['hard', 'soft']);
|
|
8
8
|
const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
|
|
9
9
|
const allowedStatuses = new Set(['passed', 'failed', 'drift', 'unavailable', 'unconfigured', 'pending-review', 'not-applicable']);
|
|
10
|
+
const requiredVisualTokens = ['--lh-canvas', '--lh-surface', '--lh-text', '--lh-action', '--lh-border', '--lh-focus'];
|
|
10
11
|
|
|
11
12
|
function descriptorProblems(descriptor) {
|
|
12
13
|
const problems = [];
|
|
@@ -102,16 +103,30 @@ async function runDeterministicCheck(descriptor, context) {
|
|
|
102
103
|
const problems = context.validateGitConventions(context.repository);
|
|
103
104
|
return result(descriptor, problems.length ? 'failed' : 'passed', [{ value: context.branch ?? '' }, { pattern: context.repository.conventions.branchPattern }], problems);
|
|
104
105
|
}
|
|
106
|
+
if (descriptor.check === 'ui-design') {
|
|
107
|
+
const expected = ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'];
|
|
108
|
+
const missing = [];
|
|
109
|
+
const drifted = [];
|
|
110
|
+
for (const path of expected) {
|
|
111
|
+
const target = context.resolve(path);
|
|
112
|
+
if (!context.managedFiles[path] || !(await context.exists(target))) missing.push(path);
|
|
113
|
+
else if (await context.readFile(target, 'utf8') !== context.managedFiles[path]) drifted.push(`${path} has drifted; regenerate it from the visual-theme profile.`);
|
|
114
|
+
}
|
|
115
|
+
const tokenProblems = requiredVisualTokens.filter((token) => !context.uiTokens?.includes(token)).map((token) => `${token} is missing from the installed Lighthouse UI token source.`);
|
|
116
|
+
const problems = [...missing.map((path) => `${path} is missing; run lighthouse reconcile after reviewing the visual-theme profile plan.`), ...drifted, ...tokenProblems];
|
|
117
|
+
const status = drifted.length ? 'drift' : problems.length ? 'failed' : 'passed';
|
|
118
|
+
return result(descriptor, status, [...expected.map((path) => ({ path })), ...requiredVisualTokens.map((token) => ({ token }))], problems);
|
|
119
|
+
}
|
|
105
120
|
return result(descriptor, 'unavailable', [], [`No deterministic checker is registered for ${descriptor.id}.`]);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch }) {
|
|
123
|
+
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens }) {
|
|
109
124
|
if (manifestProblems.length) {
|
|
110
125
|
const descriptor = catalog.guardrails.find((item) => item.check === 'manifest') ?? catalog.guardrails[0];
|
|
111
126
|
const failed = result(descriptor, 'failed', [{ path: 'lighthouse.json' }], manifestProblems.map((problem) => `Invalid lighthouse.json: ${problem}`));
|
|
112
127
|
return report([failed]);
|
|
113
128
|
}
|
|
114
|
-
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch };
|
|
129
|
+
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens };
|
|
115
130
|
const results = [];
|
|
116
131
|
for (const descriptor of catalog.guardrails) {
|
|
117
132
|
if (!appliesTo(descriptor, manifest)) {
|
package/src/lighthouse.mjs
CHANGED
|
@@ -32,6 +32,7 @@ const upgradeStateRelative = '.lighthouse/upgrade.json';
|
|
|
32
32
|
const upgradeStatePath = resolve(root, upgradeStateRelative);
|
|
33
33
|
const layout = ['src', 'docs', 'infra', 'scripts'];
|
|
34
34
|
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
|
|
35
|
+
const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
|
|
35
36
|
const managedPaths = [
|
|
36
37
|
projection,
|
|
37
38
|
'scripts/lighthouse-guard.mjs',
|
|
@@ -41,16 +42,16 @@ const managedPaths = [
|
|
|
41
42
|
'.lighthouse/sdlc.json',
|
|
42
43
|
upgradeStateRelative,
|
|
43
44
|
];
|
|
45
|
+
const profileManagedPaths = Object.freeze({
|
|
46
|
+
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
47
|
+
});
|
|
44
48
|
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
45
49
|
const legacyManagedPaths = [projection];
|
|
50
|
+
const supportedProfiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
|
|
46
51
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
47
52
|
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
|
|
48
|
-
const upgradeScopes = {
|
|
49
|
-
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml'],
|
|
50
|
-
sdlc: ['.lighthouse/sdlc.json'],
|
|
51
|
-
};
|
|
52
53
|
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
53
|
-
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
54
|
+
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
54
55
|
|
|
55
56
|
const defaultRepository = () => ({
|
|
56
57
|
layout: [...layout],
|
|
@@ -58,16 +59,59 @@ const defaultRepository = () => ({
|
|
|
58
59
|
conventions: { commits: 'conventional', branchPattern },
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
function
|
|
62
|
+
function managedPathsFor(manifest) {
|
|
63
|
+
const profiles = manifest?.profiles ?? [];
|
|
64
|
+
return [...managedPaths, ...profiles.flatMap((profile) => profileManagedPaths[profile] ?? [])];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function upgradeScopesFor(manifest) {
|
|
68
|
+
return {
|
|
69
|
+
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
70
|
+
sdlc: ['.lighthouse/sdlc.json'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function packageAsset(packageName, entry) {
|
|
75
|
+
const path = packageRequire.resolve(`${packageName}/${entry}`);
|
|
76
|
+
return readFileSync(path, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function profileAssets(manifest) {
|
|
80
|
+
if (!(manifest?.profiles ?? []).includes('visual-theme')) return {};
|
|
81
|
+
try {
|
|
82
|
+
return {
|
|
83
|
+
'docs/design/DESIGN.md': packageAsset('@juangcarmona/lighthouse-ui', 'DESIGN.md'),
|
|
84
|
+
'.agents/skills/lighthouse-ui/SKILL.md': packageAsset('@juangcarmona/lighthouse-ui', 'skills/ui-design/SKILL.md'),
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Cannot read the installed Lighthouse UI profile: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function profileArguments(args) {
|
|
92
|
+
const selected = [];
|
|
93
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
94
|
+
if (args[index] !== '--profile' && !args[index].startsWith('--profile=')) continue;
|
|
95
|
+
const value = args[index] === '--profile' ? args[++index] : args[index].slice('--profile='.length);
|
|
96
|
+
if (!value || !supportedProfiles.includes(value)) throw new Error(`Unknown profile ${value ?? ''}; use ${supportedProfiles.join(', ')}.`);
|
|
97
|
+
if (!selected.includes(value)) selected.push(value);
|
|
98
|
+
}
|
|
99
|
+
return selected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultManifest(product, profiles = []) {
|
|
103
|
+
const draft = { profiles: [...profiles] };
|
|
104
|
+
const packages = { sdlc: { ...sdlcPackage } };
|
|
105
|
+
if (profiles.includes('visual-theme')) packages.ui = { ...uiPackage };
|
|
62
106
|
return {
|
|
63
107
|
$schema: './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-manifest.schema.json',
|
|
64
108
|
schemaVersion: 1,
|
|
65
109
|
baseline: '0.1.0',
|
|
66
110
|
product,
|
|
67
|
-
profiles: [],
|
|
111
|
+
profiles: [...profiles],
|
|
68
112
|
integrations: { productShape: null, arcShape: null },
|
|
69
|
-
packages
|
|
70
|
-
managed:
|
|
113
|
+
packages,
|
|
114
|
+
managed: managedPathsFor(draft),
|
|
71
115
|
repository: defaultRepository(),
|
|
72
116
|
};
|
|
73
117
|
}
|
|
@@ -167,7 +211,9 @@ function render(manifest) {
|
|
|
167
211
|
const publicSite = repository.publicSite.enabled ? 'enabled' : 'disabled';
|
|
168
212
|
const selectedSdlc = normalizeSdlcPackage(manifest);
|
|
169
213
|
const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
|
|
170
|
-
|
|
214
|
+
const selectedUi = manifest.packages?.ui;
|
|
215
|
+
const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
|
|
216
|
+
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`;
|
|
171
217
|
}
|
|
172
218
|
|
|
173
219
|
function guardrailScript(manifest) {
|
|
@@ -259,6 +305,7 @@ function managedFiles(manifest) {
|
|
|
259
305
|
'.husky/pre-push': prePushHook(),
|
|
260
306
|
'.github/workflows/lighthouse.yml': workflow(),
|
|
261
307
|
};
|
|
308
|
+
Object.assign(files, profileAssets(manifest));
|
|
262
309
|
const controlPlane = sdlcControlPlane(manifest);
|
|
263
310
|
if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
|
|
264
311
|
return files;
|
|
@@ -269,9 +316,10 @@ function contentHash(content) {
|
|
|
269
316
|
}
|
|
270
317
|
|
|
271
318
|
function upgradeState(manifest, files = managedFiles(manifest), previous = null) {
|
|
319
|
+
const scopes = upgradeScopesFor(manifest);
|
|
272
320
|
const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, {
|
|
273
321
|
hash: contentHash(content),
|
|
274
|
-
scope: Object.entries(
|
|
322
|
+
scope: Object.entries(scopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
|
|
275
323
|
}]));
|
|
276
324
|
return {
|
|
277
325
|
$schema: upgradeSchema,
|
|
@@ -309,7 +357,8 @@ function stateProblems(state) {
|
|
|
309
357
|
return problems;
|
|
310
358
|
}
|
|
311
359
|
|
|
312
|
-
function scopesFromArgs(args) {
|
|
360
|
+
function scopesFromArgs(args, manifest) {
|
|
361
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
313
362
|
const selected = new Set();
|
|
314
363
|
for (let index = 0; index < args.length; index += 1) {
|
|
315
364
|
if (args[index] !== '--scope' && !args[index].startsWith('--scope=')) continue;
|
|
@@ -325,7 +374,8 @@ function scopesFromArgs(args) {
|
|
|
325
374
|
return selected.size ? selected : new Set(Object.keys(upgradeScopes));
|
|
326
375
|
}
|
|
327
376
|
|
|
328
|
-
function selectedPaths(scopes) {
|
|
377
|
+
function selectedPaths(scopes, manifest) {
|
|
378
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
329
379
|
return new Set([...scopes].flatMap((scope) => upgradeScopes[scope]));
|
|
330
380
|
}
|
|
331
381
|
|
|
@@ -343,7 +393,8 @@ async function planUpgrade(manifest, scopes) {
|
|
|
343
393
|
const state = await readUpgradeState();
|
|
344
394
|
const problems = stateProblems(state);
|
|
345
395
|
const files = managedFiles(manifest);
|
|
346
|
-
const paths = selectedPaths(scopes);
|
|
396
|
+
const paths = selectedPaths(scopes, manifest);
|
|
397
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
347
398
|
const changes = [];
|
|
348
399
|
for (const path of Object.keys(files).filter((candidate) => paths.has(candidate))) {
|
|
349
400
|
const target = resolve(root, path);
|
|
@@ -418,7 +469,7 @@ function validate(manifest) {
|
|
|
418
469
|
if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
|
|
419
470
|
problems.push('product must be a readable name of 1–64 characters');
|
|
420
471
|
}
|
|
421
|
-
const profiles =
|
|
472
|
+
const profiles = supportedProfiles;
|
|
422
473
|
if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
|
|
423
474
|
|| new Set(manifest.profiles).size !== manifest.profiles.length) {
|
|
424
475
|
problems.push('profiles must be unique supported profile names');
|
|
@@ -435,26 +486,42 @@ function validate(manifest) {
|
|
|
435
486
|
if ('packages' in manifest) {
|
|
436
487
|
const packages = manifest.packages;
|
|
437
488
|
const selected = packages?.sdlc;
|
|
489
|
+
const selectedUi = packages?.ui;
|
|
490
|
+
const hasVisualProfile = manifest.profiles?.includes('visual-theme');
|
|
438
491
|
if (!packages || typeof packages !== 'object' || Array.isArray(packages)
|
|
439
|
-
|| Object.keys(packages).
|
|
492
|
+
|| !Object.keys(packages).every((key) => ['sdlc', 'ui'].includes(key))
|
|
493
|
+
|| Object.keys(packages).length < 1 || Object.keys(packages).length > 2
|
|
440
494
|
|| !selected || typeof selected !== 'object' || Array.isArray(selected)
|
|
441
495
|
|| Object.keys(selected).length !== 2
|
|
442
496
|
|| selected.name !== sdlcPackage.name
|
|
443
497
|
|| typeof selected.version !== 'string' || !pin.test(selected.version)) {
|
|
444
498
|
problems.push(`packages.sdlc must declare ${sdlcPackage.name} and an exact version or commit hash`);
|
|
445
499
|
}
|
|
500
|
+
if (hasVisualProfile && (!selectedUi || typeof selectedUi !== 'object' || Array.isArray(selectedUi)
|
|
501
|
+
|| Object.keys(selectedUi).length !== 2
|
|
502
|
+
|| selectedUi.name !== uiPackage.name
|
|
503
|
+
|| typeof selectedUi.version !== 'string' || !pin.test(selectedUi.version))) {
|
|
504
|
+
problems.push(`packages.ui must declare ${uiPackage.name} and an exact version or commit hash when visual-theme is selected`);
|
|
505
|
+
}
|
|
506
|
+
if (!hasVisualProfile && selectedUi) problems.push('packages.ui requires the visual-theme profile');
|
|
507
|
+
} else if (manifest.profiles?.includes('visual-theme')) {
|
|
508
|
+
problems.push(`packages must declare ${sdlcPackage.name} and ${uiPackage.name} when visual-theme is selected`);
|
|
446
509
|
}
|
|
447
510
|
const legacyManaged = Array.isArray(manifest.managed)
|
|
448
511
|
&& manifest.managed.length === legacyManagedPaths.length
|
|
449
512
|
&& manifest.managed.every((entry, index) => entry === legacyManagedPaths[index]);
|
|
513
|
+
const expectedManagedPaths = managedPathsFor(manifest);
|
|
450
514
|
const completeManaged = Array.isArray(manifest.managed)
|
|
515
|
+
&& manifest.managed.length === expectedManagedPaths.length
|
|
516
|
+
&& manifest.managed.every((entry, index) => entry === expectedManagedPaths[index]);
|
|
517
|
+
const profilelessManaged = Array.isArray(manifest.managed)
|
|
451
518
|
&& manifest.managed.length === managedPaths.length
|
|
452
519
|
&& manifest.managed.every((entry, index) => entry === managedPaths[index]);
|
|
453
520
|
const previousCompleteManaged = Array.isArray(manifest.managed)
|
|
454
521
|
&& manifest.managed.length === previousManagedPaths.length
|
|
455
522
|
&& manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
|
|
456
|
-
if (!legacyManaged && !previousCompleteManaged && !completeManaged) {
|
|
457
|
-
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the
|
|
523
|
+
if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged) {
|
|
524
|
+
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
|
|
458
525
|
}
|
|
459
526
|
if ('repository' in manifest) {
|
|
460
527
|
const repository = manifest.repository;
|
|
@@ -563,7 +630,12 @@ function printReconciliationPlan(plan) {
|
|
|
563
630
|
async function initialize(args) {
|
|
564
631
|
let manifest = await readManifest();
|
|
565
632
|
const manifestMissing = !manifest;
|
|
566
|
-
|
|
633
|
+
const selectedProfiles = profileArguments(args);
|
|
634
|
+
if (!manifest) manifest = defaultManifest(productNameFromArgs(args), selectedProfiles);
|
|
635
|
+
else if (selectedProfiles.some((profile) => !(manifest.profiles ?? []).includes(profile))) {
|
|
636
|
+
const missing = selectedProfiles.filter((profile) => !(manifest.profiles ?? []).includes(profile));
|
|
637
|
+
throw new Error(`The existing lighthouse.json does not select ${missing.join(', ')}; edit its profiles and rerun lighthouse init.`);
|
|
638
|
+
}
|
|
567
639
|
const problems = validate(manifest);
|
|
568
640
|
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
569
641
|
const plan = await planInitialization(manifest, manifestMissing);
|
|
@@ -682,7 +754,7 @@ async function upgrade(args) {
|
|
|
682
754
|
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
683
755
|
const problems = validate(manifest);
|
|
684
756
|
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
685
|
-
const plan = await planUpgrade(manifest, scopesFromArgs(args));
|
|
757
|
+
const plan = await planUpgrade(manifest, scopesFromArgs(args, manifest));
|
|
686
758
|
const format = formatFromArgs(args);
|
|
687
759
|
const apply = args.includes('--apply');
|
|
688
760
|
const dryRun = args.includes('--dry-run') || !apply;
|
|
@@ -849,6 +921,7 @@ async function verify(manifest, format = 'text') {
|
|
|
849
921
|
readFile,
|
|
850
922
|
validateGitConventions,
|
|
851
923
|
branch: gitBranch(),
|
|
924
|
+
uiTokens: manifest?.profiles?.includes('visual-theme') ? packageAsset('@juangcarmona/lighthouse-ui', 'tokens.css') : null,
|
|
852
925
|
});
|
|
853
926
|
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
854
927
|
for (const item of adoption) {
|
|
@@ -1052,6 +1125,15 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1052
1125
|
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1053
1126
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
1054
1127
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
1128
|
+
if (arg === '--profile') {
|
|
1129
|
+
if (command !== 'init' || !commandArgs[index + 1]) invalidArgument = true;
|
|
1130
|
+
else index += 1;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
|
|
1134
|
+
if (command !== 'init') invalidArgument = true;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1055
1137
|
if (arg === '--format') {
|
|
1056
1138
|
if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1057
1139
|
else index += 1;
|