@intentius/chant 0.79.0 → 0.81.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/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +30 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts +5 -0
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +13 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +13 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +44 -3
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +8 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +6 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/vendor.d.ts +5 -0
- package/dist/cli/commands/vendor.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/init.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +20 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/vendor.d.ts +8 -3
- package/dist/cli/handlers/vendor.d.ts.map +1 -1
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +14 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +46 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts +10 -0
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts +16 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/gate-plan.d.ts +40 -0
- package/dist/components/gate-plan.d.ts.map +1 -0
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +264 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/run.d.ts.map +1 -1
- package/dist/components/verbs/build-archive.d.ts +22 -11
- package/dist/components/verbs/build-archive.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config-static.d.ts +59 -0
- package/dist/config-static.d.ts.map +1 -0
- package/dist/config.d.ts +104 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/content-digest.d.ts +17 -0
- package/dist/content-digest.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +48 -4
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +32 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts +20 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +20 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/walk.d.ts +119 -0
- package/dist/discovery/walk.d.ts.map +1 -0
- package/dist/graph-ir.d.ts +17 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/join-key.d.ts +31 -0
- package/dist/join-key.d.ts.map +1 -0
- package/dist/lexicon-module.d.ts +110 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +82 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts +5 -1
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +20 -5
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +64 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +8 -1
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +59 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/member-ledger.d.ts +67 -0
- package/dist/lifecycle/member-ledger.d.ts.map +1 -0
- package/dist/lifecycle/plan-digest.d.ts +4 -3
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +42 -1
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +90 -5
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +32 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +75 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +16 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/project-root.d.ts +26 -0
- package/dist/project-root.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
- package/dist/workspace/checks/generated.d.ts +156 -0
- package/dist/workspace/checks/generated.d.ts.map +1 -0
- package/dist/workspace/checks/ledgers.d.ts +78 -0
- package/dist/workspace/checks/ledgers.d.ts.map +1 -0
- package/dist/workspace/checks/links.d.ts +25 -0
- package/dist/workspace/checks/links.d.ts.map +1 -0
- package/dist/workspace/checks/pipelines.d.ts +76 -0
- package/dist/workspace/checks/pipelines.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +161 -0
- package/dist/workspace/checks.d.ts.map +1 -0
- package/dist/workspace/compose-graph.d.ts +115 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -0
- package/dist/workspace/compose-reports.d.ts +68 -0
- package/dist/workspace/compose-reports.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +220 -0
- package/dist/workspace/declaration.d.ts.map +1 -0
- package/dist/workspace/declaration.schema.json +434 -0
- package/dist/workspace/generated-files.d.ts +65 -0
- package/dist/workspace/generated-files.d.ts.map +1 -0
- package/dist/workspace/glob.d.ts +13 -0
- package/dist/workspace/glob.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +75 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +96 -0
- package/dist/workspace/init.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +38 -0
- package/dist/workspace/jsonc.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +192 -0
- package/dist/workspace/kinds.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +84 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -0
- package/dist/workspace/lineage-cli.d.ts +31 -0
- package/dist/workspace/lineage-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +101 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -0
- package/dist/workspace/lineage-lock.d.ts +258 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -0
- package/dist/workspace/lineage-merge.d.ts +14 -0
- package/dist/workspace/lineage-merge.d.ts.map +1 -0
- package/dist/workspace/lineage-migrations.d.ts +178 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -0
- package/dist/workspace/lineage-update.d.ts +59 -0
- package/dist/workspace/lineage-update.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +146 -0
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
- package/dist/workspace/lineage-vendor.d.ts +63 -0
- package/dist/workspace/lineage-vendor.d.ts.map +1 -0
- package/dist/workspace/lineage-version.d.ts +32 -0
- package/dist/workspace/lineage-version.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +150 -0
- package/dist/workspace/links.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +99 -0
- package/dist/workspace/ls.d.ts.map +1 -0
- package/dist/workspace/member-commands.d.ts +139 -0
- package/dist/workspace/member-commands.d.ts.map +1 -0
- package/dist/workspace/member-handles.d.ts +54 -0
- package/dist/workspace/member-handles.d.ts.map +1 -0
- package/dist/workspace/member-pipeline.d.ts +143 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -0
- package/dist/workspace/member-run.d.ts +67 -0
- package/dist/workspace/member-run.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +65 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -0
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +81 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/dist/workspace/root-refusal.d.ts +53 -0
- package/dist/workspace/root-refusal.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +150 -0
- package/dist/workspace/status.d.ts.map +1 -0
- package/dist/workspace/template-manifest.d.ts +90 -0
- package/dist/workspace/template-manifest.d.ts.map +1 -0
- package/dist/workspace/tree.d.ts +36 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts +67 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +111 -0
- package/dist/workspace/trust/policy.d.ts.map +1 -0
- package/dist/workspace/trust/provenance.d.ts +67 -0
- package/dist/workspace/trust/provenance.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +43 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
- package/dist/workspace/trust/test-repo.d.ts +42 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -0
- package/dist/workspace/trust/verify-cli.d.ts +9 -0
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
- package/dist/workspace/trust/verify.d.ts +66 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -0
- package/dist/workspace/which-chant.d.ts +58 -0
- package/dist/workspace/which-chant.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +133 -0
- package/package.json +4 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +102 -36
- package/src/audit/report-html.ts +3 -1
- package/src/audit/report-model.ts +14 -1
- package/src/audit/terraform-state.test.ts +81 -1
- package/src/audit/terraform-state.ts +25 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-truncation.test.ts +167 -0
- package/src/cli/commands/audit.ts +132 -29
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/check-lexicon.ts +24 -0
- package/src/cli/commands/doctor.test.ts +30 -0
- package/src/cli/commands/doctor.ts +21 -0
- package/src/cli/commands/import-agents.ts +15 -4
- package/src/cli/commands/init.ts +76 -25
- package/src/cli/commands/lint.test.ts +52 -1
- package/src/cli/commands/lint.ts +41 -71
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +18 -5
- package/src/cli/commands/vendor.ts +11 -6
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
- package/src/cli/handlers/build-member.test.ts +127 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +35 -1
- package/src/cli/handlers/components.test.ts +127 -0
- package/src/cli/handlers/components.ts +70 -8
- package/src/cli/handlers/dev.ts +6 -1
- package/src/cli/handlers/fan-out.ts +9 -2
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/init.ts +54 -1
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.test.ts +68 -0
- package/src/cli/handlers/operator.ts +39 -10
- package/src/cli/handlers/promote.test.ts +377 -0
- package/src/cli/handlers/promote.ts +391 -0
- package/src/cli/handlers/run-generate-member.test.ts +83 -0
- package/src/cli/handlers/run-generate.test.ts +158 -0
- package/src/cli/handlers/run-generate.ts +168 -0
- package/src/cli/handlers/run.test.ts +47 -0
- package/src/cli/handlers/run.ts +41 -6
- package/src/cli/handlers/vendor.ts +147 -43
- package/src/cli/main.test.ts +137 -1
- package/src/cli/main.ts +290 -24
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +43 -5
- package/src/cli/registry.ts +46 -1
- package/src/cli/static-config-read.test.ts +208 -0
- package/src/codegen/fetch.test.ts +63 -1
- package/src/codegen/fetch.ts +30 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +9 -3
- package/src/components/discover.test.ts +27 -57
- package/src/components/discover.ts +20 -51
- package/src/components/driver.test.ts +90 -12
- package/src/components/driver.ts +43 -5
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/gate-plan.ts +53 -0
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +645 -0
- package/src/components/promote.ts +641 -0
- package/src/components/sandbox/driver.ts +8 -8
- package/src/components/sandbox/run.ts +4 -1
- package/src/components/verbs/build-archive.ts +27 -21
- package/src/components/verbs/run-agent.ts +3 -5
- package/src/config-static.test.ts +136 -0
- package/src/config-static.ts +232 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +161 -11
- package/src/content-digest.test.ts +120 -0
- package/src/content-digest.ts +21 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +91 -59
- package/src/discovery/fold-import.ts +127 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/sandbox/driver.ts +73 -35
- package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
- package/src/discovery/sandbox/fork.ts +83 -43
- package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/discovery/walk.test.ts +263 -0
- package/src/discovery/walk.ts +334 -0
- package/src/effect-receipt.ts +2 -2
- package/src/graph-ir.ts +18 -1
- package/src/join-key.test.ts +25 -0
- package/src/join-key.ts +39 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +215 -0
- package/src/lexicon.ts +105 -1
- package/src/lifecycle/build-ledger-store.ts +13 -2
- package/src/lifecycle/digest.ts +9 -5
- package/src/lifecycle/gate-ledger.test.ts +20 -0
- package/src/lifecycle/gate-ledger.ts +28 -6
- package/src/lifecycle/git.test.ts +13 -1
- package/src/lifecycle/git.ts +221 -176
- package/src/lifecycle/lease.ts +25 -10
- package/src/lifecycle/legacy-digest.test.ts +206 -0
- package/src/lifecycle/legacy-digest.ts +103 -0
- package/src/lifecycle/member-ledger.test.ts +319 -0
- package/src/lifecycle/member-ledger.ts +145 -0
- package/src/lifecycle/plan-digest.ts +7 -7
- package/src/lifecycle/release-ledger.test.ts +20 -0
- package/src/lifecycle/release-ledger.ts +53 -3
- package/src/lifecycle/status.test.ts +151 -1
- package/src/lifecycle/status.ts +228 -7
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/op/activities/activity-contracts.ts +37 -0
- package/src/op/activities/index.ts +10 -0
- package/src/op/activities/propose-upgrade.ts +206 -0
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover.test.ts +14 -4
- package/src/op/discover.ts +19 -23
- package/src/op/gate-approval.ts +3 -1
- package/src/op/gate-name.ts +8 -0
- package/src/op/gate-summary.test.ts +14 -0
- package/src/op/gate-summary.ts +25 -2
- package/src/op/gate.ts +36 -7
- package/src/op/generate-pipeline.test.ts +14 -5
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- package/src/project-root.test.ts +66 -1
- package/src/project-root.ts +48 -0
- package/src/workspace/__fixtures__/contract-repo.ts +100 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
- package/src/workspace/behold-kinds.test.ts +162 -0
- package/src/workspace/check-contract.test.ts +127 -0
- package/src/workspace/check.schema.json +347 -0
- package/src/workspace/checks/generated.test.ts +287 -0
- package/src/workspace/checks/generated.ts +475 -0
- package/src/workspace/checks/ledgers.test.ts +139 -0
- package/src/workspace/checks/ledgers.ts +246 -0
- package/src/workspace/checks/links.ts +183 -0
- package/src/workspace/checks/pipelines.test.ts +133 -0
- package/src/workspace/checks/pipelines.ts +225 -0
- package/src/workspace/checks-repo.test.ts +71 -0
- package/src/workspace/checks.test.ts +450 -0
- package/src/workspace/checks.ts +461 -0
- package/src/workspace/compose-graph.test.ts +146 -0
- package/src/workspace/compose-graph.ts +228 -0
- package/src/workspace/compose-reports.test.ts +109 -0
- package/src/workspace/compose-reports.ts +186 -0
- package/src/workspace/declaration.schema.json +434 -0
- package/src/workspace/declaration.test.ts +347 -0
- package/src/workspace/declaration.ts +623 -0
- package/src/workspace/generated-files.test.ts +114 -0
- package/src/workspace/generated-files.ts +94 -0
- package/src/workspace/glob.ts +49 -0
- package/src/workspace/graph-cli.ts +220 -0
- package/src/workspace/graph-contract.test.ts +204 -0
- package/src/workspace/graph.schema.json +662 -0
- package/src/workspace/init.test.ts +223 -0
- package/src/workspace/init.ts +585 -0
- package/src/workspace/jsonc.test.ts +65 -0
- package/src/workspace/jsonc.ts +231 -0
- package/src/workspace/kinds.test.ts +288 -0
- package/src/workspace/kinds.ts +506 -0
- package/src/workspace/lineage-check.ts +264 -0
- package/src/workspace/lineage-cli.ts +114 -0
- package/src/workspace/lineage-init.test.ts +231 -0
- package/src/workspace/lineage-init.ts +365 -0
- package/src/workspace/lineage-lock.test.ts +224 -0
- package/src/workspace/lineage-lock.ts +357 -0
- package/src/workspace/lineage-merge.ts +44 -0
- package/src/workspace/lineage-migrations.test.ts +178 -0
- package/src/workspace/lineage-migrations.ts +343 -0
- package/src/workspace/lineage-update.ts +160 -0
- package/src/workspace/lineage-upgrade-cli.ts +151 -0
- package/src/workspace/lineage-upgrade.test.ts +474 -0
- package/src/workspace/lineage-upgrade.ts +624 -0
- package/src/workspace/lineage-vendor.test.ts +126 -0
- package/src/workspace/lineage-vendor.ts +215 -0
- package/src/workspace/lineage-version.ts +127 -0
- package/src/workspace/links.test.ts +344 -0
- package/src/workspace/links.ts +331 -0
- package/src/workspace/ls-contract.test.ts +200 -0
- package/src/workspace/ls.schema.json +190 -0
- package/src/workspace/ls.ts +254 -0
- package/src/workspace/member-commands.e2e.test.ts +244 -0
- package/src/workspace/member-commands.test.ts +217 -0
- package/src/workspace/member-commands.ts +540 -0
- package/src/workspace/member-handles.test.ts +76 -0
- package/src/workspace/member-handles.ts +228 -0
- package/src/workspace/member-pipeline.test.ts +135 -0
- package/src/workspace/member-pipeline.ts +317 -0
- package/src/workspace/member-run.ts +146 -0
- package/src/workspace/read-contract.test.ts +129 -0
- package/src/workspace/reason-codes.test.ts +113 -0
- package/src/workspace/reason-codes.ts +81 -0
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +198 -0
- package/src/workspace/records-contract.test.ts +93 -0
- package/src/workspace/records.schema.json +150 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +384 -0
- package/src/workspace/root-refusal.ts +97 -0
- package/src/workspace/status-contract.test.ts +308 -0
- package/src/workspace/status.schema.json +210 -0
- package/src/workspace/status.ts +412 -0
- package/src/workspace/template-manifest.test.ts +79 -0
- package/src/workspace/template-manifest.ts +221 -0
- package/src/workspace/tree.ts +132 -0
- package/src/workspace/trust/attestor.ts +93 -0
- package/src/workspace/trust/policy.ts +306 -0
- package/src/workspace/trust/provenance.ts +181 -0
- package/src/workspace/trust/ssh-commit.ts +138 -0
- package/src/workspace/trust/test-repo.ts +138 -0
- package/src/workspace/trust/trust.test.ts +397 -0
- package/src/workspace/trust/verify-cli.ts +56 -0
- package/src/workspace/trust/verify.ts +194 -0
- package/src/workspace/which-chant.test.ts +117 -0
- package/src/workspace/which-chant.ts +151 -0
- package/src/workspace/workspace-kinds.schema.json +133 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace init [dir] [--name <name>] [--yes] [--verbose]` (#2534):
|
|
3
|
+
* propose a declaration from the projects already in a repository, print it
|
|
4
|
+
* with the directories that would leave the root project, and write it only
|
|
5
|
+
* once the user confirms (#2525 rule 1, ws-012).
|
|
6
|
+
*
|
|
7
|
+
* The proposal is a starting point for review, never written silently:
|
|
8
|
+
*
|
|
9
|
+
* - each outermost chant project (a chant.config.ts or .json) becomes a
|
|
10
|
+
* `chant` member, and the root's own config makes a root member `"."`;
|
|
11
|
+
* - each nested declaration becomes a `workspace` member;
|
|
12
|
+
* - each other npm package, from the npm workspaces or any other
|
|
13
|
+
* package.json, becomes an `other` member with a `because`;
|
|
14
|
+
* - projects under an `examples`, `test` or `fixtures`-style directory become
|
|
15
|
+
* example groups (ws-051), one glob per tree where the tree is mostly
|
|
16
|
+
* projects, explicit paths where it isn't;
|
|
17
|
+
* - each `chant` member gets an `ownership.stack` of its own (#2538, ws-037):
|
|
18
|
+
* a stack no other member uses is kept, and a shared or missing one gets a
|
|
19
|
+
* proposed name. The stack stays in the member's chant.config, since
|
|
20
|
+
* markers carry no member key, so this is printed for the user to apply and
|
|
21
|
+
* never written.
|
|
22
|
+
*
|
|
23
|
+
* Nothing here runs project code: it reads file names and package.json files.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { existsSync, readdirSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { basename, join, resolve } from "node:path";
|
|
29
|
+
import { formatError, formatSuccess } from "../cli/format";
|
|
30
|
+
import type { CommandContext } from "../cli/registry";
|
|
31
|
+
import {
|
|
32
|
+
DECLARATION_FILES,
|
|
33
|
+
NAME_PATTERN,
|
|
34
|
+
parseDeclaration,
|
|
35
|
+
resolveGroups,
|
|
36
|
+
rootExclusions,
|
|
37
|
+
WorkspaceReadError,
|
|
38
|
+
isInside,
|
|
39
|
+
} from "./declaration";
|
|
40
|
+
import { expandGlob } from "./glob";
|
|
41
|
+
import { holdsChantProject } from "./kinds";
|
|
42
|
+
import { gitTop, skippedDir, workingTree, type WorkspaceTree } from "./tree";
|
|
43
|
+
|
|
44
|
+
const USAGE = "chant workspace init [dir] [--name <name>] [--yes] [--verbose]";
|
|
45
|
+
|
|
46
|
+
/** Directory names whose children are examples or test fixtures, not members. */
|
|
47
|
+
const EXAMPLE_DIRS = new Map<string, "examples" | "fixtures">([
|
|
48
|
+
["examples", "examples"],
|
|
49
|
+
["example", "examples"],
|
|
50
|
+
["samples", "examples"],
|
|
51
|
+
["test", "fixtures"],
|
|
52
|
+
["tests", "fixtures"],
|
|
53
|
+
["fixtures", "fixtures"],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const CONFIG_FILES = new Set(["chant.config.ts", "chant.config.json"]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Test-runner directories (`__fixtures__` and the like). A project inside one
|
|
60
|
+
* is a fixture of the package around it, so it stays with that package.
|
|
61
|
+
*/
|
|
62
|
+
const SCAFFOLDING = /(^|\/)__[a-z]+__(\/|$)/;
|
|
63
|
+
|
|
64
|
+
export interface ProposedEntry {
|
|
65
|
+
name: string;
|
|
66
|
+
dir?: string;
|
|
67
|
+
kind: string;
|
|
68
|
+
because?: string;
|
|
69
|
+
glob?: string | string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The ownership stack proposed for one `chant` member (#2538). */
|
|
73
|
+
export interface StackProposal {
|
|
74
|
+
member: string;
|
|
75
|
+
dir: string;
|
|
76
|
+
/** The config file the stack is set in, relative to the root. */
|
|
77
|
+
config: string;
|
|
78
|
+
/**
|
|
79
|
+
* The member's `ownership.stack` as written, when it is a plain string in
|
|
80
|
+
* the config. Null when it sets none, or computes it.
|
|
81
|
+
*/
|
|
82
|
+
current: string | null;
|
|
83
|
+
/** The stack the member should use; equal to `current` when that is kept. */
|
|
84
|
+
proposed: string;
|
|
85
|
+
/** Why the stack changes: `missing`, `shared` with another member, or `computed`. Absent when kept. */
|
|
86
|
+
reason?: "missing" | "shared" | "computed";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Proposal {
|
|
90
|
+
root: string;
|
|
91
|
+
/** The declaration as it would be written. */
|
|
92
|
+
declaration: { name: string; schema: 1; members: ProposedEntry[] };
|
|
93
|
+
/** Directories leaving the root project, with the .ts source files each holds. */
|
|
94
|
+
leaving: { dir: string; owner: string; files: string[] }[];
|
|
95
|
+
/** One ownership stack per `chant` member, all distinct (ws-037). */
|
|
96
|
+
stacks: StackProposal[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Files ────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/** Every file under `root`, relative and `/`-separated: git's view when in a repository, else a walk. */
|
|
102
|
+
function listFiles(root: string): string[] {
|
|
103
|
+
if (gitTop(root)) {
|
|
104
|
+
try {
|
|
105
|
+
const out = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
106
|
+
cwd: root,
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
109
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
110
|
+
});
|
|
111
|
+
return out
|
|
112
|
+
.split("\0")
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.filter((f) => !f.split("/").some((s) => skippedDir(s)) && existsSync(join(root, f)));
|
|
115
|
+
} catch {
|
|
116
|
+
// Fall through to the walk.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const out: string[] = [];
|
|
120
|
+
const walk = (dir: string, rel: string) => {
|
|
121
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
122
|
+
if (e.isDirectory()) {
|
|
123
|
+
if (!skippedDir(e.name)) walk(join(dir, e.name), rel ? `${rel}/${e.name}` : e.name);
|
|
124
|
+
} else if (e.isFile()) {
|
|
125
|
+
out.push(rel ? `${rel}/${e.name}` : e.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
walk(root, "");
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const dirOf = (file: string): string => (file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "");
|
|
134
|
+
|
|
135
|
+
/** Keep only the directories no other kept directory contains. */
|
|
136
|
+
function outermost(dirs: Iterable<string>): string[] {
|
|
137
|
+
const kept: string[] = [];
|
|
138
|
+
for (const d of [...new Set(dirs)].sort()) {
|
|
139
|
+
if (!kept.some((k) => isInside(d, k))) kept.push(d);
|
|
140
|
+
}
|
|
141
|
+
return kept;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Names ────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export function sanitizeName(raw: string): string {
|
|
147
|
+
return raw
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
150
|
+
.replace(/-+/g, "-")
|
|
151
|
+
.replace(/^[^a-z0-9]+/, "")
|
|
152
|
+
.slice(0, 40)
|
|
153
|
+
.replace(/-+$/, "");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readPackage(tree: WorkspaceTree, dir: string): Record<string, unknown> | undefined {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(tree.read(dir ? `${dir}/package.json` : "package.json")) as Record<string, unknown>;
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The workspace's name: `--name`, else the git remote's repository name, else the root package, else the directory. */
|
|
165
|
+
function workspaceName(root: string, tree: WorkspaceTree, given?: string): string {
|
|
166
|
+
if (given) return given;
|
|
167
|
+
try {
|
|
168
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
169
|
+
const repo = sanitizeName(url.replace(/\.git$/, "").split(/[/:]/).pop() ?? "");
|
|
170
|
+
if (repo) return repo;
|
|
171
|
+
} catch {
|
|
172
|
+
// No remote.
|
|
173
|
+
}
|
|
174
|
+
const pkg = readPackage(tree, "");
|
|
175
|
+
if (typeof pkg?.name === "string") {
|
|
176
|
+
const n = sanitizeName(pkg.name.replace(/^@[^/]+\//, ""));
|
|
177
|
+
if (n) return n;
|
|
178
|
+
}
|
|
179
|
+
return sanitizeName(basename(root)) || "workspace";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A member's name from its package.json, dropping the scope and the workspace's own prefix, else from its directory. */
|
|
183
|
+
function memberName(tree: WorkspaceTree, dir: string, workspace: string): string {
|
|
184
|
+
const pkg = readPackage(tree, dir);
|
|
185
|
+
if (typeof pkg?.name === "string") {
|
|
186
|
+
let n = pkg.name.replace(/^@[^/]+\//, "");
|
|
187
|
+
if (n.startsWith(`${workspace}-`)) n = n.slice(workspace.length + 1);
|
|
188
|
+
n = sanitizeName(n);
|
|
189
|
+
if (n && n !== workspace) return n;
|
|
190
|
+
}
|
|
191
|
+
if (dir === "") return "root";
|
|
192
|
+
return sanitizeName(basename(dir)) || "member";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function uniqueName(name: string, taken: Set<string>): string {
|
|
196
|
+
let n = name;
|
|
197
|
+
for (let i = 2; taken.has(n); i++) n = `${name.slice(0, 37)}-${i}`;
|
|
198
|
+
taken.add(n);
|
|
199
|
+
return n;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Ownership stacks ─────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** A string literal in TS or JSON source. */
|
|
205
|
+
const STRING = String.raw`(?:"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|\`([^\`$\\]*)\`)`;
|
|
206
|
+
const TS_STACK = new RegExp(String.raw`\bownership\s*:\s*\{[^{}]*?\bstack\s*:\s*` + STRING);
|
|
207
|
+
const TS_OWNERSHIP = /\bownership\s*:/;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A member's `ownership.stack` read from its config's text, without running
|
|
211
|
+
* it: `{ stack }` when it is a plain string, `{ computed: true }` when the
|
|
212
|
+
* config has an ownership block whose stack is not one, and `{}` when it sets
|
|
213
|
+
* none.
|
|
214
|
+
*/
|
|
215
|
+
export function readOwnershipStack(tree: WorkspaceTree, dir: string): { config: string; stack?: string; computed?: boolean } | undefined {
|
|
216
|
+
for (const name of ["chant.config.ts", "chant.config.json"]) {
|
|
217
|
+
const config = dir ? `${dir}/${name}` : name;
|
|
218
|
+
if (tree.stat(config) !== "file") continue;
|
|
219
|
+
const text = tree.read(config);
|
|
220
|
+
if (name.endsWith(".json")) {
|
|
221
|
+
try {
|
|
222
|
+
const stack = (JSON.parse(text) as { ownership?: { stack?: unknown } }).ownership?.stack;
|
|
223
|
+
return typeof stack === "string" ? { config, stack } : { config };
|
|
224
|
+
} catch {
|
|
225
|
+
return { config };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const m = TS_STACK.exec(text);
|
|
229
|
+
if (m) return { config, stack: m[1] ?? m[2] ?? m[3] };
|
|
230
|
+
return TS_OWNERSHIP.test(text) && /\bstack\s*:/.test(text) ? { config, computed: true } : { config };
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A stack name from a member name: the name itself, else the name with a number, avoiding `taken`. */
|
|
236
|
+
function freeStack(name: string, taken: Set<string>): string {
|
|
237
|
+
let s = name;
|
|
238
|
+
for (let i = 2; taken.has(s); i++) s = `${name}-${i}`;
|
|
239
|
+
return s;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* One distinct stack per `chant` member, in declaration order, so the root
|
|
244
|
+
* member and then the outermost directories keep theirs first. A stack only
|
|
245
|
+
* one member uses is kept. A shared stack stays with the first member using
|
|
246
|
+
* it, and the others get their member name. A member with no stack, or one
|
|
247
|
+
* the config computes, gets its member name too. A proposed name never takes
|
|
248
|
+
* a stack another member already sets.
|
|
249
|
+
*/
|
|
250
|
+
export function proposeStacks(tree: WorkspaceTree, members: { name: string; dir: string; kind: string }[]): StackProposal[] {
|
|
251
|
+
const chant = members
|
|
252
|
+
.filter((m) => m.kind === "chant")
|
|
253
|
+
.map((m) => ({ m, read: readOwnershipStack(tree, m.dir === "." ? "" : m.dir) }))
|
|
254
|
+
.filter((x): x is { m: (typeof members)[number]; read: NonNullable<typeof x.read> } => x.read !== undefined);
|
|
255
|
+
const setBy = new Map<string, string[]>();
|
|
256
|
+
for (const { m, read } of chant) if (read.stack) setBy.set(read.stack, [...(setBy.get(read.stack) ?? []), m.name]);
|
|
257
|
+
const taken = new Set<string>();
|
|
258
|
+
const out: StackProposal[] = [];
|
|
259
|
+
for (const { m, read } of chant) {
|
|
260
|
+
const current = read.stack ?? null;
|
|
261
|
+
const base = { member: m.name, dir: m.dir, config: read.config, current };
|
|
262
|
+
if (current !== null && !taken.has(current)) {
|
|
263
|
+
taken.add(current);
|
|
264
|
+
out.push({ ...base, proposed: current });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// Avoid every stack a member sets, so no rename lands on a later member's.
|
|
268
|
+
const proposed = freeStack(m.name, new Set([...taken, ...setBy.keys()]));
|
|
269
|
+
taken.add(proposed);
|
|
270
|
+
out.push({ ...base, proposed, reason: current !== null ? "shared" : read.computed ? "computed" : "missing" });
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Examples ─────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/** The example match holding `dir`: the directory just below its last examples-style segment, if any. */
|
|
278
|
+
function exampleMatch(dir: string): { match: string; parent: string } | undefined {
|
|
279
|
+
const segs = dir.split("/");
|
|
280
|
+
for (let k = segs.length - 2; k >= 0; k--) {
|
|
281
|
+
if (EXAMPLE_DIRS.has(segs[k])) return { match: segs.slice(0, k + 2).join("/"), parent: segs.slice(0, k + 1).join("/") };
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function dependsOnChant(tree: WorkspaceTree, dir: string): boolean {
|
|
287
|
+
const pkg = readPackage(tree, dir);
|
|
288
|
+
if (!pkg) return false;
|
|
289
|
+
return ["dependencies", "devDependencies", "peerDependencies"].some((f) => {
|
|
290
|
+
const deps = pkg[f];
|
|
291
|
+
return !!deps && typeof deps === "object" && Object.keys(deps).some((d) => d === "@intentius/chant" || d.startsWith("@intentius/chant-lexicon-"));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Globs covering `matches`. A tree whose subdirectories are mostly matches
|
|
297
|
+
* gets `<parent>/*`, and parents that differ in one segment merge into one
|
|
298
|
+
* glob with `*` there, when the merged glob claims no member. A tree that is
|
|
299
|
+
* mostly something else lists its matches one by one.
|
|
300
|
+
*/
|
|
301
|
+
function exampleGlobs(tree: WorkspaceTree, matches: string[], memberDirs: string[]): string[] {
|
|
302
|
+
const byParent = new Map<string, string[]>();
|
|
303
|
+
for (const m of matches) {
|
|
304
|
+
const parent = dirOf(m);
|
|
305
|
+
byParent.set(parent, [...(byParent.get(parent) ?? []), m]);
|
|
306
|
+
}
|
|
307
|
+
const wide: string[] = [];
|
|
308
|
+
const explicit: string[] = [];
|
|
309
|
+
for (const [parent, ms] of byParent) {
|
|
310
|
+
const subdirs = (tree.list(parent) ?? []).filter((e) => e.type === "dir" && !skippedDir(e.name)).length;
|
|
311
|
+
if (ms.length * 2 >= subdirs) wide.push(parent);
|
|
312
|
+
else explicit.push(...ms);
|
|
313
|
+
}
|
|
314
|
+
const safe = (glob: string): boolean =>
|
|
315
|
+
expandGlob(tree, glob).every((d) => !memberDirs.some((m) => m !== "" && isInside(m, d)) && (matches.includes(d) || !holdsChantProject(tree, d)));
|
|
316
|
+
|
|
317
|
+
const globs: string[] = [];
|
|
318
|
+
const left = new Set(wide);
|
|
319
|
+
// Try every one-segment wildcard, largest merge first.
|
|
320
|
+
const candidates = new Map<string, string[]>();
|
|
321
|
+
for (const p of wide) {
|
|
322
|
+
const segs = p.split("/");
|
|
323
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
324
|
+
const pattern = [...segs.slice(0, i), "*", ...segs.slice(i + 1)].join("/");
|
|
325
|
+
candidates.set(pattern, [...(candidates.get(pattern) ?? []), p]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const [pattern, parents] of [...candidates].sort((a, b) => b[1].length - a[1].length)) {
|
|
329
|
+
const open = parents.filter((p) => left.has(p));
|
|
330
|
+
if (open.length < 2 || !safe(`${pattern}/*`)) continue;
|
|
331
|
+
globs.push(`${pattern}/*`);
|
|
332
|
+
for (const p of open) left.delete(p);
|
|
333
|
+
}
|
|
334
|
+
for (const p of left) globs.push(`${p}/*`);
|
|
335
|
+
// Explicit matches a glob already covers need no entry of their own.
|
|
336
|
+
const covered = new Set(globs.flatMap((g) => expandGlob(tree, g)));
|
|
337
|
+
for (const m of explicit) if (!covered.has(m)) globs.push(m);
|
|
338
|
+
return globs.sort();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The group a glob belongs to: `examples`, `fixtures`, or prefixed by the tree it sits in, such as `lexicon-examples`. */
|
|
342
|
+
function groupName(glob: string): string {
|
|
343
|
+
const segs = glob.split("/");
|
|
344
|
+
let k = segs.length - 1;
|
|
345
|
+
while (k > 0 && !EXAMPLE_DIRS.has(segs[k])) k--;
|
|
346
|
+
const base = EXAMPLE_DIRS.get(segs[k]) ?? "examples";
|
|
347
|
+
const words: string[] = [];
|
|
348
|
+
for (let i = 0; i < k; i++) {
|
|
349
|
+
if (segs[i] === "*" || EXAMPLE_DIRS.has(segs[i])) continue;
|
|
350
|
+
words.push(segs[i + 1] === "*" ? segs[i].replace(/s$/, "") : segs[i]);
|
|
351
|
+
}
|
|
352
|
+
return sanitizeName([...words, base].join("-")) || base;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── The proposal ─────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
export function proposeWorkspace(root: string, options: { name?: string } = {}): Proposal {
|
|
358
|
+
const tree = workingTree(root);
|
|
359
|
+
const files = listFiles(root);
|
|
360
|
+
const dirsWith = (names: (n: string) => boolean) => new Set(files.filter((f) => names(f.slice(f.lastIndexOf("/") + 1))).map(dirOf));
|
|
361
|
+
|
|
362
|
+
const name = workspaceName(root, tree, options.name);
|
|
363
|
+
const configDirs = new Set([...dirsWith((n) => CONFIG_FILES.has(n))].filter((d) => !SCAFFOLDING.test(d)));
|
|
364
|
+
const declDirs = dirsWith((n) => (DECLARATION_FILES as readonly string[]).includes(n));
|
|
365
|
+
const pkgDirs = dirsWith((n) => n === "package.json");
|
|
366
|
+
|
|
367
|
+
const nested = outermost([...declDirs].filter((d) => d !== ""));
|
|
368
|
+
const notNested = (d: string) => !nested.some((n) => isInside(d, n));
|
|
369
|
+
|
|
370
|
+
const projects = outermost([...configDirs].filter((d) => d !== "" && notNested(d)));
|
|
371
|
+
const matches = new Set<string>();
|
|
372
|
+
const chantMembers: string[] = [];
|
|
373
|
+
for (const p of projects) {
|
|
374
|
+
const ex = exampleMatch(p);
|
|
375
|
+
if (ex) matches.add(ex.match);
|
|
376
|
+
else chantMembers.push(p);
|
|
377
|
+
}
|
|
378
|
+
// Examples with no config name their lexicon in package.json.
|
|
379
|
+
for (const d of pkgDirs) {
|
|
380
|
+
const ex = d === "" || SCAFFOLDING.test(d) ? undefined : exampleMatch(d);
|
|
381
|
+
if (ex && ex.match === d && notNested(d) && dependsOnChant(tree, d)) matches.add(ex.match);
|
|
382
|
+
}
|
|
383
|
+
// A match holding a member's project would put a member inside a group.
|
|
384
|
+
for (const m of [...matches]) if (chantMembers.some((c) => isInside(c, m) || isInside(m, c))) matches.delete(m);
|
|
385
|
+
|
|
386
|
+
const npmWorkspaces = new Set<string>();
|
|
387
|
+
const rootPkg = readPackage(tree, "");
|
|
388
|
+
const ws = rootPkg?.workspaces;
|
|
389
|
+
const wsGlobs = Array.isArray(ws) ? ws : ws && typeof ws === "object" && Array.isArray((ws as { packages?: unknown }).packages) ? (ws as { packages: unknown[] }).packages : [];
|
|
390
|
+
for (const g of wsGlobs) if (typeof g === "string" && !g.startsWith("!")) for (const d of expandGlob(tree, g.replace(/^\.\//, "").replace(/\/$/, ""))) npmWorkspaces.add(d);
|
|
391
|
+
|
|
392
|
+
const claimed = (d: string) =>
|
|
393
|
+
!notNested(d) ||
|
|
394
|
+
[...matches].some((m) => isInside(d, m)) ||
|
|
395
|
+
chantMembers.some((c) => isInside(d, c) || isInside(c, d)) ||
|
|
396
|
+
exampleMatch(d) !== undefined;
|
|
397
|
+
const packages = outermost([...pkgDirs].filter((d) => d !== "" && !claimed(d)));
|
|
398
|
+
|
|
399
|
+
const taken = new Set<string>([name]);
|
|
400
|
+
const entries: (ProposedEntry & { sort: string })[] = [];
|
|
401
|
+
if (configDirs.has("")) entries.push({ sort: "", name: uniqueName(memberName(tree, "", name), taken), dir: ".", kind: "chant" });
|
|
402
|
+
for (const d of nested) entries.push({ sort: d, name: uniqueName(memberName(tree, d, name), taken), dir: d, kind: "workspace" });
|
|
403
|
+
for (const d of chantMembers) entries.push({ sort: d, name: uniqueName(memberName(tree, d, name), taken), dir: d, kind: "chant" });
|
|
404
|
+
for (const d of packages) {
|
|
405
|
+
entries.push({
|
|
406
|
+
sort: d,
|
|
407
|
+
name: uniqueName(memberName(tree, d, name), taken),
|
|
408
|
+
dir: d,
|
|
409
|
+
kind: "other",
|
|
410
|
+
because: npmWorkspaces.has(d) ? "an npm workspace package with no chant project" : "an npm package with no chant project",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
entries.sort((a, b) => (a.sort < b.sort ? -1 : a.sort > b.sort ? 1 : 0));
|
|
414
|
+
taken.delete(name);
|
|
415
|
+
|
|
416
|
+
const memberDirs = entries.map((e) => (e.dir === "." ? "" : e.dir!));
|
|
417
|
+
const byGroup = new Map<string, string[]>();
|
|
418
|
+
for (const g of exampleGlobs(tree, [...matches].sort(), memberDirs)) {
|
|
419
|
+
const n = groupName(g);
|
|
420
|
+
byGroup.set(n, [...(byGroup.get(n) ?? []), g]);
|
|
421
|
+
}
|
|
422
|
+
const groups: ProposedEntry[] = [...byGroup]
|
|
423
|
+
.map(([n, globs]) => ({ name: uniqueName(n, taken), kind: "examples", glob: globs.length === 1 ? globs[0] : globs }))
|
|
424
|
+
.sort((a, b) => ([a.glob].flat()[0] < [b.glob].flat()[0] ? -1 : 1));
|
|
425
|
+
|
|
426
|
+
const declaration = {
|
|
427
|
+
name,
|
|
428
|
+
schema: 1 as const,
|
|
429
|
+
members: [...entries.map(({ sort: _sort, ...e }) => e), ...groups],
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Read the proposal back through the real reader: it must be a valid
|
|
433
|
+
// declaration before anyone is asked to write it.
|
|
434
|
+
const parsed = parseDeclaration(JSON.stringify(declaration), "chant.workspace.json");
|
|
435
|
+
const resolved = resolveGroups(parsed, tree);
|
|
436
|
+
const isSource = (f: string) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts");
|
|
437
|
+
const leaving = rootExclusions(parsed, resolved).map((x) => ({
|
|
438
|
+
...x,
|
|
439
|
+
files: files.filter((f) => isInside(f, x.dir) && isSource(f)),
|
|
440
|
+
}));
|
|
441
|
+
const stacks = proposeStacks(tree, entries.map((e) => ({ name: e.name, dir: e.dir!, kind: e.kind })));
|
|
442
|
+
return { root, declaration, leaving, stacks };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── The command ──────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
function formatLeaving(proposal: Proposal, verbose: boolean): string[] {
|
|
448
|
+
const lines: string[] = [];
|
|
449
|
+
if (proposal.leaving.length === 0) {
|
|
450
|
+
lines.push("No directory leaves the root project.");
|
|
451
|
+
return lines;
|
|
452
|
+
}
|
|
453
|
+
lines.push("Once the file exists, these directories leave the root project: chant build, lint, run and audit");
|
|
454
|
+
lines.push("at the root stop reading them, and the member or example group named beside each takes them over.");
|
|
455
|
+
lines.push("");
|
|
456
|
+
// Group matches are summed per group; members get a row each.
|
|
457
|
+
const rows: { label: string; owner: string; files: string[] }[] = [];
|
|
458
|
+
const groupNames = new Set(proposal.declaration.members.filter((m) => m.kind === "examples").map((m) => m.name));
|
|
459
|
+
for (const x of proposal.leaving) {
|
|
460
|
+
if (groupNames.has(x.owner) && !verbose) {
|
|
461
|
+
const row = rows.find((r) => r.owner === x.owner);
|
|
462
|
+
if (row) {
|
|
463
|
+
row.files.push(...x.files);
|
|
464
|
+
row.label = `${Number(row.label.split(" ")[0]) + 1} directories`;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
rows.push({ label: "1 directories", owner: x.owner, files: [...x.files] });
|
|
468
|
+
} else {
|
|
469
|
+
rows.push({ label: x.dir, owner: x.owner, files: x.files });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const r of rows) if (r.label === "1 directories") r.label = "1 directory";
|
|
473
|
+
const w0 = Math.max(9, ...rows.map((r) => r.label.length));
|
|
474
|
+
const w1 = Math.max(5, ...rows.map((r) => r.owner.length));
|
|
475
|
+
lines.push(` ${"DIRECTORY".padEnd(w0)} ${"OWNER".padEnd(w1)} .TS FILES`);
|
|
476
|
+
for (const r of rows) {
|
|
477
|
+
lines.push(` ${r.label.padEnd(w0)} ${r.owner.padEnd(w1)} ${r.files.length}`);
|
|
478
|
+
if (verbose) for (const f of r.files) lines.push(` ${f}`);
|
|
479
|
+
}
|
|
480
|
+
const total = proposal.leaving.reduce((n, x) => n + x.files.length, 0);
|
|
481
|
+
lines.push("");
|
|
482
|
+
lines.push(`${proposal.leaving.length} director${proposal.leaving.length === 1 ? "y" : "ies"} with ${total} .ts source file${total === 1 ? "" : "s"} leave the root project.${verbose ? "" : " --verbose lists every file."}`);
|
|
483
|
+
return lines;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function formatStacks(proposal: Proposal): string[] {
|
|
487
|
+
if (proposal.stacks.length === 0) return [];
|
|
488
|
+
const lines: string[] = [];
|
|
489
|
+
lines.push("Each chant member needs its own ownership.stack: markers carry no member name, and chant workspace");
|
|
490
|
+
lines.push("check fails when two members share one. The stack stays in the member's chant.config, so apply");
|
|
491
|
+
lines.push("the changes below by hand. Renaming a stack that is already deployed changes its resources' markers.");
|
|
492
|
+
lines.push("");
|
|
493
|
+
const w0 = Math.max(6, ...proposal.stacks.map((s) => s.member.length));
|
|
494
|
+
const w1 = Math.max(5, ...proposal.stacks.map((s) => s.proposed.length));
|
|
495
|
+
lines.push(` ${"MEMBER".padEnd(w0)} ${"STACK".padEnd(w1)} CHANGE`);
|
|
496
|
+
for (const s of proposal.stacks) {
|
|
497
|
+
let change = "keep";
|
|
498
|
+
if (s.reason === "missing") change = `set ownership.stack in ${s.config}`;
|
|
499
|
+
if (s.reason === "computed") change = `computed in ${s.config}; make sure it is distinct`;
|
|
500
|
+
if (s.reason === "shared") change = `rename from ${JSON.stringify(s.current)} in ${s.config}, which another member uses`;
|
|
501
|
+
lines.push(` ${s.member.padEnd(w0)} ${s.proposed.padEnd(w1)} ${change}`);
|
|
502
|
+
}
|
|
503
|
+
return lines;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function confirm(question: string): Promise<boolean> {
|
|
507
|
+
const { createInterface } = await import("node:readline");
|
|
508
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
509
|
+
return new Promise((resolveAnswer) => {
|
|
510
|
+
rl.question(question, (answer) => {
|
|
511
|
+
resolveAnswer(/^y(es)?$/i.test(answer.trim()));
|
|
512
|
+
rl.close();
|
|
513
|
+
});
|
|
514
|
+
// EOF without an answer is a no.
|
|
515
|
+
rl.on("close", () => resolveAnswer(false));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function runWorkspaceInit(ctx: CommandContext): Promise<number> {
|
|
520
|
+
const { args } = ctx;
|
|
521
|
+
const cwd = process.cwd();
|
|
522
|
+
const root = args.extraPositional ? resolve(args.extraPositional) : (gitTop(cwd) ?? cwd);
|
|
523
|
+
if (!existsSync(root)) {
|
|
524
|
+
console.error(formatError({ message: `${root} does not exist`, hint: USAGE }));
|
|
525
|
+
return 1;
|
|
526
|
+
}
|
|
527
|
+
const existing = DECLARATION_FILES.filter((f) => existsSync(join(root, f)));
|
|
528
|
+
if (existing.length > 0) {
|
|
529
|
+
console.error(
|
|
530
|
+
formatError({
|
|
531
|
+
message: `${join(root, existing[0])} already exists`,
|
|
532
|
+
hint: "Edit it by hand, or run chant workspace ls to see what it declares.",
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
if (args.selectName !== undefined && !NAME_PATTERN.test(args.selectName)) {
|
|
538
|
+
console.error(
|
|
539
|
+
formatError({
|
|
540
|
+
message: `--name ${JSON.stringify(args.selectName)} is not a valid workspace name`,
|
|
541
|
+
hint: "Use lowercase letters, digits and hyphens, starting with a letter or digit, at most 40 characters.",
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
return 1;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let proposal: Proposal;
|
|
548
|
+
try {
|
|
549
|
+
proposal = proposeWorkspace(root, { name: args.selectName });
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
552
|
+
// The proposal failed its own check; that is a bug in the proposer, but say what it found.
|
|
553
|
+
console.error(formatError({ message: `the proposed declaration is not valid: ${err.describe()}`, hint: "Write chant.workspace.json by hand for now." }));
|
|
554
|
+
return 1;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const target = join(root, "chant.workspace.json");
|
|
558
|
+
const text = `${JSON.stringify(proposal.declaration, null, 2)}\n`;
|
|
559
|
+
console.log(`Proposed ${target}:`);
|
|
560
|
+
console.log("");
|
|
561
|
+
console.log(text);
|
|
562
|
+
for (const line of formatLeaving(proposal, !!args.verbose)) console.log(line);
|
|
563
|
+
console.log("");
|
|
564
|
+
const stackLines = formatStacks(proposal);
|
|
565
|
+
if (stackLines.length > 0) {
|
|
566
|
+
for (const line of stackLines) console.log(line);
|
|
567
|
+
console.log("");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let write = !!args.yes;
|
|
571
|
+
if (!write) {
|
|
572
|
+
if (!process.stdin.isTTY) {
|
|
573
|
+
console.log("Nothing written. Re-run with --yes to write it, or save the proposal above and edit it first.");
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
write = await confirm("Write chant.workspace.json? [y/N] ");
|
|
577
|
+
}
|
|
578
|
+
if (!write) {
|
|
579
|
+
console.log("Nothing written.");
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
writeFileSync(target, text, { flag: "wx" });
|
|
583
|
+
console.log(formatSuccess(`Wrote ${target}. chant workspace ls lists what it declares.`));
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { parseJsonText } from "./jsonc";
|
|
3
|
+
|
|
4
|
+
const json = { jsonc: false };
|
|
5
|
+
const jsonc = { jsonc: true };
|
|
6
|
+
|
|
7
|
+
describe("parseJsonText (#2534)", () => {
|
|
8
|
+
test("reads what JSON.parse reads", () => {
|
|
9
|
+
const text = '{ "a": [1, -2.5e3, true, false, null, "x\\n\\u00e9"], "b": {} }';
|
|
10
|
+
const r = parseJsonText(text, json);
|
|
11
|
+
expect(r.ok && r.value).toEqual(JSON.parse(text));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("locates values and keys by JSON Pointer, and falls back to the nearest ancestor", () => {
|
|
15
|
+
const r = parseJsonText('{\n "members": [\n { "name": "a" },\n { "a/b": 1 }\n ]\n}\n', json);
|
|
16
|
+
if (!r.ok) throw new Error(r.message);
|
|
17
|
+
expect(r.locate("/members/0/name")).toEqual({ line: 3, column: 15 });
|
|
18
|
+
expect(r.locate("/members/0/name", true)).toEqual({ line: 3, column: 7 });
|
|
19
|
+
expect(r.locate("/members/1/a~1b", true)).toEqual({ line: 4, column: 7 });
|
|
20
|
+
expect(r.locate("/members/1/missing")).toEqual({ line: 4, column: 5 });
|
|
21
|
+
expect(r.locate("")).toEqual({ line: 1, column: 1 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("names the line and column of a syntax error", () => {
|
|
25
|
+
const r = parseJsonText('{\n "name": "a",\n "schema": 1\n "members": []\n}', json);
|
|
26
|
+
expect(r).toMatchObject({ ok: false, location: { line: 4, column: 3 } });
|
|
27
|
+
if (!r.ok) expect(r.message).toMatch(/expected "," or "}"/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("never reads an empty or blank file as a value", () => {
|
|
31
|
+
for (const text of ["", " \n", ""]) {
|
|
32
|
+
expect(parseJsonText(text, json)).toMatchObject({ ok: false, message: "the file is empty" });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("refuses duplicate keys at the second key", () => {
|
|
37
|
+
const r = parseJsonText('{ "a": 1,\n "a": 2 }', json);
|
|
38
|
+
expect(r).toMatchObject({ ok: false, location: { line: 2, column: 3 } });
|
|
39
|
+
if (!r.ok) expect(r.message).toBe('duplicate key "a"');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("comments and trailing commas only in jsonc", () => {
|
|
43
|
+
const text = '// a workspace\n{ "a": [1, 2,], /* note */ "b": 1, }\n';
|
|
44
|
+
expect(parseJsonText(text, jsonc)).toMatchObject({ ok: true, value: { a: [1, 2], b: 1 } });
|
|
45
|
+
const strict = parseJsonText(text, json);
|
|
46
|
+
expect(strict).toMatchObject({ ok: false, location: { line: 1, column: 1 } });
|
|
47
|
+
if (!strict.ok) expect(strict.message).toMatch(/only allowed in chant\.workspace\.jsonc/);
|
|
48
|
+
const comma = parseJsonText('{ "a": 1, }', json);
|
|
49
|
+
if (!comma.ok) expect(comma.message).toMatch(/trailing comma/);
|
|
50
|
+
expect(comma.ok).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("refuses what JSON refuses", () => {
|
|
54
|
+
for (const text of ["{ a: 1 }", "{ 'a': 1 }", '{ "a": 01 }', '{ "a": "\t" }', "[1] [2]", '{ "a": NaN }', '"unterminated']) {
|
|
55
|
+
expect(parseJsonText(text, jsonc).ok, text).toBe(false);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a __proto__ key is data, not a prototype", () => {
|
|
60
|
+
const r = parseJsonText('{ "__proto__": { "polluted": true } }', json);
|
|
61
|
+
if (!r.ok) throw new Error(r.message);
|
|
62
|
+
expect(Object.keys(r.value as object)).toEqual(["__proto__"]);
|
|
63
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|