@juangcarmona/lighthouse 0.5.0 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +8 -2
  2. package/assets/dotnet-template/Directory.Build.props +22 -0
  3. package/assets/dotnet-template/Directory.Packages.props +16 -0
  4. package/assets/dotnet-template/Lighthouse.Analyzers/BackendContractAnalyzer.cs +72 -0
  5. package/assets/dotnet-template/Lighthouse.Analyzers/Lighthouse.Analyzers.csproj +12 -0
  6. package/assets/dotnet-template/Lighthouse.Analyzers/packages.lock.json +121 -0
  7. package/assets/dotnet-template/Lighthouse.Cqrs/Handlers.cs +11 -0
  8. package/assets/dotnet-template/Lighthouse.Cqrs/Lighthouse.Cqrs.csproj +5 -0
  9. package/assets/dotnet-template/Lighthouse.Cqrs/packages.lock.json +6 -0
  10. package/assets/dotnet-template/Lighthouse.slnx +17 -0
  11. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/CatalogModule.cs +24 -0
  12. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/CreateEntry.cs +47 -0
  13. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/EntryStore.cs +17 -0
  14. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/GetEntry.cs +30 -0
  15. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj +12 -0
  16. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/packages.lock.json +10 -0
  17. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj +6 -0
  18. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Program.cs +25 -0
  19. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Properties/launchSettings.json +10 -0
  20. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/packages.lock.json +86 -0
  21. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj +13 -0
  22. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Program.cs +3 -0
  23. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Properties/launchSettings.json +15 -0
  24. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/aspire.config.json +5 -0
  25. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/packages.lock.json +689 -0
  26. package/assets/dotnet-template/global.json +6 -0
  27. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/BackendContractTests.cs +102 -0
  28. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj +9 -0
  29. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/packages.lock.json +125 -0
  30. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/CatalogTests.cs +111 -0
  31. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj +9 -0
  32. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/packages.lock.json +490 -0
  33. package/assets/profiles/catalog.json +48 -0
  34. package/assets/profiles/dotnet/README.md +32 -0
  35. package/assets/profiles/dotnet/SKILL.md +18 -0
  36. package/assets/profiles/dotnet/adoption.md +39 -0
  37. package/assets/profiles/dotnet/architecture.md +37 -0
  38. package/assets/profiles/dotnet/guardrails.md +32 -0
  39. package/assets/profiles/dotnet/practices.md +36 -0
  40. package/assets/profiles/dotnet/testing-and-development.md +29 -0
  41. package/assets/profiles/dotnet/use-cases.md +29 -0
  42. package/assets/schemas/lighthouse-manifest.schema.json +10 -7
  43. package/assets/schemas/profile-catalog.schema.json +63 -0
  44. package/package.json +5 -2
  45. package/src/create-dotnet.mjs +112 -0
  46. package/src/lighthouse.mjs +63 -13
  47. package/src/profiles.mjs +133 -0
  48. package/src/verify-dotnet.mjs +85 -0
