rcf-lite 0.13.0 → 0.14.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/CHANGELOG.md +29 -1
- package/bin/rcf.js +3 -1
- package/blueprints/application-api-rest/docs/topics.md +1 -1
- package/blueprints/application-spa/README.md +3 -3
- package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
- package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
- package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
- package/blueprints/application-spa/docs/topics.md +1 -1
- package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
- package/blueprints/delivery-ci-workflows/README.md +57 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +55 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +65 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +71 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +61 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
- package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +2 -1
- package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
- package/blueprints/{ci-pipeline/contributions/adrs/adr-702-ci-pipeline-strict-coverage-gate.json → delivery-ci-workflows/contributions/adrs/adr-702-delivery-ci-workflows-strict-coverage-gate.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/adrs/adr-703-ci-pipeline-node-only-runner.json → delivery-ci-workflows/contributions/adrs/adr-703-delivery-ci-workflows-node-only-runner.json} +1 -1
- package/blueprints/{ci-pipeline/contributions/adrs/adr-704-ci-pipeline-report-shape.json → delivery-ci-workflows/contributions/adrs/adr-704-delivery-ci-workflows-report-shape.json} +3 -3
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
- package/blueprints/{ci-pipeline/contributions/tacs/tac-701-ci-pipeline-gate-runner.json → delivery-ci-workflows/contributions/tacs/tac-701-delivery-ci-workflows-gate-runner.json} +3 -3
- package/blueprints/{ci-pipeline/contributions/tacs/tac-702-ci-pipeline-gate-report.json → delivery-ci-workflows/contributions/tacs/tac-702-delivery-ci-workflows-gate-report.json} +1 -1
- package/blueprints/{ci-pipeline/contributions/tacs/tac-703-ci-pipeline-aggregate-report.json → delivery-ci-workflows/contributions/tacs/tac-703-delivery-ci-workflows-aggregate-report.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +64 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6102.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6102.json} +3 -3
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6103.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6103.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6104.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6104.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6105.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6105.json} +6 -6
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6106.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6106.json} +3 -3
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6107.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6107.json} +5 -5
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6108.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6108.json} +4 -4
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6110.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6110.json} +3 -3
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
- package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +136 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
- package/blueprints/email-smtp-resend/docs/topics.md +1 -1
- package/blueprints/observability-essentials/README.md +2 -2
- package/blueprints/observability-essentials/docs/topics.md +5 -5
- package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/README.md +2 -2
- package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
- package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
- package/blueprints/persistence-data-sqlite/README.md +1 -1
- package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
- package/blueprints/security-auth-clerk/README.md +5 -3
- package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
- package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
- package/blueprints/security-auth-clerk/blueprint.json +1 -1
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/README.md +1 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
- package/blueprints/security-auth-oauth2/README.md +1 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/README.md +1 -1
- package/blueprints/security-secrets-management/docs/topics.md +3 -3
- package/guidance/build-cycle-playbook.md +2 -2
- package/guidance/document-model.md +1 -1
- package/guidance/harness-template.md +13 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +13 -0
- package/package.json +5 -2
- package/rcf/adrs/adr-001.json +1 -1
- package/rcf/adrs/adr-009.json +1 -1
- package/rcf/build-sequence.json +1 -1
- package/rcf/manifest.json +2 -2
- package/rcf/prd.json +2 -2
- package/releases/releases.yaml +116 -0
- package/src/blueprint/apply.js +51 -13
- package/src/blueprint/index.js +12 -0
- package/src/blueprint/library-loader.js +271 -0
- package/src/blueprint/library-registry.js +341 -0
- package/src/blueprint/list.js +38 -4
- package/src/blueprint/shelf-resolver.js +144 -31
- package/src/cli/blueprint-library.js +419 -0
- package/src/cli/blueprint.js +46 -9
- package/src/cli/guidance.js +1 -1
- package/src/cli/help.js +27 -1
- package/src/cli/version.js +673 -0
- package/src/cli/view.js +282 -1
- package/src/server/index.js +3 -0
- package/src/server/routes.js +15 -1
- package/src/server/scope-endpoint.js +105 -0
- package/src/view/live-client.js +253 -6
- package/src/view/scope.js +231 -0
- package/src/view/style.css +42 -0
- package/blueprints/ci-pipeline/README.md +0 -49
- package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
- package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
- package/blueprints/ci-pipeline/blueprint.json +0 -46
- package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
- package/blueprints/ci-pipeline/docs/topics.md +0 -49
- package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
- package/rcf/.identity/profile.md +0 -37
- package/rcf/knowledge/INDEX.md +0 -12
- package/rcf/knowledge/README.md +0 -41
- package/rcf/knowledge/docs/.gitkeep +0 -0
- package/rcf/knowledge/notes/.gitkeep +0 -0
- /package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/pipeline.json +0 -0
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
// Resolve a `rcf define blueprint add <source>` argument to an absolute
|
|
2
2
|
// blueprint directory.
|
|
3
3
|
//
|
|
4
|
-
// Resolution order (Phase 1
|
|
5
|
-
//
|
|
4
|
+
// Resolution order (Phase 1 + Phase 2b external-library registry;
|
|
5
|
+
// see external-blueprint-libraries-spec-2026-08-31.md section 5.2 for
|
|
6
|
+
// the ratified spec text):
|
|
6
7
|
//
|
|
7
|
-
// 1.
|
|
8
|
+
// 1. `@stock/<slug>` is reserved for the packaged shelf. The `@stock/`
|
|
9
|
+
// qualifier strips off and the bare slug resolves against the
|
|
10
|
+
// packaged shelf. This is the recommended long-form.
|
|
11
|
+
// 2. Any other `@<library>/<slug>` (slash form) is rejected. The
|
|
12
|
+
// ratified qualified surface (spec §9.2) is COLON-separated
|
|
13
|
+
// (`<libraryPrefix>:<slug>`); the slash form is a reserved
|
|
14
|
+
// non-canonical shape and the refusal message points at the colon
|
|
15
|
+
// form so the operator sees the right invocation.
|
|
16
|
+
// 3. `<libraryPrefix>:<slug>` (colon form) is the qualified external-
|
|
17
|
+
// library reference (spec §5.2 step 1). The segment before the
|
|
18
|
+
// colon must match a `libraryPrefix` in the project registry at
|
|
19
|
+
// `rcf/blueprint-libraries.json`; the resolver expands to
|
|
20
|
+
// `<library-cache>/blueprints/<segment-after-colon>` and returns
|
|
21
|
+
// an `effectiveSlug` (`<libraryPrefix>-<slug>`) plus the library
|
|
22
|
+
// prefix so the apply layer can stamp the qualified identity on
|
|
23
|
+
// contributions and the applied-blueprint record.
|
|
24
|
+
// 4. Any argument that names a filesystem location - starts with `./`,
|
|
8
25
|
// `../`, `/`, or `~`, or contains a path separator, or names an
|
|
9
26
|
// existing directory - is treated as a PATH and returned unchanged.
|
|
10
27
|
// This preserves the existing local / relative-path semantic every
|
|
11
28
|
// test and existing operator invocation relies on.
|
|
12
|
-
//
|
|
13
|
-
// qualifier strips off and the bare slug resolves against the
|
|
14
|
-
// packaged shelf. This is the recommended long-form.
|
|
15
|
-
// 3. Any other bare kebab slug (`deploy-cloudflare-workers`,
|
|
29
|
+
// 5. Any other bare kebab slug (`deploy-cloudflare-workers`,
|
|
16
30
|
// `application-spa`) resolves against the packaged shelf. This is
|
|
17
31
|
// the sugar the persona reviews asked for; it means the docs and
|
|
18
32
|
// the CLI say the same thing.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// keeps the phase-2 external-libraries mechanism free to land the
|
|
23
|
-
// registry surface later without a breaking rename.
|
|
33
|
+
// 6. Anything that has slipped through here falls through as a path
|
|
34
|
+
// so the loader's own "no blueprint.json found" error names the
|
|
35
|
+
// exact string the operator typed.
|
|
24
36
|
//
|
|
25
37
|
// The packaged shelf lives at `<packageRoot>/blueprints/<slug>/` - the
|
|
26
38
|
// tarball is built with `files: [ "blueprints" ]` and a `prepack`
|
|
@@ -33,6 +45,7 @@ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
|
33
45
|
import { fileURLToPath } from 'node:url';
|
|
34
46
|
|
|
35
47
|
import { rcfError } from '../core/errors/index.js';
|
|
48
|
+
import { findLibrary, readLibraryRegistry } from './library-registry.js';
|
|
36
49
|
|
|
37
50
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
51
|
// packages/rcf-lite/src/blueprint -> packages/rcf-lite
|
|
@@ -41,27 +54,50 @@ const PACKAGED_SHELF = join(PACKAGE_ROOT, 'blueprints');
|
|
|
41
54
|
|
|
42
55
|
const KEBAB_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
43
56
|
const QUALIFIED = /^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
|
|
57
|
+
// Colon-form qualified reference: <libraryPrefix>:<blueprintSlug>. Both
|
|
58
|
+
// sides are kebab slugs (per spec §5.1 / §5.2). Grammar matches the
|
|
59
|
+
// blueprintSlug pattern on each side.
|
|
60
|
+
const COLON_QUALIFIED = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
|
|
44
61
|
|
|
45
62
|
const PATH_HINT = /[\\/]|^~|^\./;
|
|
46
63
|
|
|
47
64
|
/**
|
|
48
65
|
* @typedef {object} ResolvedSource
|
|
49
|
-
* @property {'path' | 'shelf'} kind
|
|
66
|
+
* @property {'path' | 'shelf' | 'library'} kind
|
|
50
67
|
* `path` if the argument was treated as a filesystem path (unchanged);
|
|
51
68
|
* `shelf` if a bare slug or `@stock/<slug>` was resolved against the
|
|
52
|
-
* packaged shelf
|
|
53
|
-
*
|
|
54
|
-
*
|
|
69
|
+
* packaged shelf; `library` if a colon-qualified `<libraryPrefix>:<slug>`
|
|
70
|
+
* was resolved through the project registry to an external-library
|
|
71
|
+
* blueprint directory. The apply.js layer treats every kind the same
|
|
72
|
+
* once resolved (all are absolute directory paths); the kind is
|
|
73
|
+
* exposed for the CLI's diagnostics and (for `library`) so the apply
|
|
74
|
+
* layer can pick up the `effectiveSlug` and `libraryPrefix`.
|
|
55
75
|
* @property {string} resolved Absolute path suitable for `applyBlueprint({ source })`.
|
|
56
76
|
* @property {string} original The argument the operator typed, preserved for
|
|
57
|
-
* diagnostics
|
|
58
|
-
*
|
|
77
|
+
* diagnostics, the conflict-renderer's supersede hint, and (for
|
|
78
|
+
* `library`) as the qualified ref written into `manifest.blueprints[].source`.
|
|
79
|
+
* @property {string} [slug] The bare slug when resolution went through the shelf.
|
|
80
|
+
* @property {string} [libraryPrefix] The library prefix when resolution went through the registry.
|
|
81
|
+
* @property {string} [libraryBlueprintSlug]
|
|
82
|
+
* The blueprint's own bare slug inside the library.
|
|
83
|
+
* @property {string} [effectiveSlug] `<libraryPrefix>-<libraryBlueprintSlug>` when
|
|
84
|
+
* kind is `library` (spec §5.3); the value the
|
|
85
|
+
* apply layer stamps as both the blueprint's
|
|
86
|
+
* namespace and the applied record's `slug`.
|
|
87
|
+
* @property {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} [libraryBands]
|
|
88
|
+
* The library's declared bands, forwarded so the
|
|
89
|
+
* apply-time gate can refuse contributions that
|
|
90
|
+
* fall outside them.
|
|
59
91
|
*/
|
|
60
92
|
|
|
61
93
|
/**
|
|
62
94
|
* @param {string} source - argument as typed by the operator
|
|
63
95
|
* @param {object} [opts]
|
|
64
96
|
* @param {string} [opts.packagedShelf] - override for tests
|
|
97
|
+
* @param {string} [opts.projectRoot] - project root for registry lookups.
|
|
98
|
+
* When omitted, colon-qualified sources refuse with a "no registry
|
|
99
|
+
* context" error. Callers apply-time (CLI, MCP) always supply this;
|
|
100
|
+
* test callers that only exercise the shelf paths may omit it.
|
|
65
101
|
* @returns {Promise<ResolvedSource | import('../core/errors/index.js').RcfError>}
|
|
66
102
|
*/
|
|
67
103
|
export async function resolveBlueprintSource(source, opts = {}) {
|
|
@@ -70,11 +106,11 @@ export async function resolveBlueprintSource(source, opts = {}) {
|
|
|
70
106
|
}
|
|
71
107
|
const packagedShelf = opts.packagedShelf ?? PACKAGED_SHELF;
|
|
72
108
|
|
|
73
|
-
// Rule
|
|
109
|
+
// Rule 1 / 2 FIRST: qualified `@lib/slug` forms carry a `/` that
|
|
74
110
|
// would otherwise trip the path-hint check below. `@stock/<slug>`
|
|
75
|
-
// resolves to the packaged shelf;
|
|
76
|
-
//
|
|
77
|
-
//
|
|
111
|
+
// resolves to the packaged shelf; every other slash form is the
|
|
112
|
+
// non-canonical shape and refuses, pointing at the ratified colon
|
|
113
|
+
// form (spec §9.2).
|
|
78
114
|
const qualified = QUALIFIED.exec(source);
|
|
79
115
|
if (qualified) {
|
|
80
116
|
const [, library, slug] = qualified;
|
|
@@ -84,13 +120,30 @@ export async function resolveBlueprintSource(source, opts = {}) {
|
|
|
84
120
|
return rcfError({
|
|
85
121
|
kind: 'usage',
|
|
86
122
|
message: (
|
|
87
|
-
`blueprint source '${source}'
|
|
88
|
-
+
|
|
123
|
+
`blueprint source '${source}' uses the slash-qualified '@<library>/<slug>' shape, which is not the ratified external-library reference form. `
|
|
124
|
+
+ `Use the colon form '${library}:${slug}' after registering the library with 'rcf define blueprint library add <ref>', or '@stock/<slug>' for the packaged shelf.`
|
|
89
125
|
),
|
|
90
126
|
});
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
// Rule
|
|
129
|
+
// Rule 3: colon-qualified `<libraryPrefix>:<slug>` is the ratified
|
|
130
|
+
// external-library reference (spec §5.2). We check before the path
|
|
131
|
+
// hint because the colon-form is unambiguous: a colon is not a
|
|
132
|
+
// filesystem separator on any platform we target, and the kebab
|
|
133
|
+
// grammar on both sides rules out an accidental match with, for
|
|
134
|
+
// example, a Windows drive letter (`C:\path`, uppercase, backslash).
|
|
135
|
+
const colon = COLON_QUALIFIED.exec(source);
|
|
136
|
+
if (colon) {
|
|
137
|
+
const [, libraryPrefix, blueprintSlug] = colon;
|
|
138
|
+
return resolveLibraryQualified({
|
|
139
|
+
libraryPrefix,
|
|
140
|
+
blueprintSlug,
|
|
141
|
+
original: source,
|
|
142
|
+
projectRoot: opts.projectRoot,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Rule 4: path-looking arguments are passed through unchanged. We look
|
|
94
147
|
// at the SHAPE only (does it contain a separator, does it start with a
|
|
95
148
|
// relative-path marker, is it absolute), so an existing operator with a
|
|
96
149
|
// `./blueprints/foo` invocation keeps the current behaviour byte-for-byte.
|
|
@@ -98,19 +151,79 @@ export async function resolveBlueprintSource(source, opts = {}) {
|
|
|
98
151
|
return { kind: 'path', resolved: resolve(source), original: source };
|
|
99
152
|
}
|
|
100
153
|
|
|
101
|
-
// Rule
|
|
154
|
+
// Rule 5: any other bare kebab token is a shelf slug.
|
|
102
155
|
if (KEBAB_SLUG.test(source)) {
|
|
103
156
|
return resolveShelfSlug(source, source, packagedShelf);
|
|
104
157
|
}
|
|
105
158
|
|
|
106
|
-
//
|
|
107
|
-
// qualified library slug, nor a bare kebab slug. Treat it as a
|
|
108
|
-
// the loader emits the familiar "no blueprint.json found"
|
|
109
|
-
// the exact string the operator typed; that keeps the
|
|
110
|
-
// close to what they wrote rather than adding a
|
|
159
|
+
// Rule 6: anything that has slipped through here is neither a path,
|
|
160
|
+
// nor a qualified library slug, nor a bare kebab slug. Treat it as a
|
|
161
|
+
// path so the loader emits the familiar "no blueprint.json found"
|
|
162
|
+
// error against the exact string the operator typed; that keeps the
|
|
163
|
+
// failure locus close to what they wrote rather than adding a
|
|
164
|
+
// resolver-specific class.
|
|
111
165
|
return { kind: 'path', resolved: resolve(source), original: source };
|
|
112
166
|
}
|
|
113
167
|
|
|
168
|
+
async function resolveLibraryQualified({ libraryPrefix, blueprintSlug, original, projectRoot }) {
|
|
169
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
170
|
+
return rcfError({
|
|
171
|
+
kind: 'usage',
|
|
172
|
+
message: `blueprint source '${original}' uses the qualified library form but the resolver was called without a projectRoot; the registry at rcf/blueprint-libraries.json cannot be located.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
176
|
+
if (registry.kind) return registry; // RcfError
|
|
177
|
+
const entry = findLibrary(registry, libraryPrefix);
|
|
178
|
+
if (!entry) {
|
|
179
|
+
const known = registry.libraries.map((l) => l.libraryPrefix);
|
|
180
|
+
const hint = known.length > 0
|
|
181
|
+
? ` Registered libraries: ${known.join(', ')}.`
|
|
182
|
+
: ' No libraries are registered on this project; run `rcf define blueprint library add <ref>` first.';
|
|
183
|
+
return rcfError({
|
|
184
|
+
kind: 'usage',
|
|
185
|
+
message: `blueprint source '${original}' names library '${libraryPrefix}' which is not registered on this project.${hint}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const libraryRoot = isAbsolute(entry.cachePath) ? entry.cachePath : join(projectRoot, entry.cachePath);
|
|
189
|
+
const bpEntry = findBlueprintEntry(entry, blueprintSlug);
|
|
190
|
+
if (!bpEntry) {
|
|
191
|
+
return rcfError({
|
|
192
|
+
kind: 'usage',
|
|
193
|
+
message: `blueprint source '${original}': library '${libraryPrefix}' has no blueprint with slug '${blueprintSlug}' in its registered manifest snapshot; the operator may need to re-run 'rcf define blueprint library add <ref>' to pick up a newer library.`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const resolved = join(libraryRoot, bpEntry.path);
|
|
197
|
+
if (!existsSync(resolved)) {
|
|
198
|
+
return rcfError({
|
|
199
|
+
kind: 'usage',
|
|
200
|
+
message: `blueprint source '${original}': library '${libraryPrefix}' blueprint '${blueprintSlug}' resolved to ${resolved} but that path is missing. Run 'rcf define blueprint library refresh ${libraryPrefix}'.`,
|
|
201
|
+
filePath: resolved,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: 'library',
|
|
206
|
+
resolved,
|
|
207
|
+
original,
|
|
208
|
+
libraryPrefix,
|
|
209
|
+
libraryBlueprintSlug: blueprintSlug,
|
|
210
|
+
effectiveSlug: `${libraryPrefix}-${blueprintSlug}`,
|
|
211
|
+
libraryBands: entry.bands,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function findBlueprintEntry(entry, blueprintSlug) {
|
|
216
|
+
// Registry entries snapshot the library's blueprints[] at add time
|
|
217
|
+
// (spec §4.2); the snapshot is what the resolver consults. When the
|
|
218
|
+
// snapshot is absent (a future registry-version compatibility case),
|
|
219
|
+
// fall through and let the on-disk library dictate the layout by
|
|
220
|
+
// relying on the conventional blueprints/<slug>/ path.
|
|
221
|
+
if (!Array.isArray(entry.blueprints)) {
|
|
222
|
+
return { slug: blueprintSlug, path: `blueprints/${blueprintSlug}` };
|
|
223
|
+
}
|
|
224
|
+
return entry.blueprints.find((b) => b.slug === blueprintSlug);
|
|
225
|
+
}
|
|
226
|
+
|
|
114
227
|
async function resolveShelfSlug(slug, original, packagedShelf) {
|
|
115
228
|
const candidate = join(packagedShelf, slug);
|
|
116
229
|
if (!existsSync(candidate)) {
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// `rcf define blueprint library <verb>` CLI.
|
|
2
|
+
//
|
|
3
|
+
// Verbs (Phase 2b):
|
|
4
|
+
// add register an external library on this project (local source in
|
|
5
|
+
// 2b; network fetchers land in 2c)
|
|
6
|
+
// list list registered libraries
|
|
7
|
+
// remove unregister a library (refuses when any applied blueprint on
|
|
8
|
+
// the project came through the library)
|
|
9
|
+
// refresh re-validate an already-registered library's on-disk shape
|
|
10
|
+
//
|
|
11
|
+
// Spec: external-blueprint-libraries-spec-2026-08-31.md sections 4, 8.
|
|
12
|
+
|
|
13
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
14
|
+
import { stat } from 'node:fs/promises';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
import { isRcfError } from '#core/errors';
|
|
18
|
+
import { findProjectRoot } from '../view/index.js';
|
|
19
|
+
import { loadLibrary } from '../blueprint/library-loader.js';
|
|
20
|
+
import {
|
|
21
|
+
detectBandOverlap,
|
|
22
|
+
detectPrefixCollision,
|
|
23
|
+
findLibrary,
|
|
24
|
+
loadCoreBandReservations,
|
|
25
|
+
readLibraryRegistry,
|
|
26
|
+
writeLibraryRegistry,
|
|
27
|
+
} from '../blueprint/library-registry.js';
|
|
28
|
+
import { knownShelfSlugs, packagedShelfPath } from '../blueprint/shelf-resolver.js';
|
|
29
|
+
import { walkTree } from '#core/store';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Union of core-shelf blueprint slugs used for the prefix-collision
|
|
33
|
+
* gate. The packaged-shelf directory is copied into the tarball at
|
|
34
|
+
* prepack time (`files: ['blueprints']`), so a source checkout does
|
|
35
|
+
* not have `packages/rcf-lite/blueprints/` on disk; falling back to
|
|
36
|
+
* the shipped `data/core-band-reservations.json` keeps the gate
|
|
37
|
+
* effective in both shapes.
|
|
38
|
+
*/
|
|
39
|
+
async function collectCoreSlugs() {
|
|
40
|
+
const fromShelf = await knownShelfSlugs(packagedShelfPath()).catch(() => []);
|
|
41
|
+
const reservations = await loadCoreBandReservations();
|
|
42
|
+
const slugs = new Set(fromShelf);
|
|
43
|
+
for (const row of reservations.ac ?? []) if (row.blueprint) slugs.add(row.blueprint);
|
|
44
|
+
for (const row of reservations.suffixBlocks ?? []) if (row.blueprint) slugs.add(row.blueprint);
|
|
45
|
+
return [...slugs];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const LIBRARY_HELP = `Usage: rcf define blueprint library <verb> [options]
|
|
49
|
+
|
|
50
|
+
Verbs:
|
|
51
|
+
add <ref> Register an external library on this project.
|
|
52
|
+
<ref> is either a local absolute or relative path
|
|
53
|
+
to a library root (a directory containing
|
|
54
|
+
library.json). Fetches metadata, runs review-on-
|
|
55
|
+
add, and writes an entry to
|
|
56
|
+
rcf/blueprint-libraries.json.
|
|
57
|
+
|
|
58
|
+
Phase 2b covers local sources; the git and
|
|
59
|
+
tarball fetchers land in Phase 2c. A private-repo
|
|
60
|
+
library today is registered via a local clone.
|
|
61
|
+
|
|
62
|
+
list [--json] List every registered library on this project
|
|
63
|
+
(prefix, source, publisher, blueprint count).
|
|
64
|
+
|
|
65
|
+
remove <prefix> Unregister a library. Refuses when any applied
|
|
66
|
+
blueprint on the project came through this
|
|
67
|
+
library.
|
|
68
|
+
|
|
69
|
+
refresh <prefix> Re-validate an already-registered library's
|
|
70
|
+
on-disk shape. Phase 2b re-reads the local
|
|
71
|
+
library.json and reports any drift from the
|
|
72
|
+
registry snapshot; phase 2c will re-fetch a git
|
|
73
|
+
or tarball source and verify the sha.
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--prefix <slug> (add) Override the library's declared prefix.
|
|
77
|
+
Rarely needed; only useful when two libraries
|
|
78
|
+
collide on prefix locally.
|
|
79
|
+
--i-have-reviewed (add) Skip the interactive review-on-add prompt.
|
|
80
|
+
Required companion to --no-review when scripting;
|
|
81
|
+
the two-flag form keeps a library-add trust
|
|
82
|
+
decision loud (spec §9.7).
|
|
83
|
+
--no-review (add) Bypass the interactive prompt. Requires
|
|
84
|
+
--i-have-reviewed.
|
|
85
|
+
--json (add, list) Emit machine-readable JSON.
|
|
86
|
+
--dry-run Print intended writes without executing.
|
|
87
|
+
--quiet Suppress non-error stdout.
|
|
88
|
+
--help Print this help.
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const LIBRARY_OPTION_SPEC = {
|
|
92
|
+
prefix: { type: 'string' },
|
|
93
|
+
'i-have-reviewed': { type: 'boolean' },
|
|
94
|
+
'no-review': { type: 'boolean' },
|
|
95
|
+
json: { type: 'boolean' },
|
|
96
|
+
'dry-run': { type: 'boolean' },
|
|
97
|
+
quiet: { type: 'boolean' },
|
|
98
|
+
help: { type: 'boolean' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle `rcf define blueprint library <verb>`.
|
|
103
|
+
*
|
|
104
|
+
* @param {import('node:util').ParseArgsConfig} parsed - already-parsed argv (see cli/blueprint.js)
|
|
105
|
+
* @param {string[]} rest - positional args after `library`
|
|
106
|
+
* @param {object} deps
|
|
107
|
+
* @returns {Promise<number>}
|
|
108
|
+
*/
|
|
109
|
+
export async function handleLibraryVerb(parsed, rest, deps) {
|
|
110
|
+
const stdout = deps.stdout;
|
|
111
|
+
const stderr = deps.stderr;
|
|
112
|
+
const cwd = deps.cwd;
|
|
113
|
+
const now = deps.now ?? new Date();
|
|
114
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
115
|
+
|
|
116
|
+
if (parsed.values.help || rest.length === 0) {
|
|
117
|
+
stdout.write(LIBRARY_HELP);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
const verb = rest[0];
|
|
121
|
+
const args = rest.slice(1);
|
|
122
|
+
|
|
123
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
124
|
+
if (!projectRoot) {
|
|
125
|
+
stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (verb === 'add') return handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin });
|
|
130
|
+
if (verb === 'list') return handleList({ parsed, projectRoot, stdout, stderr });
|
|
131
|
+
if (verb === 'remove') return handleRemove({ args, parsed, projectRoot, stdout, stderr });
|
|
132
|
+
if (verb === 'refresh') return handleRefresh({ args, parsed, projectRoot, stdout, stderr });
|
|
133
|
+
|
|
134
|
+
stderr.write(`[error] blueprint library: unknown verb '${verb}'\n`);
|
|
135
|
+
stderr.write(LIBRARY_HELP);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin }) {
|
|
140
|
+
if (args.length === 0) {
|
|
141
|
+
stderr.write('[error] blueprint library add: missing <ref>\n');
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
const ref = args[0];
|
|
145
|
+
|
|
146
|
+
// Phase 2b: local sources only. Fetchers land in 2c.
|
|
147
|
+
const kind = classifySourceRef(ref);
|
|
148
|
+
if (kind !== 'local') {
|
|
149
|
+
stderr.write(
|
|
150
|
+
`[error] blueprint library add: source kind '${kind}' requires the network fetchers landing in Phase 2c. `
|
|
151
|
+
+ 'In 2b, register the library from a local path (a directory carrying library.json). '
|
|
152
|
+
+ 'For private git repos today, clone the repo locally and point add at the clone.\n',
|
|
153
|
+
);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const localRoot = isAbsolute(ref) ? ref : resolve(projectRoot, ref);
|
|
158
|
+
let libStat;
|
|
159
|
+
try {
|
|
160
|
+
libStat = await stat(localRoot);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
stderr.write(`[error] blueprint library add: path '${localRoot}' cannot be read: ${err.message}\n`);
|
|
163
|
+
return 2;
|
|
164
|
+
}
|
|
165
|
+
if (!libStat.isDirectory()) {
|
|
166
|
+
stderr.write(`[error] blueprint library add: path '${localRoot}' is not a directory.\n`);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const library = await loadLibrary(localRoot, { validateBlueprints: true });
|
|
171
|
+
if (isRcfError(library)) {
|
|
172
|
+
stderr.write(`[error] blueprint library add: ${library.message}\n`);
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const libraryPrefix = typeof parsed.values.prefix === 'string' && parsed.values.prefix.length > 0
|
|
177
|
+
? parsed.values.prefix
|
|
178
|
+
: library.libraryPrefix;
|
|
179
|
+
|
|
180
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
181
|
+
if (isRcfError(registry)) {
|
|
182
|
+
stderr.write(`[error] blueprint library add: ${registry.message}\n`);
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
if (findLibrary(registry, libraryPrefix)) {
|
|
186
|
+
stderr.write(`[error] blueprint library add: libraryPrefix '${libraryPrefix}' is already registered on this project. Use 'library refresh' or 'library remove' first.\n`);
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
const coreSlugs = await collectCoreSlugs();
|
|
190
|
+
const prefixErr = detectPrefixCollision({ libraryPrefix, registry, coreSlugs });
|
|
191
|
+
if (prefixErr) {
|
|
192
|
+
stderr.write(`[error] blueprint library add: ${prefixErr.message}\n`);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
const coreReservations = await loadCoreBandReservations();
|
|
196
|
+
const bandErr = detectBandOverlap({
|
|
197
|
+
candidate: { libraryPrefix, bands: library.bands },
|
|
198
|
+
registry,
|
|
199
|
+
coreReservations,
|
|
200
|
+
});
|
|
201
|
+
if (bandErr) {
|
|
202
|
+
stderr.write(`[error] blueprint library add: ${bandErr.message}\n`);
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const wantsReview = parsed.values['no-review'] !== true;
|
|
207
|
+
const iHaveReviewed = parsed.values['i-have-reviewed'] === true;
|
|
208
|
+
if (parsed.values['no-review'] === true && !iHaveReviewed) {
|
|
209
|
+
stderr.write('[error] blueprint library add: --no-review requires --i-have-reviewed (spec §9.7: the two-flag form keeps the trust decision loud).\n');
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
if (wantsReview) {
|
|
213
|
+
printReview({ stdout, ref, library, libraryPrefix, coreReservations });
|
|
214
|
+
if (!iHaveReviewed) {
|
|
215
|
+
const proceed = await prompt(stdin, stdout, 'Proceed with add? [y/N] ');
|
|
216
|
+
if (!/^y(es)?$/i.test((proceed ?? '').trim())) {
|
|
217
|
+
stdout.write('[blueprint library] aborted by operator; no registry entry written.\n');
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const entry = {
|
|
224
|
+
libraryPrefix,
|
|
225
|
+
sourceKind: 'local',
|
|
226
|
+
sourceRef: localRoot,
|
|
227
|
+
displayName: library.displayName,
|
|
228
|
+
publisher: { ...library.publisher },
|
|
229
|
+
libraryRef: library.libraryRef,
|
|
230
|
+
bands: library.bands,
|
|
231
|
+
blueprints: library.blueprints.map((b) => ({ slug: b.slug, path: b.path })),
|
|
232
|
+
addedAt: now.toISOString(),
|
|
233
|
+
reviewedBy: 'operator',
|
|
234
|
+
provenance: { tier: 'local' },
|
|
235
|
+
cachePath: localRoot,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const nextRegistry = {
|
|
239
|
+
registryVersion: registry.registryVersion,
|
|
240
|
+
libraries: [...registry.libraries, entry],
|
|
241
|
+
};
|
|
242
|
+
const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
|
|
243
|
+
if (isRcfError(write)) {
|
|
244
|
+
stderr.write(`[error] blueprint library add: ${write.message}\n`);
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
if (parsed.values.json) {
|
|
248
|
+
stdout.write(`${JSON.stringify({
|
|
249
|
+
added: !parsed.values['dry-run'],
|
|
250
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
251
|
+
libraryPrefix: entry.libraryPrefix,
|
|
252
|
+
blueprintCount: entry.blueprints.length,
|
|
253
|
+
registryPath: write.path,
|
|
254
|
+
})}\n`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
if (!parsed.values.quiet) {
|
|
258
|
+
if (parsed.values['dry-run']) {
|
|
259
|
+
stdout.write(`[blueprint library] dry-run: would add '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
|
|
260
|
+
} else {
|
|
261
|
+
stdout.write(`[blueprint library] added '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleList({ parsed, projectRoot, stdout, stderr }) {
|
|
268
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
269
|
+
if (isRcfError(registry)) {
|
|
270
|
+
stderr.write(`[error] blueprint library list: ${registry.message}\n`);
|
|
271
|
+
return 2;
|
|
272
|
+
}
|
|
273
|
+
if (parsed.values.json) {
|
|
274
|
+
stdout.write(`${JSON.stringify({
|
|
275
|
+
registryVersion: registry.registryVersion,
|
|
276
|
+
libraries: registry.libraries.map((l) => ({
|
|
277
|
+
libraryPrefix: l.libraryPrefix,
|
|
278
|
+
sourceKind: l.sourceKind,
|
|
279
|
+
sourceRef: l.sourceRef,
|
|
280
|
+
publisher: l.publisher,
|
|
281
|
+
libraryRef: l.libraryRef,
|
|
282
|
+
blueprintCount: Array.isArray(l.blueprints) ? l.blueprints.length : 0,
|
|
283
|
+
addedAt: l.addedAt,
|
|
284
|
+
})),
|
|
285
|
+
}, null, 2)}\n`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (registry.libraries.length === 0) {
|
|
289
|
+
if (!parsed.values.quiet) stdout.write('[blueprint library] no libraries registered on this project.\n');
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
for (const l of registry.libraries) {
|
|
293
|
+
const count = Array.isArray(l.blueprints) ? l.blueprints.length : 0;
|
|
294
|
+
stdout.write(`${l.libraryPrefix}\t${l.sourceKind}\t${l.publisher.displayName}\t${count} blueprint(s)\t${l.sourceRef}\n`);
|
|
295
|
+
}
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function handleRemove({ args, parsed, projectRoot, stdout, stderr }) {
|
|
300
|
+
if (args.length === 0) {
|
|
301
|
+
stderr.write('[error] blueprint library remove: missing <libraryPrefix>\n');
|
|
302
|
+
return 2;
|
|
303
|
+
}
|
|
304
|
+
const libraryPrefix = args[0];
|
|
305
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
306
|
+
if (isRcfError(registry)) {
|
|
307
|
+
stderr.write(`[error] blueprint library remove: ${registry.message}\n`);
|
|
308
|
+
return 2;
|
|
309
|
+
}
|
|
310
|
+
const entry = findLibrary(registry, libraryPrefix);
|
|
311
|
+
if (!entry) {
|
|
312
|
+
stderr.write(`[error] blueprint library remove: library '${libraryPrefix}' is not registered.\n`);
|
|
313
|
+
return 2;
|
|
314
|
+
}
|
|
315
|
+
// Refuse when any applied blueprint on the project came through the
|
|
316
|
+
// library. Signal: `manifest.blueprints[].source` starts with `<prefix>:`.
|
|
317
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
318
|
+
if (errors.length > 0) {
|
|
319
|
+
for (const e of errors) stderr.write(`[tree] ${e.kind}: ${e.message}\n`);
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
const referring = (tree.manifest?.blueprints ?? []).filter((b) => typeof b.source === 'string' && b.source.startsWith(`${libraryPrefix}:`));
|
|
323
|
+
if (referring.length > 0) {
|
|
324
|
+
stderr.write(`[error] blueprint library remove: ${referring.length} applied blueprint(s) came through '${libraryPrefix}':\n`);
|
|
325
|
+
for (const r of referring) stderr.write(` ${r.slug} <- ${r.source}\n`);
|
|
326
|
+
stderr.write('remove those first (rcf define blueprint remove <slug>) or explicitly force with an escalation flag (not in v1).\n');
|
|
327
|
+
return 3;
|
|
328
|
+
}
|
|
329
|
+
const nextRegistry = {
|
|
330
|
+
registryVersion: registry.registryVersion,
|
|
331
|
+
libraries: registry.libraries.filter((l) => l.libraryPrefix !== libraryPrefix),
|
|
332
|
+
};
|
|
333
|
+
const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
|
|
334
|
+
if (isRcfError(write)) {
|
|
335
|
+
stderr.write(`[error] blueprint library remove: ${write.message}\n`);
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
if (!parsed.values.quiet) {
|
|
339
|
+
stdout.write(`[blueprint library] removed '${libraryPrefix}' from ${write.path}.\n`);
|
|
340
|
+
}
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function handleRefresh({ args, parsed, projectRoot, stdout, stderr }) {
|
|
345
|
+
if (args.length === 0) {
|
|
346
|
+
stderr.write('[error] blueprint library refresh: missing <libraryPrefix>\n');
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
const libraryPrefix = args[0];
|
|
350
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
351
|
+
if (isRcfError(registry)) {
|
|
352
|
+
stderr.write(`[error] blueprint library refresh: ${registry.message}\n`);
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
const entry = findLibrary(registry, libraryPrefix);
|
|
356
|
+
if (!entry) {
|
|
357
|
+
stderr.write(`[error] blueprint library refresh: library '${libraryPrefix}' is not registered.\n`);
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
if (entry.sourceKind !== 'local') {
|
|
361
|
+
stderr.write(`[error] blueprint library refresh: source kind '${entry.sourceKind}' requires the network fetchers landing in Phase 2c.\n`);
|
|
362
|
+
return 2;
|
|
363
|
+
}
|
|
364
|
+
const library = await loadLibrary(entry.cachePath, { validateBlueprints: true });
|
|
365
|
+
if (isRcfError(library)) {
|
|
366
|
+
stderr.write(`[error] blueprint library refresh: ${library.message}\n`);
|
|
367
|
+
return 2;
|
|
368
|
+
}
|
|
369
|
+
// Detect drift from the registry snapshot on load-bearing fields.
|
|
370
|
+
const drifted = [];
|
|
371
|
+
if (library.libraryPrefix !== entry.libraryPrefix) drifted.push(`libraryPrefix '${entry.libraryPrefix}' -> '${library.libraryPrefix}'`);
|
|
372
|
+
if (library.libraryRef !== entry.libraryRef) drifted.push(`libraryRef '${entry.libraryRef}' -> '${library.libraryRef}'`);
|
|
373
|
+
if (JSON.stringify(library.bands) !== JSON.stringify(entry.bands)) drifted.push('bands changed');
|
|
374
|
+
if (drifted.length > 0) {
|
|
375
|
+
stderr.write(`[blueprint library refresh] '${libraryPrefix}' has drifted from the registered snapshot:\n`);
|
|
376
|
+
for (const d of drifted) stderr.write(` ${d}\n`);
|
|
377
|
+
stderr.write("re-run 'rcf define blueprint library add <ref>' to pick up the newer library (spec §10.1: freshness is advisory, adoption is an operator act).\n");
|
|
378
|
+
return 3;
|
|
379
|
+
}
|
|
380
|
+
if (!parsed.values.quiet) {
|
|
381
|
+
stdout.write(`[blueprint library] '${libraryPrefix}' refresh clean: on-disk library matches the registry snapshot.\n`);
|
|
382
|
+
}
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function classifySourceRef(ref) {
|
|
387
|
+
if (ref.startsWith('git+') || ref.startsWith('git@') || ref.startsWith('https://') || ref.startsWith('http://') || ref.startsWith('ssh://')) return 'git';
|
|
388
|
+
if (ref.endsWith('.tar.gz') || ref.endsWith('.tgz') || ref.endsWith('.tar')) return 'tarball';
|
|
389
|
+
return 'local';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function printReview({ stdout, ref, library, libraryPrefix, coreReservations }) {
|
|
393
|
+
const suffix = (library.bands.suffixBlocks ?? []).map((b) => `${b.kind} ${b.start}-${b.end}`).join(', ');
|
|
394
|
+
stdout.write(`\nREVIEW - you are about to add this library to the project registry.\n\n`);
|
|
395
|
+
stdout.write(` Library : ${library.displayName}\n`);
|
|
396
|
+
stdout.write(` Prefix : ${libraryPrefix}\n`);
|
|
397
|
+
stdout.write(` Publisher : ${library.publisher.displayName}${library.publisher.contact ? ` <${library.publisher.contact}>` : ''}\n`);
|
|
398
|
+
stdout.write(` Source : ${ref} (local)\n`);
|
|
399
|
+
stdout.write(` Library ref : ${library.libraryRef}\n`);
|
|
400
|
+
stdout.write(` AC band : ${library.bands.ac.start} - ${library.bands.ac.end}\n`);
|
|
401
|
+
if (suffix.length > 0) stdout.write(` Suffix blocks: ${suffix}\n`);
|
|
402
|
+
stdout.write(`\n Blueprints on this library:\n`);
|
|
403
|
+
for (const bp of library.blueprints) {
|
|
404
|
+
stdout.write(` ${libraryPrefix}:${bp.slug}\n`);
|
|
405
|
+
}
|
|
406
|
+
stdout.write(`\n Provenance : local (dev use)\n`);
|
|
407
|
+
stdout.write(` Band check : cross-checked ${coreReservations.ac.length} core AC row(s), ${coreReservations.suffixBlocks.length} core suffix block(s); no overlap.\n`);
|
|
408
|
+
stdout.write(`\n`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function prompt(stdin, stdout, question) {
|
|
412
|
+
return new Promise((resolveP) => {
|
|
413
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
414
|
+
rl.question(question, (answer) => {
|
|
415
|
+
rl.close();
|
|
416
|
+
resolveP(answer);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|