@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 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 and `DESIGN.md`. Stack profiles own their source templates, guidance and checks.
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.4.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
  },
@@ -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)) {
@@ -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 defaultManifest(product) {
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: { sdlc: { ...sdlcPackage } },
70
- managed: [...managedPaths],
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
- return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package and host adapters without storing agent runtime data.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
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(upgradeScopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
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 = ['node-tooling', 'dotnet-backend', 'visual-theme'];
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).length !== 1
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 current complete set`);
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
- if (!manifest) manifest = defaultManifest(productNameFromArgs(args));
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;