@@ -0,0 +1,37 @@
1
+ # Backend structure and boundaries
2
+
3
+ Use the product namespace under juangcarmona. The example's namespace is `JuanGCarmona.Lighthouse.Example`; the creator replaces it with `JuanGCarmona.<Product>`. Established products retain their namespace as a documented adoption choice.
4
+
5
+ ```text
6
+ src/
7
+ root/<Product>.Api/ host, HTTP policy, composition
8
+ root/<Product>.AppHost/ optional development orchestration
9
+ modules/<Product>.Modules.<Name>/
10
+ <Name>Module.cs public registration and route composition
11
+ Features/<Feature>/
12
+ CreateThing.cs one complete command slice
13
+ GetThing.cs one complete query slice
14
+ Domain/ when the feature needs domain behavior
15
+ Infrastructure/Persistence/ when there is actual persistence
16
+ dotnet/Lighthouse.Cqrs/ initial typed command/query contracts
17
+ dotnet/Lighthouse.Analyzers/ local, versioned tooling source
18
+ src/dotnet/tests/ analyzer, HTTP and architecture tests
19
+ ```
20
+
21
+ Paths describe responsibilities; create folders only when they hold real code. The distributed starter keeps its examples under `src/`; generated products start directly under `src/`.
22
+
23
+ ## Dependencies
24
+
25
+ The API host composes modules. A module may depend on a sibling's separate `.Contracts` assembly, never its implementation assembly. Add a contracts project only when a consumer exists. Keep contracts small: integration DTOs, stable interfaces and events that describe a real boundary. A separate contracts assembly is a structural boundary; it does not by itself guarantee safe data exposure or correct authorization.
26
+
27
+ Within a feature, use cases coordinate domain behavior and infrastructure. A command may write; a query returns a read model and must not mutate business state. Keep the domain independent of ASP.NET Core and storage APIs. A schema/DbContext per module is recommended when data ownership warrants that cost; cross-module joins or shared tables need an explicit decision. An in-memory single-module example cannot prove those persistence boundaries.
28
+
29
+ ## Composition and cross-cutting behavior
30
+
31
+ Use explicit DI registration first. Introduce scanning or dispatch only when it removes measured repetition. The example injects typed handlers directly; it makes no mediator package mandatory. When logging, validation, authorization or caching become decorators, document and test their order. Authorization belongs at the edge and at the relevant domain/data boundary; validation must execute before mutation, including invocations outside HTTP.
32
+
33
+ Transactions belong to the consistency boundary of a command. Use domain events for real domain coordination; introduce an outbox when delivery must survive process/transaction failure. Cache keys and invalidation must be tested together. These capabilities are recommendations with adoption costs, not speculative packages in the starter.
34
+
35
+ ## Runtime and diagnostics
36
+
37
+ Aspire owns the optional local dependency graph and startup feedback. A deployment can use containers, a managed host or another product-approved topology. Production readiness covers configuration, identity, persistent data, migrations, health, telemetry, backup/recovery and release controls; the example implements only the smaller contract documented in its README.
@@ -0,0 +1,32 @@
1
+ # .NET guardrails and enforcement
2
+
3
+ Each product maintainer owns activation and exceptions. Lighthouse maintainers own the reference diagnostics and their tests. The distinction between implemented and recommended checks is part of the contract.
4
+
5
+ | Rule | Mechanism now | Failure | Exception / owner |
6
+ | --- | --- | --- | --- |
7
+ | LH1001: handlers stay internal sealed | Roslyn semantic rule for implementations of `JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<,>` / `IQueryHandler<,>` | Error during module build | Maintainer records a narrow suppression and rationale; test the changed boundary |
8
+ | LH1002: sibling module access uses Contracts | Roslyn compilation rule checks assembly references in the same `.Modules.` family | Error even for an unused implementation reference | Redesign dependency; temporary scoped suppression requires owner/review date |
9
+ | Every implementation module activates the rules | Verifier inspects evaluated MSBuild properties and analyzer references for solution projects | Verifier fails if a named module silently omits enforcement | Product maintainer defines an explicit migration scope before adopting this full verifier |
10
+ | Nullable correctness and SDK analysis | `src/dotnet/Directory.Build.props`, warnings-as-errors | Compiler/analysis warning fails build | Narrow documented suppression |
11
+ | Formatting | `.editorconfig` and `dotnet format whitespace --verify-no-changes` | Verifier fails on whitespace drift | Product-owned formatting policy |
12
+ | Reproducible dependency resolution | Pinned versions, SDK feature band, committed NuGet locks, locked restore | Restore fails on drift | Reviewed dependency update regenerates locks |
13
+ | Known dependency vulnerabilities | NuGet audit of all dependencies; warnings-as-errors | Restore fails for reported advisories at low or above | Security owner decides explicit, expiring risk acceptance; no blanket audit disable |
14
+ | HTTP validation and authorization | Functional tests of 400/401/403/201/200/404 behavior | Test fails | Accepted behavior changes require corresponding product decision |
15
+ | Compiled module boundary | Reflection architecture test checks exported handlers and references | Test fails | Match product's actual architectural contract |
16
+ | Broken compilation rules really fail build | `src/packages/cli/internal/verify-dotnet.mjs` compiles deliberately invalid temporary projects | Verifier fails unless LH1001 and LH1002 reject the fixture | Fix tooling; never accept a fixture merely because a unit test passed |
17
+ | Public entity leaks, controller prohibition, domain-framework dependency | Documented profile rules; targeted automated rules are future work | Review finding today | Maintainer records exception; do not claim automatic enforcement |
18
+ | Persistent data/tenant isolation | Product-specific integration tests, recommended | No coverage in this in-memory example | Product owner and security/data owner define requirements |
19
+
20
+ ## Activation
21
+
22
+ Attach the analyzer as an analyzer-only project reference, set `LighthouseModule=true` on implementation projects, and expose that property with `CompilerVisibleProperty`. Module assembly names follow `<Root>.Modules.<Name>` and separate contract assemblies end in `.Contracts`. The distributed Catalog project and build properties are the executable contract.
23
+
24
+ `LH1001` recognizes the supplied CQRS interfaces semantically, including aliases/inheritance. A different mediator/handler abstraction needs an adapter or another tested rule; class-name conventions alone are not enforcement. The rule skips compiler-generated code. `LH1002` checks compilation references for the configured naming family; it does not inspect payload semantics, reflection-based access, network calls or dependency injection at runtime. Use architecture/functional tests for those boundaries.
25
+
26
+ ## Diagnostic lifecycle
27
+
28
+ New rules require positive and negative compiler fixtures, a stable diagnostic ID, useful remediation, release tracking and a failing-build rehearsal. Keep their compiler API compatible with the supported SDK. Move rules to the shipped ledger only as part of an accepted release. The current analyzers are local source tooling, not a published NuGet package.
29
+
30
+ ## Exceptions
31
+
32
+ Record rule/diagnostic, affected paths, reason, owner, review date and compensating test. Suppress the smallest symbol/project scope, and remove the suppression after the migration. An architectural exception must not be disguised as generated code. CI runs the same verifier; required merge status still depends on the host's branch rules.
@@ -0,0 +1,36 @@
1
+ # Backend practice catalog
2
+
3
+ This catalog makes the profile reviewable. Product code owners choose applicability; security/data owners accept changes to their boundaries. “Reference” means the example demonstrates a pattern. “Enforced” names the current tool. A recommendation becomes a gate only after a product implements and verifies it.
4
+
5
+ | Practice | Problem addressed / when to apply | Mechanism in this release |
6
+ | --- | --- | --- |
7
+ | One deployable host, modules at real seams | Keep operations simple while preserving domain/data ownership; extra modules add coordination cost | Reference layout; boundary design is a review decision |
8
+ | Feature-first organization | Keep behavior changes adjacent; avoid mandatory layer-by-layer file hopping | Reference and guidance |
9
+ | One file per command/query | Keep route, DTOs, request, validation and handler together | Reference and review; file shape has no custom analyzer yet |
10
+ | CQRS without separate storage by default | Distinguish reads from writes without adding a broker/read replica prematurely | Typed handler contracts and two working slices |
11
+ | HTTP-independent handlers | Reuse/test application behavior without transport dependencies | Reference; dedicated dependency rule is future work |
12
+ | Internal sealed handlers | Prevent application implementation from becoming a public API or inheritance surface | Enforced LH1001 for supported handler interfaces |
13
+ | Public composition, private implementation | Make module registration explicit; expose only deliberate integration contracts | Reference; public entity-leak rule is future work |
14
+ | Sibling Contracts assemblies | Stop modules depending on implementation internals; add contracts only for an actual consumer | Enforced LH1002 plus compiled architecture test |
15
+ | Small shared kernel | Keep genuinely common technical contracts in one place without building an internal framework | Two typed handler interfaces; review additional abstractions |
16
+ | Validation before side effects | Reject bad commands whether invoked through HTTP or another path | Reference handler validates before mutation; HTTP tests |
17
+ | Explicit typed outcomes / ProblemDetails | Keep expected failures separate from unexpected exceptions and stable at the edge | Reference validation/missing-resource responses; HTTP tests |
18
+ | Cross-cutting pipeline with tested order | Avoid inconsistent logging/validation/cache behavior after repetition justifies decorators | Recommendation; document order and test cache invalidation if introduced |
19
+ | Trusted identity and authorization | Prevent callers from selecting their own owner/tenant boundary | JWT edge policy and 401/403 tests; tenant data checks require a persistent product slice |
20
+ | Module-owned persistence and migrations | Avoid accidental shared-table coupling; separate schema/DbContext when ownership warrants the complexity | Recommendation; example has only in-memory storage |
21
+ | Transaction boundaries per command | Make all-or-nothing behavior and concurrency conflicts explicit | Recommendation; database-backed tests required when introduced |
22
+ | Domain events with durable delivery when needed | Keep domain coordination explicit; use an outbox only when delivery must survive failure | Recommendation; no event/outbox framework in the starter |
23
+ | Read-model cache and tested invalidation | Reduce measured read cost without returning stale business state | Recommendation; no cache in the reference |
24
+ | Cancellation and explicit identifiers | Bound abandoned work and expose stable resource identity | Reference propagates CancellationToken and uses GUID-v7 identifiers |
25
+ | Same-origin/BFF where appropriate | Simplify browser session and API trust boundaries for products that benefit from one web origin | Optional architecture choice; no frontend/BFF in the example |
26
+ | Generated API contracts | Detect API/client drift while keeping generated files disposable | Recommendation; require a real consumer before adding generation |
27
+ | Structured logs and trace context | Diagnose a failed use case without logging sensitive payloads | Host development logging; exporter, redaction and event-ID policies remain product work |
28
+ | Aspire development graph | Start related services with observable readiness and coherent configuration | Runnable AppHost for the API; provision only dependencies the product uses |
29
+ | Layered verification by risk | Catch cheap faults early and reserve expensive journeys for valuable system behavior | Semantic analyzer, architecture and HTTP tests; engine/browser backend journeys added when needed |
30
+ | SDK/package pins and lockfiles | Make local and CI feedback reproducible | Locked restore, central versions and SDK feature band |
31
+ | Warnings, style and audit as failures | Surface compiler issues, code drift and known vulnerable dependencies before merge | SDK analysis, format verification and NuGet audit in the verifier |
32
+ | Scoped CI with evidence of routing | Control cost without letting path filters hide meaningful changes | Current CI runs all selected profile checks; selective routing needs its own tests |
33
+ | Concise agent/document routers | Help humans and agents find behavior, architecture and commands without duplicated truths | AGENTS entrypoint and profile navigation |
34
+ | Owned, expiring exceptions | Make deviations deliberate and reversible | Documented process; no expiry automation is claimed |
35
+
36
+ Use [architecture](architecture.md) for dependency direction, [use cases](use-cases.md) for implementation detail, and [guardrails](guardrails.md) for diagnostics, failure consequences and exception ownership.
@@ -0,0 +1,29 @@
1
+ # Tests and development feedback
2
+
3
+ Run `npm run verify:dotnet` from Lighthouse or the generated backend. It performs locked restore, Release build, tests and a deliberately failing-build rehearsal. The repository CI definition runs this command as its .NET job.
4
+
5
+ | Level | Purpose | Reference / cost |
6
+ | --- | --- | --- |
7
+ | Analyzer semantic fixtures | Reject forbidden shapes and allow legitimate code | Compiler in memory; fast |
8
+ | Architecture | Inspect actual exports and assembly dependencies | Compiled assemblies; fast |
9
+ | Functional HTTP | Verify routing, auth, validation and responses through the real ASP.NET pipeline | WebApplicationFactory; no network/services |
10
+ | Persistence | Prove queries, transactions, schema and isolation against the real engine | Add Testcontainers when persistence is introduced |
11
+ | Browser journey | Prove an actual user interaction across the system | Product UI only; slower lane |
12
+
13
+ Choose boundaries according to failure risk. Use domain tests for complex domain rules, functional tests for API/data integration, and a small number of valuable end-to-end journeys. Don't replace functional coverage with mocks or make every PR start a full deployment without evidence that the cost helps.
14
+
15
+ ## Local loop
16
+
17
+ ```sh
18
+ dotnet run --project src/dotnet/examples/src/root/Lighthouse.Example.Api
19
+ dotnet run --project src/dotnet/examples/src/root/Lighthouse.Example.AppHost
20
+ dotnet test src/dotnet/tests/Lighthouse.Example.Tests -c Release
21
+ ```
22
+
23
+ Run either the API or AppHost entrypoint. The AppHost starts the API and exposes Aspire's local dashboard; the example has no fake database dependency. Host telemetry/health are development feedback, not proof of a production collector, alerts or durable storage. When adding PostgreSQL or another service, provision it in the local graph, wait for readiness, expose configuration through DI, and add engine-backed tests.
24
+
25
+ Configure `Authentication__Authority` and `Authentication__Audience` with the product's trusted OIDC provider for authenticated manual requests. JWT defaults require issuer/signature/expiry validation and secure metadata. The test scheme and synthetic identity headers are confined to the test assembly. Missing credentials fail closed; the example ships no bypass, identity service or secret. `/health` is a minimal process-readiness route, not proof of data-service readiness.
26
+
27
+ Use user-secrets/environment/approved secret storage for development configuration. Define production logging, redaction, traces/metrics exporters, graceful shutdown, dependency timeouts and recovery according to the product. The in-memory store and lack of full production hardening are explicit example limits.
28
+
29
+ The AppHost pins MessagePack 2.5.303 to resolve [GHSA-qhrr-8q5h-9q3h](https://github.com/MessagePack-CSharp/MessagePack-CSharp/security/advisories/GHSA-qhrr-8q5h-9q3h) while retaining NuGet audit. Revisit the override when the upstream dependency includes the fix.
@@ -0,0 +1,29 @@
1
+ # One file per use case
2
+
3
+ Read the distributed starter's `CreateEntry.cs` and `GetEntry.cs` examples. Each file owns a concrete behavior rather than a technical layer.
4
+
5
+ | Part | Responsibility | Boundary |
6
+ | --- | --- | --- |
7
+ | Route mapping | URL, binding, authorization policy, cancellation and HTTP outcome | Thin transport adapter |
8
+ | Request / response | External input and output shape | Never expose persistence entities |
9
+ | Command / query | Application intent and its inputs | No HttpContext or IResult |
10
+ | Validator | Structural/business input constraints before mutation | Callable for non-HTTP invocations |
11
+ | Handler | Coordinate the use case and domain/storage work | Internal sealed; returns application data/outcome |
12
+
13
+ Nest these types in the use-case class so related changes remain adjacent. Omit a request DTO where there is no body and no benefit; the query example binds its ID from the route. Keep reusable domain rules in the domain rather than duplicating them across validators.
14
+
15
+ ## Completion checklist
16
+
17
+ 1. Define the accepted behavior and failure examples before implementing the slice.
18
+ 2. Map a Minimal API endpoint and apply its real authorization policy.
19
+ 3. Resolve authenticated identity/tenant from trusted context when needed; never trust an owner ID supplied by the body.
20
+ 4. Map to a command/query, validate and call an HTTP-independent handler.
21
+ 5. Produce explicit success, validation, missing-resource and conflict outcomes as applicable. Hide unexpected exception details behind ProblemDetails and log context without secrets or payload dumps.
22
+ 6. Add the cheapest useful tests: domain rules, functional outcomes, authorization failures and persistence boundaries where present.
23
+ 7. Run build/analyzers and the required verification command; record any approved exception.
24
+
25
+ The example keeps validation and outcome records local rather than introducing a general results framework. Products can choose an established result/validation library with a pinned version. Cancellation, authorization, logging and transactions still need verification regardless of the library.
26
+
27
+ ## Avoid accidental complexity
28
+
29
+ Do not create a controller/service/repository class for every endpoint, a contracts assembly per feature, or a separate read database because a diagram says CQRS. Extract shared code only after a real use case needs it. Keep generated clients/metadata disposable; validate generator inputs, deterministic output and consumer contracts rather than hand-editing generated files.
@@ -10,16 +10,19 @@
10
10
  "schemaVersion": {"const": 1},
11
11
  "baseline": {"const": "0.1.0"},
12
12
  "product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
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
+ },
13
22
  "profiles": {"type": "array", "items": {"enum": ["node-tooling", "dotnet-backend", "visual-theme"]}, "uniqueItems": true},
14
23
  "integrations": {"type": "object", "additionalProperties": false, "required": ["productShape", "arcShape"], "properties": {"productShape": {"$ref": "#/$defs/pin"}, "arcShape": {"$ref": "#/$defs/pin"}}},
15
24
  "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
- "managed": {"anyOf": [
17
- {"const": ["LIGHTHOUSE.md"]},
18
- {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
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"]},
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"]}
22
- ]},
25
+ "managed": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true},
23
26
  "repository": {
24
27
  "type": "object",
25
28
  "additionalProperties": false,
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/profile-catalog.schema.json",
4
+ "title": "Lighthouse profile catalog",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "schemaVersion", "profiles"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "profiles": {
12
+ "type": "array",
13
+ "items": { "$ref": "#/$defs/profile" }
14
+ }
15
+ },
16
+ "$defs": {
17
+ "profile": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": ["id", "owner", "purpose", "appliesTo", "surfaces", "verification"],
21
+ "properties": {
22
+ "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
23
+ "owner": { "type": "string", "minLength": 1 },
24
+ "purpose": { "type": "string", "minLength": 1 },
25
+ "appliesTo": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["shapes", "stacks"],
29
+ "properties": {
30
+ "shapes": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true },
31
+ "stacks": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
32
+ }
33
+ },
34
+ "surfaces": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "uniqueItems": true },
35
+ "verification": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["layers"],
39
+ "properties": {
40
+ "layers": {
41
+ "type": "array",
42
+ "items": { "$ref": "#/$defs/layer" },
43
+ "minItems": 1,
44
+ "uniqueItems": true
45
+ }
46
+ }
47
+ }
48
+ }
49
+ },
50
+ "layer": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["id", "level", "kind", "verification"],
54
+ "properties": {
55
+ "id": { "enum": ["static", "unit", "architecture", "integration", "functional", "browser-e2e", "operational"] },
56
+ "level": { "enum": ["required", "recommended", "optional"] },
57
+ "kind": { "enum": ["hard", "soft"] },
58
+ "verification": { "enum": ["deterministic", "semantic", "human"] },
59
+ "command": { "type": ["string", "null"] }
60
+ }
61
+ }
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -23,13 +23,16 @@
23
23
  "@arcshape/cli": "0.1.3",
24
24
  "@juangcarmona/lighthouse-ui": "0.2.0",
25
25
  "@prodshape/cli": "0.22.0",
26
- "@juangcarmona/lighthouse-sdlc": "0.5.0"
26
+ "@juangcarmona/lighthouse-sdlc": "0.6.0"
27
27
  },
28
28
  "files": [
29
29
  "src/lighthouse.mjs",
30
30
  "src/guardrails.mjs",
31
31
  "assets",
32
32
  "README.md",
33
+ "src/profiles.mjs",
34
+ "src/create-dotnet.mjs",
35
+ "src/verify-dotnet.mjs",
33
36
  "upgrades"
34
37
  ],
35
38
  "bin": {
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createRequire } from 'node:module';
6
+
7
+ const packageRoot = fileURLToPath(new URL('../', import.meta.url));
8
+ const templateRoot = resolve(packageRoot, 'assets/dotnet-template');
9
+ const cliPackage = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
10
+ const require = createRequire(import.meta.url);
11
+ const sdlcContract = JSON.parse(await readFile(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'), 'utf8'));
12
+ const args = process.argv.slice(2);
13
+ const target = args[0] ? resolve(args[0]) : null;
14
+ const name = args.includes('--name') ? args[args.indexOf('--name') + 1] : null;
15
+
16
+ if (!target || !name || !/^[A-Z][A-Za-z0-9]{1,47}$/.test(name) || args.length !== 3 || args[1] !== '--name') {
17
+ process.stderr.write('Usage: lighthouse create-dotnet <empty-target> --name Workshop\nUse a PascalCase product identifier of 2–48 letters/digits.\n');
18
+ process.exit(2);
19
+ }
20
+
21
+ let existing = false;
22
+ try {
23
+ const info = await lstat(target);
24
+ if (!info.isDirectory() || info.isSymbolicLink() || (await readdir(target)).length > 0) throw new Error('The target must be a new directory or an empty directory.');
25
+ existing = true;
26
+ } catch (error) {
27
+ if (error.code !== 'ENOENT') throw error;
28
+ }
29
+
30
+ const transform = (text) => text
31
+ .replaceAll('JuanGCarmona.Lighthouse.Example', `JuanGCarmona.${name}`)
32
+ .replaceAll('Lighthouse.Example', name)
33
+ .replaceAll('Lighthouse_Example', name)
34
+ .replaceAll('../../../../Lighthouse.Cqrs/', '../../dotnet/Lighthouse.Cqrs/')
35
+ .replaceAll('../../../../Lighthouse.Analyzers/', '../../dotnet/Lighthouse.Analyzers/')
36
+ .replaceAll('../../Lighthouse.Analyzers/', '../../../src/dotnet/Lighthouse.Analyzers/')
37
+ .replaceAll('../../examples/src/root/', '../../../src/root/');
38
+
39
+ async function copy(from, to) {
40
+ const info = await lstat(from);
41
+ if (info.isDirectory()) {
42
+ await mkdir(to, { recursive: true });
43
+ for (const entry of await readdir(from)) {
44
+ if (['bin', 'obj'].includes(entry)) continue;
45
+ await copy(join(from, entry), join(to, entry.replaceAll('Lighthouse.Example', name)));
46
+ }
47
+ return;
48
+ }
49
+ if (!info.isFile()) throw new Error('Starter sources must be ordinary files and directories.');
50
+ await mkdir(dirname(to), { recursive: true });
51
+ await writeFile(to, transform(await readFile(from, 'utf8')));
52
+ }
53
+
54
+ const stage = await mkdtemp(join(dirname(target), '.lighthouse-create-dotnet-'));
55
+ try {
56
+ for (const [from, to] of [
57
+ ['Lighthouse.Analyzers', 'src/dotnet/Lighthouse.Analyzers'],
58
+ ['Lighthouse.Cqrs', 'src/dotnet/Lighthouse.Cqrs'],
59
+ ['examples/src', 'src'],
60
+ ['tests', 'tests/dotnet'],
61
+ ['Directory.Build.props', 'Directory.Build.props'],
62
+ ['Directory.Packages.props', 'Directory.Packages.props'],
63
+ ['global.json', 'global.json'],
64
+ ['Lighthouse.slnx', `${name}.slnx`],
65
+ ]) await copy(resolve(templateRoot, from), resolve(stage, to));
66
+
67
+ const solutionFile = resolve(stage, `${name}.slnx`);
68
+ const solutionText = (await readFile(solutionFile, 'utf8'))
69
+ .replaceAll('examples/src/', 'src/')
70
+ .replaceAll('Lighthouse.Analyzers/', 'src/dotnet/Lighthouse.Analyzers/')
71
+ .replaceAll('Lighthouse.Cqrs/', 'src/dotnet/Lighthouse.Cqrs/')
72
+ .replaceAll('tests/', 'tests/dotnet/');
73
+ const projects = [...solutionText.matchAll(/<Project Path="[^"]+"\s*\/>/g)].map((match) => match[0]);
74
+ await writeFile(solutionFile, `<Solution>\n${projects.map((project) => ` ${project}`).join('\n')}\n</Solution>\n`);
75
+
76
+ await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
77
+ await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
78
+ await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
79
+ await copy(resolve(packageRoot, 'src/verify-dotnet.mjs'), resolve(stage, 'scripts/verify-dotnet.mjs'));
80
+ await copy(resolve(packageRoot, 'assets/guardrails'), resolve(stage, 'assets/guardrails'));
81
+ await copy(resolve(packageRoot, 'assets/profiles'), resolve(stage, 'assets/profiles'));
82
+ await copy(resolve(packageRoot, 'assets/schemas'), resolve(stage, 'schemas'));
83
+
84
+ await writeFile(resolve(stage, 'lighthouse.json'), `${JSON.stringify({
85
+ $schema: './schemas/lighthouse-manifest.schema.json', schemaVersion: 1, baseline: '0.1.0',
86
+ product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
87
+ integrations: { productShape: null, arcShape: null },
88
+ 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'],
90
+ repository: {
91
+ layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
92
+ conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
93
+ },
94
+ }, null, 2)}\n`);
95
+ await writeFile(resolve(stage, 'package.json'), `${JSON.stringify({
96
+ name: `@juangcarmona/${name.toLowerCase()}`, private: true, type: 'module', engines: { node: '>=22' },
97
+ scripts: { verify: 'node scripts/lighthouse.mjs verify && node scripts/verify-dotnet.mjs' },
98
+ }, null, 2)}\n`);
99
+ await writeFile(resolve(stage, 'README.md'), `# ${name}\n\nBackend seeded from Lighthouse ${cliPackage.version}. Application and tooling sources are product-owned after creation.\n\nRun dotnet restore ${name}.slnx once to create lockfiles, then npm run verify. Commit lockfiles.\n\nStart dotnet run --project src/root/${name}.Api or the optional src/root/${name}.AppHost. Configure authentication using a trusted OIDC provider for manual API access. Test identities exist only in tests.\n\nThe sample catalog uses disposable in-memory storage. Add real persistence, data isolation, telemetry and deployment policy according to product requirements. Keep each command/query's endpoint, DTOs, validation and internal sealed handler together.\n`);
100
+ await writeFile(resolve(stage, 'AGENTS.md'), `# ${name} engineering guidance\n\nUse the Lighthouse .NET profile from the pinned baseline revision. Keep Minimal API commands/queries as complete single-file slices. Handlers remain internal sealed and independent of HTTP. Modules use sibling Contracts assemblies. Run npm run verify.\n\nApplication and tooling source is product-owned. Do not hand-edit LIGHTHOUSE.md. Record narrow, expiring exceptions for suppressed diagnostics.\n`);
101
+ await mkdir(resolve(stage, '.github/workflows'), { recursive: true });
102
+ await writeFile(resolve(stage, '.github/workflows/ci.yml'), `name: Backend verification\non: [push, pull_request]\npermissions:\n contents: read\njobs:\n dotnet:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n - uses: actions/setup-dotnet@v4\n with:\n global-json-file: global.json\n - run: npm run verify\n`);
103
+
104
+ const initialized = (await import('node:child_process')).spawnSync(process.execPath, [resolve(stage, 'scripts/lighthouse.mjs'), 'init'], { cwd: stage, encoding: 'utf8' });
105
+ if (initialized.status !== 0) throw new Error(initialized.stderr || 'Projection generation failed.');
106
+ if (existing) await rmdir(target);
107
+ await rename(stage, target);
108
+ process.stdout.write(`Created ${name} in ${target}. Run dotnet restore ${name}.slnx, then npm run verify.\n`);
109
+ } catch (error) {
110
+ await rm(stage, { recursive: true, force: true });
111
+ throw error;
112
+ }
@@ -6,6 +6,7 @@ import { createRequire } from 'node:module';
6
6
  import { basename, dirname, resolve } from 'node:path';
7
7
  import { execFileSync, spawnSync } from 'node:child_process';
8
8
  import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
9
+ import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
9
10
  let sdlcCapabilities;
10
11
  let sdlcReconcile;
11
12
  let sdlcGithub;
@@ -44,6 +45,7 @@ const managedPaths = [
44
45
  ];
45
46
  const profileManagedPaths = Object.freeze({
46
47
  'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
48
+ 'dotnet-backend': ['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'],
47
49
  });
48
50
  const previousManagedPaths = managedPaths.slice(0, -1);
49
51
  const legacyManagedPaths = [projection];
@@ -51,7 +53,7 @@ const supportedProfiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
51
53
  const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
52
54
  const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
53
55
  const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
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]';
56
+ 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]';
55
57
 
56
58
  const defaultRepository = () => ({
57
59
  layout: [...layout],
@@ -77,15 +79,24 @@ function packageAsset(packageName, entry) {
77
79
  }
78
80
 
79
81
  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}`);
82
+ const assets = {};
83
+ if ((manifest?.profiles ?? []).includes('visual-theme')) {
84
+ try {
85
+ assets['docs/design/DESIGN.md'] = packageAsset('@juangcarmona/lighthouse-ui', 'DESIGN.md');
86
+ assets['.agents/skills/lighthouse-ui/SKILL.md'] = packageAsset('@juangcarmona/lighthouse-ui', 'skills/ui-design/SKILL.md');
87
+ } catch (error) {
88
+ throw new Error(`Cannot read the installed Lighthouse UI profile: ${error.message}`);
89
+ }
88
90
  }
91
+ if ((manifest?.profiles ?? []).includes('dotnet-backend')) {
92
+ const files = ['README.md', 'adoption.md', 'architecture.md', 'guardrails.md', 'practices.md', 'testing-and-development.md', 'use-cases.md', 'SKILL.md'];
93
+ for (const file of files) {
94
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-dotnet/SKILL.md' : `docs/profiles/dotnet/${file}`;
95
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/dotnet/${file}`, import.meta.url), 'utf8'); }
96
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET profile: ${error.message}`); }
97
+ }
98
+ }
99
+ return assets;
89
100
  }
90
101
 
91
102
  function profileArguments(args) {
@@ -108,6 +119,7 @@ function defaultManifest(product, profiles = []) {
108
119
  schemaVersion: 1,
109
120
  baseline: '0.1.0',
110
121
  product,
122
+ application: { shape: 'unknown', stacks: [] },
111
123
  profiles: [...profiles],
112
124
  integrations: { productShape: null, arcShape: null },
113
125
  packages,
@@ -458,7 +470,7 @@ function printUpgradePlan(plan) {
458
470
  function validate(manifest) {
459
471
  const problems = [];
460
472
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return ['manifest must be an object'];
461
- const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'packages', 'managed', 'repository'];
473
+ const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'application', 'profiles', 'integrations', 'packages', 'managed', 'repository'];
462
474
  for (const key of Object.keys(manifest)) if (!allowed.includes(key)) problems.push(`unknown field ${key}`);
463
475
  for (const key of ['schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'managed']) {
464
476
  if (!(key in manifest)) problems.push(`missing ${key}`);
@@ -469,6 +481,15 @@ function validate(manifest) {
469
481
  if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
470
482
  problems.push('product must be a readable name of 1–64 characters');
471
483
  }
484
+ if ('application' in manifest) {
485
+ const application = manifest.application;
486
+ if (!application || typeof application !== 'object' || Array.isArray(application)
487
+ || !['unknown', 'frontend', 'backend', 'full-stack', 'service', 'library', 'tooling', 'cli', 'worker', 'mobile'].includes(application.shape)
488
+ || !Array.isArray(application.stacks) || application.stacks.some((stack) => typeof stack !== 'string' || !stack)
489
+ || new Set(application.stacks).size !== application.stacks.length) {
490
+ problems.push('application must declare a supported shape and unique stack names');
491
+ }
492
+ }
472
493
  const profiles = supportedProfiles;
473
494
  if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
474
495
  || new Set(manifest.profiles).size !== manifest.profiles.length) {
@@ -923,6 +944,11 @@ async function verify(manifest, format = 'text') {
923
944
  branch: gitBranch(),
924
945
  uiTokens: manifest?.profiles?.includes('visual-theme') ? packageAsset('@juangcarmona/lighthouse-ui', 'tokens.css') : null,
925
946
  });
947
+ const profileComposition = composeProfiles(readProfileCatalog(), manifest);
948
+ report.profiles = profileComposition;
949
+ const profileResult = profileCompositionResult(profileComposition);
950
+ report.results.push(profileResult);
951
+ report.summary[profileResult.status] = (report.summary[profileResult.status] ?? 0) + 1;
926
952
  const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
927
953
  for (const item of adoption) {
928
954
  if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
@@ -956,6 +982,20 @@ async function verify(manifest, format = 'text') {
956
982
  console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
957
983
  }
958
984
 
985
+ async function profiles(manifest, format = 'text') {
986
+ const composition = composeProfiles(readProfileCatalog(), manifest);
987
+ if (format === 'json') {
988
+ console.log(JSON.stringify(composition, null, 2));
989
+ if (composition.status === 'failed') process.exitCode = 1;
990
+ return;
991
+ }
992
+ console.log(`Lighthouse profile composition: ${composition.status}`);
993
+ console.log(` application ${composition.application.shape}${composition.application.stacks.length ? ` (${composition.application.stacks.join(', ')})` : ''}`);
994
+ for (const profile of composition.selected) console.log(` ${profile.id}: ${profile.status}${profile.nextAction ? `; next: ${profile.nextAction}` : ''}`);
995
+ if (!composition.selected.length) console.log(' no profiles selected');
996
+ if (composition.status === 'failed') process.exitCode = 1;
997
+ }
998
+
959
999
  function planHash(plan) {
960
1000
  const { authorization, ...payload } = plan;
961
1001
  return contentHash(JSON.stringify(payload));
@@ -1117,6 +1157,11 @@ function formatFromArgs(args) {
1117
1157
  const args = process.argv.slice(2);
1118
1158
  const command = args[0];
1119
1159
  const commandArgs = args.slice(1);
1160
+ if (command === 'create-dotnet') {
1161
+ process.argv.splice(2, 1);
1162
+ await import('./create-dotnet.mjs');
1163
+ process.exit(0);
1164
+ }
1120
1165
  let invalidArgument = false;
1121
1166
  for (let index = 0; index < commandArgs.length; index += 1) {
1122
1167
  const arg = commandArgs[index];
@@ -1135,12 +1180,12 @@ for (let index = 0; index < commandArgs.length; index += 1) {
1135
1180
  continue;
1136
1181
  }
1137
1182
  if (arg === '--format') {
1138
- if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1183
+ if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1139
1184
  else index += 1;
1140
1185
  continue;
1141
1186
  }
1142
1187
  if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
1143
- if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
1188
+ if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
1144
1189
  continue;
1145
1190
  }
1146
1191
  if (arg === '--scope') {
@@ -1160,7 +1205,7 @@ for (let index = 0; index < commandArgs.length; index += 1) {
1160
1205
  if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
1161
1206
  invalidArgument = true;
1162
1207
  }
1163
- if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
1208
+ if (!['init', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
1164
1209
  console.error(usage);
1165
1210
  process.exit(2);
1166
1211
  }
@@ -1170,6 +1215,11 @@ try {
1170
1215
  else if (command === 'reconcile') await reconcile(args.slice(1));
1171
1216
  else if (command === 'upgrade') await upgrade(args.slice(1));
1172
1217
  else if (command === 'capabilities') await capabilities(args.slice(1));
1218
+ else if (command === 'profiles') {
1219
+ const manifest = await readManifest();
1220
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
1221
+ await profiles(manifest, formatFromArgs(commandArgs));
1222
+ }
1173
1223
  else {
1174
1224
  const manifest = await readManifest();
1175
1225
  if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');