@opengsd/gsd-core 1.20.0 → 1.21.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -6,34 +6,68 @@ const { execFileSync } = require('child_process');
|
|
|
6
6
|
const { existsSync, readdirSync, appendFileSync, readFileSync } = require('fs');
|
|
7
7
|
|
|
8
8
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'pr-
|
|
26
|
-
'pr-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
9
|
+
const { classifyUnstrippedContent, NOISY_FOR_SOURCE_REACHABILITY } = require('./gen-platform-conformance-tier.cjs');
|
|
10
|
+
|
|
11
|
+
// #6075: CI YAML (any `.github/**/*.yml|*.yaml`: workflows, composite actions,
|
|
12
|
+
// dependabot/labeler/issue-form config) is "inert" CI. A PR that changes only CI
|
|
13
|
+
// YAML never runs the matrix: it reports code_changed=true, product_changed=false,
|
|
14
|
+
// full_matrix=false and targets only the workflow-lint tests (the `test-inert`
|
|
15
|
+
// lane). Mixed with product code, the product files decide the scope.
|
|
16
|
+
// Anchored at the repo root: a nested `.github/` under a product prefix is not CI YAML.
|
|
17
|
+
const CI_YAML_RE = /^\.github\/.+\.ya?ml$/;
|
|
18
|
+
function isCiYaml(filePath) {
|
|
19
|
+
return CI_YAML_RE.test(filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tests shared by both the 'workflow automation' and 'inert CI' rules.
|
|
23
|
+
const WORKFLOW_LINT_TESTS = [
|
|
24
|
+
'tests/workflow-shell-pinning.test.cjs',
|
|
25
|
+
'tests/pr-template-policy.test.cjs',
|
|
26
|
+
'tests/lint-pr-check-project-dir.test.cjs',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// #6075: tests that read `.github/**` YAML content (workflow contracts). A CI
|
|
30
|
+
// YAML edit re-runs them in the targeted lane (never full_matrix). Several are
|
|
31
|
+
// conformance-tier members; that does not flip full_matrix for a CI-YAML-only
|
|
32
|
+
// change because classify() exempts isCiYaml(file) from the windows-hint
|
|
33
|
+
// escalation and forces full_matrix=false when only CI YAML changed.
|
|
34
|
+
const WORKFLOW_CONTRACT_TESTS = [
|
|
35
|
+
'tests/adr-218-release-version-validation.test.cjs',
|
|
36
|
+
'tests/adr-230-pr-target-policy.test.cjs',
|
|
37
|
+
'tests/ci-conformance-pr-gating.test.cjs',
|
|
38
|
+
'tests/ci-docs-guard-registry.test.cjs',
|
|
39
|
+
'tests/ci-durations-upload-nonfatal.test.cjs',
|
|
40
|
+
'tests/ci-full-lane-sharding.test.cjs',
|
|
41
|
+
'tests/ci-merge-group-wiring.test.cjs',
|
|
42
|
+
'tests/ci-pr-mergeability.test.cjs',
|
|
43
|
+
'tests/ci-test-job-timeout-budget.test.cjs',
|
|
44
|
+
'tests/ci-test-scope.test.cjs',
|
|
45
|
+
'tests/merge-commit-pr.test.cjs',
|
|
46
|
+
'tests/mutation-tap-runner-wiring.test.cjs',
|
|
47
|
+
'tests/mutation-workflow-base-ref.test.cjs',
|
|
48
|
+
'tests/policy-release-no-npm-self-upgrade.test.cjs',
|
|
49
|
+
'tests/policy-shell-pinning.test.cjs',
|
|
50
|
+
'tests/release-backmerge-invariants.test.cjs',
|
|
51
|
+
'tests/release-coverage-scope.test.cjs',
|
|
52
|
+
'tests/release-finalize-notes-5922.test.cjs',
|
|
53
|
+
'tests/release-finalize-syncs-next-version.test.cjs',
|
|
54
|
+
'tests/release-hotfix-empty-cherry-pick.test.cjs',
|
|
55
|
+
'tests/release-notes-real-os-5922.test.cjs',
|
|
56
|
+
'tests/release-shard-lane-sharding.test.cjs',
|
|
57
|
+
'tests/release-tarball-smoke-workflow.test.cjs',
|
|
58
|
+
'tests/require-issue-link-policy.test.cjs',
|
|
59
|
+
'tests/reviewed-or-maintainer-workflow.test.cjs',
|
|
60
|
+
'tests/security-scan.security.test.cjs',
|
|
61
|
+
'tests/self-test-report-workflow.test.cjs',
|
|
62
|
+
'tests/workflow-token-permissions.test.cjs',
|
|
63
|
+
];
|
|
32
64
|
|
|
33
65
|
// Workflows that gate merges, ship the product, or run security/cross-platform
|
|
34
|
-
// suites
|
|
35
|
-
//
|
|
36
|
-
//
|
|
66
|
+
// suites. Kept as a registry of gating workflows. Enforced by the test
|
|
67
|
+
// 'every protected workflow name refers to a real file' in
|
|
68
|
+
// tests/ci-pr-mergeability.test.cjs (existence parity) and, at module load, by
|
|
69
|
+
// missingProtectedWorkflows() below. Since #6075 a change to any of them is CI
|
|
70
|
+
// YAML and, alone, runs only the workflow-lint and workflow-contract tests.
|
|
37
71
|
const PROTECTED_WORKFLOWS = new Set([
|
|
38
72
|
'test.yml',
|
|
39
73
|
'install-smoke.yml',
|
|
@@ -41,41 +75,16 @@ const PROTECTED_WORKFLOWS = new Set([
|
|
|
41
75
|
'security-scan.yml',
|
|
42
76
|
'release.yml',
|
|
43
77
|
// #3833: the reusable gate every `pull_request` compute lane depends on —
|
|
44
|
-
//
|
|
45
|
-
// running the full matrix it is responsible for enforcing.
|
|
78
|
+
// the gate it defines is exercised by tests/ci-pr-mergeability.test.cjs.
|
|
46
79
|
'pr-mergeable-preflight.yml',
|
|
47
80
|
]);
|
|
48
|
-
for (const wf of PROTECTED_WORKFLOWS) {
|
|
49
|
-
if (INERT_WORKFLOWS.has(wf)) {
|
|
50
|
-
throw new Error(`ci-test-scope: protected workflow "${wf}" must not be in INERT_WORKFLOWS (it requires the full test matrix).`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Returns true if the path is an inert (non-pipeline) workflow file.
|
|
56
|
-
* Only `.github/workflows/<name>` where <name> is in INERT_WORKFLOWS qualifies.
|
|
57
|
-
*/
|
|
58
|
-
function isInertCi(filePath) {
|
|
59
|
-
if (!filePath.startsWith('.github/workflows/')) return false;
|
|
60
|
-
const name = filePath.slice('.github/workflows/'.length);
|
|
61
|
-
// Must be a direct child (no further slashes) and in the allowlist.
|
|
62
|
-
return !name.includes('/') && INERT_WORKFLOWS.has(name);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Tests shared by both the 'workflow automation' and 'inert CI' rules.
|
|
66
|
-
const WORKFLOW_LINT_TESTS = [
|
|
67
|
-
'tests/workflow-shell-pinning.test.cjs',
|
|
68
|
-
'tests/pr-template-policy.test.cjs',
|
|
69
|
-
'tests/lint-pr-check-project-dir.test.cjs',
|
|
70
|
-
];
|
|
71
81
|
|
|
72
82
|
const RULES = [
|
|
73
83
|
{
|
|
74
84
|
name: 'workflow automation',
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
match: filePath =>
|
|
78
|
-
filePath.startsWith('.github/rulesets/'),
|
|
85
|
+
// .github/rulesets/* (JSON branch protection) trigger the full matrix.
|
|
86
|
+
// CI YAML (.github/**/*.y{a,}ml) is the 'inert CI' rule below (#6075).
|
|
87
|
+
match: filePath => filePath.startsWith('.github/rulesets/'),
|
|
79
88
|
fullMatrix: true,
|
|
80
89
|
tests: [
|
|
81
90
|
...WORKFLOW_LINT_TESTS,
|
|
@@ -84,11 +93,12 @@ const RULES = [
|
|
|
84
93
|
},
|
|
85
94
|
{
|
|
86
95
|
name: 'inert CI',
|
|
87
|
-
match: filePath =>
|
|
96
|
+
match: filePath => isCiYaml(filePath),
|
|
88
97
|
fullMatrix: false,
|
|
89
98
|
tests: [
|
|
90
99
|
...WORKFLOW_LINT_TESTS,
|
|
91
100
|
'tests/policy-lint-shallow-checkout.test.cjs',
|
|
101
|
+
...WORKFLOW_CONTRACT_TESTS,
|
|
92
102
|
],
|
|
93
103
|
},
|
|
94
104
|
{
|
|
@@ -530,7 +540,7 @@ function reachesConformanceTierOrSeam(file, deps = {}) {
|
|
|
530
540
|
|
|
531
541
|
if (file.startsWith('src/')) {
|
|
532
542
|
const content = readFileSync(file, 'utf8');
|
|
533
|
-
const { signals } =
|
|
543
|
+
const { signals } = classifyUnstrippedContent(content);
|
|
534
544
|
const narrowSignals = signals.filter(signal => !NOISY_FOR_SOURCE_REACHABILITY.has(signal));
|
|
535
545
|
return narrowSignals.length > 0;
|
|
536
546
|
}
|
|
@@ -549,7 +559,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
549
559
|
const targeted = new Set();
|
|
550
560
|
const reasons = [];
|
|
551
561
|
let productOrPipelineChanged = false; // product/pipeline code (excludes docs)
|
|
552
|
-
let inertCiChanged = false; //
|
|
562
|
+
let inertCiChanged = false; // CI YAML files (#6075)
|
|
553
563
|
let fullMatrix = false;
|
|
554
564
|
|
|
555
565
|
for (const file of files) {
|
|
@@ -571,13 +581,8 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
571
581
|
productOrPipelineChanged = true;
|
|
572
582
|
}
|
|
573
583
|
|
|
574
|
-
//
|
|
575
|
-
if (
|
|
576
|
-
productOrPipelineChanged = true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Inert workflow files set a lightweight signal.
|
|
580
|
-
if (isInertCi(file)) {
|
|
584
|
+
// CI YAML sets a lightweight signal; it is never product/pipeline code.
|
|
585
|
+
if (isCiYaml(file)) {
|
|
581
586
|
inertCiChanged = true;
|
|
582
587
|
}
|
|
583
588
|
|
|
@@ -637,11 +642,13 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
637
642
|
// 14 of 16 rules, newly escalating most ordinary product-code PRs
|
|
638
643
|
// (src/, agents/, commands/, hooks/, skills/, config paths). Only one
|
|
639
644
|
// rule's escalation is live today: 'portability lint rules (ADR-1703)'.
|
|
640
|
-
// Four others already had fullMatrix: true (no-op here), and
|
|
641
|
-
// CI'
|
|
645
|
+
// Four others already had fullMatrix: true (no-op here), and CI YAML
|
|
646
|
+
// (the 'inert CI' rule) is excluded from this escalation entirely.
|
|
642
647
|
// Distinct, greppable reason so the conformance-lane coverage for it
|
|
643
648
|
// is traceable per rule.
|
|
644
|
-
|
|
649
|
+
// #6075: CI YAML never escalates (it is lint-only scope, also when mixed
|
|
650
|
+
// with product code, so the product files alone decide full_matrix).
|
|
651
|
+
if (!isCiYaml(file) && rule.tests.some(t => isWindowsHint(t) && reachesConformanceTierOrSeam(t, reachabilityDeps))) {
|
|
645
652
|
fullMatrix = true;
|
|
646
653
|
reasons.push(`${file}: ${rule.name} (windows-hint rule test, #4641)`);
|
|
647
654
|
}
|
|
@@ -660,7 +667,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
660
667
|
]);
|
|
661
668
|
for (const f of SCOPED_LANE_EXCLUDE) { targeted.delete(f); }
|
|
662
669
|
|
|
663
|
-
// code_changed: true when product/pipeline OR
|
|
670
|
+
// code_changed: true when product/pipeline code OR CI YAML changed.
|
|
664
671
|
// Docs-only PRs (neither flag set) get code_changed=false → full matrix skip.
|
|
665
672
|
const codeChanged = productOrPipelineChanged || inertCiChanged;
|
|
666
673
|
|
|
@@ -672,7 +679,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
672
679
|
targetedTests.push('unit');
|
|
673
680
|
}
|
|
674
681
|
|
|
675
|
-
//
|
|
682
|
+
// CI-YAML-only: full_matrix must be false (override any RULES that fired).
|
|
676
683
|
if (inertCiChanged && !productOrPipelineChanged) {
|
|
677
684
|
fullMatrix = false;
|
|
678
685
|
}
|
|
@@ -732,7 +739,6 @@ module.exports = {
|
|
|
732
739
|
RULES,
|
|
733
740
|
missingRuleTestFiles,
|
|
734
741
|
PROTECTED_WORKFLOWS,
|
|
735
|
-
INERT_WORKFLOWS,
|
|
736
742
|
missingProtectedWorkflows,
|
|
737
743
|
classify,
|
|
738
744
|
reachesConformanceTierOrSeam,
|
|
@@ -939,21 +939,70 @@ const REMEDIES = Object.freeze({
|
|
|
939
939
|
*
|
|
940
940
|
* File-derived strings (registry cells, marker tokens, workflow text) are
|
|
941
941
|
* echoed into lint output that AI agents read as trusted instructions —
|
|
942
|
-
* every interpolated field
|
|
943
|
-
* before it is written, so a malicious repo file cannot make
|
|
944
|
-
* instruction-shaped text
|
|
945
|
-
*
|
|
942
|
+
* every interpolated field has its hostile code points ESCAPED and is
|
|
943
|
+
* length-capped before it is written, so a malicious repo file cannot make
|
|
944
|
+
* the lint emit instruction-shaped text, and the reader can still trace the
|
|
945
|
+
* spelling as written (a dropped character would show text that does not
|
|
946
|
+
* occur in the file).
|
|
947
|
+
*
|
|
948
|
+
* Escape format: each hostile code point (see isEchoHostile) is rendered as
|
|
949
|
+
* `\u{X}` with UPPERCASE hex and no padding, e.g. U+0085 -> `\u{85}`,
|
|
950
|
+
* U+200B -> `\u{200B}`, tag characters (U+E0000-U+E007F) -> `\u{E0041}`. The
|
|
951
|
+
* escape introducer itself, a literal backslash U+005C, is escaped as `\u{5C}`
|
|
952
|
+
* so a spelled-out `\u{202E}` in the input can never print identically to a
|
|
953
|
+
* real U+202E: whenever nothing was dropped (guaranteed when the output is at
|
|
954
|
+
* most 190 code points, since the longest escape `\u{10FFFF}` is 10), the
|
|
955
|
+
* output decodes back to exactly the input, so distinct inputs have distinct
|
|
956
|
+
* output. Once the cap truncates, the tail is dropped and inputs differing
|
|
957
|
+
* only beyond it can share one output (e.g. 195 `a` with and without a
|
|
958
|
+
* trailing U+0085). The output is free of every hostile character.
|
|
959
|
+
*
|
|
960
|
+
* Cap: 200 code points of OUTPUT (an escape counts as its own length, never
|
|
961
|
+
* split), so the result is bounded however hostile the input; scanning stops
|
|
962
|
+
* as soon as the next unit would exceed the cap, costing O(200) matches on a
|
|
963
|
+
* huge input. A surrogate pair is never split.
|
|
964
|
+
*
|
|
965
|
+
* Implemented as a code-point filter rather than a regex so no
|
|
966
|
+
* control-character regex literal exists to lint.
|
|
946
967
|
*/
|
|
947
968
|
function sanitizeEcho(text) {
|
|
969
|
+
const CAP = 200;
|
|
948
970
|
let out = '';
|
|
971
|
+
let used = 0;
|
|
949
972
|
for (const ch of String(text)) {
|
|
950
973
|
const code = ch.codePointAt(0);
|
|
951
|
-
|
|
952
|
-
|
|
974
|
+
const hostile = code === 0x5c || isEchoHostile(code);
|
|
975
|
+
const unit = hostile ? `\\u{${code.toString(16).toUpperCase()}}` : ch;
|
|
976
|
+
const len = hostile ? unit.length : 1; // escapes are ASCII: one code point per char
|
|
977
|
+
if (used + len > CAP) break;
|
|
978
|
+
out += unit;
|
|
979
|
+
used += len;
|
|
953
980
|
}
|
|
954
|
-
return out
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Code points sanitizeEcho escapes: C0 + DEL, C1 (incl. 8-bit CSI U+009B),
|
|
986
|
+
* line/paragraph separators, zero-width + LRM/RLM, bidi embeddings/overrides
|
|
987
|
+
* and isolates, the Arabic letter mark, word joiner/invisible operators, the
|
|
988
|
+
* Mongolian vowel separator, the soft hyphen and the BOM; all able to forge
|
|
989
|
+
* or hide output a reader (human or agent) trusts. Escaping U+200D makes
|
|
990
|
+
* joined emoji sequences visible as `\u{200D}`, which is acceptable for echoed paths.
|
|
991
|
+
* @param {number} code
|
|
992
|
+
* @returns {boolean}
|
|
993
|
+
*/
|
|
994
|
+
function isEchoHostile(code) {
|
|
995
|
+
return ECHO_HOSTILE_RE.test(String.fromCodePoint(code));
|
|
955
996
|
}
|
|
956
997
|
|
|
998
|
+
// Category-based: Cc controls (C0, DEL, C1), Cf format chars (zero-width, bidi,
|
|
999
|
+
// soft hyphen, BOM, tags U+E0000-E007F, interlinear annotation, musical
|
|
1000
|
+
// format), Zl/Zp separators, Cs surrogates, Co private use, Cn unassigned;
|
|
1001
|
+
// plus variation selectors and the invisible filler characters that are not Cf.
|
|
1002
|
+
// Each combining/variation/filler member is its own alternative so no
|
|
1003
|
+
// character class holds a combining sequence (no-misleading-character-class).
|
|
1004
|
+
const ECHO_HOSTILE_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}\p{Co}\p{Cn}]|[\u{FE00}-\u{FE0F}]|[\u{E0100}-\u{E01EF}]|\u{180B}|\u{180C}|\u{180D}|\u{180F}|\u{34F}|\u{115F}|\u{1160}|\u{3164}|\u{FFA0}|\u{17B4}|\u{17B5}|\u{2800}/u;
|
|
1005
|
+
|
|
957
1006
|
module.exports = {
|
|
958
1007
|
CANONICAL_TOOLS,
|
|
959
1008
|
parseFrontmatter,
|
|
@@ -180,6 +180,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
180
180
|
// against the milestone-release-map module's exported enums.
|
|
181
181
|
'tests/milestone-release-map.test.cjs': ['docs/CONFIGURATION.md'],
|
|
182
182
|
'tests/analyze-dependencies.test.cjs': ['docs/COMMANDS.md'],
|
|
183
|
+
'tests/smart-discuss-classification.test.cjs': ['docs/COMMANDS.md'],
|
|
183
184
|
'tests/auto-select-attribute.test.cjs': ['docs/reference/plan-md.md'],
|
|
184
185
|
'tests/autonomous-converge.test.cjs': [
|
|
185
186
|
'docs/COMMANDS.md',
|
|
@@ -192,6 +193,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
192
193
|
'docs/reference/capability-manifest.md',
|
|
193
194
|
],
|
|
194
195
|
'tests/claude-md.test.cjs': ['docs/COMMANDS.md'],
|
|
196
|
+
'tests/claude-orchestration-4747.test.cjs': ['docs/how-to/enable-claude-orchestration-workflow-backend.md'],
|
|
195
197
|
'tests/claude-orchestration.test.cjs': ['docs/explanation/claude-orchestration-capability.md'],
|
|
196
198
|
'tests/command-contract.test.cjs': [
|
|
197
199
|
'docs/INVENTORY.md',
|
|
@@ -309,6 +311,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
309
311
|
'docs/pt-BR/context-monitor.md',
|
|
310
312
|
'docs/zh-CN/context-monitor.md',
|
|
311
313
|
],
|
|
314
|
+
'tests/gen-cli-surface-manifest.test.cjs': ['docs/CLI-SURFACE-MANIFEST.json'],
|
|
312
315
|
'tests/gen-state-md-docs.test.cjs': [
|
|
313
316
|
'docs/reference/state-md.md',
|
|
314
317
|
'docs/ja-JP/reference/state-md.md',
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates docs/CLI-SURFACE-MANIFEST.json from the CLI surface registry
|
|
6
|
+
* (`src/cli-surface.cts`, compiled to `gsd-core/bin/lib/cli-surface.cjs`) and the
|
|
7
|
+
* command aliases (`gsd-core/bin/lib/command-aliases.cjs`). ADR-4909 D2, #6087.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/gen-cli-surface-manifest.cjs # print the manifest to stdout
|
|
11
|
+
* node scripts/gen-cli-surface-manifest.cjs --write # rewrite the manifest file
|
|
12
|
+
* node scripts/gen-cli-surface-manifest.cjs --check # exit 1 if the manifest is missing/stale
|
|
13
|
+
* node scripts/gen-cli-surface-manifest.cjs --check --json # machine-readable report
|
|
14
|
+
* node scripts/gen-cli-surface-manifest.cjs --manifest-path <p> # test-only: target another file
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
23
|
+
const MANIFEST_REL = 'docs/CLI-SURFACE-MANIFEST.json';
|
|
24
|
+
const MANIFEST_PATH = path.join(ROOT, MANIFEST_REL);
|
|
25
|
+
const SURFACE_REL = 'gsd-core/bin/lib/cli-surface.cjs';
|
|
26
|
+
const SURFACE_PATH = path.join(ROOT, SURFACE_REL);
|
|
27
|
+
const ALIASES_PATH = path.join(ROOT, 'gsd-core/bin/lib/command-aliases.cjs');
|
|
28
|
+
|
|
29
|
+
const USAGE =
|
|
30
|
+
'Usage: node scripts/gen-cli-surface-manifest.cjs [--write | --check] [--json] [--manifest-path <path>]';
|
|
31
|
+
|
|
32
|
+
// Each compiled module the manifest reads, beside the source it is built from. The registry
|
|
33
|
+
// is read through its compiled form, so a stale compile would write or check a stale manifest.
|
|
34
|
+
const COMPILED_PAIRS = [
|
|
35
|
+
{ source: path.join(ROOT, 'src/cli-surface.cts'), compiled: SURFACE_PATH },
|
|
36
|
+
{
|
|
37
|
+
source: path.join(ROOT, 'src/command-arg-projection.cts'),
|
|
38
|
+
compiled: path.join(ROOT, 'gsd-core/bin/lib/command-arg-projection.cjs'),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether the compiled file is exactly what the build would emit for the source: true or false,
|
|
44
|
+
* or null when typescript or the build options cannot be loaded (the caller falls back to mtime).
|
|
45
|
+
*/
|
|
46
|
+
function compiledMatchesSource(source, compiled, loadTypescript) {
|
|
47
|
+
try {
|
|
48
|
+
const ts = loadTypescript();
|
|
49
|
+
const config = ts.readConfigFile(path.join(ROOT, 'tsconfig.build.json'), ts.sys.readFile);
|
|
50
|
+
if (config.error !== undefined) return null;
|
|
51
|
+
// parseJsonConfigFileContent resolves `extends`; 18003 (no input files) is about the include globs, not the options.
|
|
52
|
+
const { options, errors } = ts.parseJsonConfigFileContent(config.config, ts.sys, ROOT);
|
|
53
|
+
if (errors.some((e) => e.code !== 18003)) return null;
|
|
54
|
+
const emitted = ts.transpileModule(fs.readFileSync(source, 'utf8'), { compilerOptions: options, fileName: source });
|
|
55
|
+
return emitted.outputText === fs.readFileSync(compiled, 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Why the compiled registry cannot be trusted, or null when it is fresh: a compiled file that
|
|
63
|
+
* is missing, or older than the source beside it, means `npm run build:lib` has not run since
|
|
64
|
+
* the source changed. A source that is not on disk (an installed package) is not checked.
|
|
65
|
+
* Pairs are compared one to one: tsc leaves an unchanged module's output alone, so a source that
|
|
66
|
+
* is newer by mtime only (a checkout, a touch) is judged by content: the source is transpiled
|
|
67
|
+
* with the repo's own typescript and the build options of tsconfig.build.json, and equal to the
|
|
68
|
+
* compiled text means fresh. When typescript cannot be loaded the mtime comparison decides.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<{source: string, compiled: string}>} [pairs]
|
|
71
|
+
* @param {() => object} [loadTypescript]
|
|
72
|
+
* @returns {string | null}
|
|
73
|
+
*/
|
|
74
|
+
function staleCompileProblem(pairs = COMPILED_PAIRS, loadTypescript = () => require('typescript')) {
|
|
75
|
+
for (const { source, compiled } of pairs) {
|
|
76
|
+
const rel = (p) => path.relative(ROOT, p) || p;
|
|
77
|
+
if (!fs.existsSync(compiled)) {
|
|
78
|
+
return `compiled artifact not found at ${rel(compiled)}. Run \`npm run build:lib\` first.`;
|
|
79
|
+
}
|
|
80
|
+
if (!fs.existsSync(source)) continue;
|
|
81
|
+
if (fs.statSync(source).mtimeMs > fs.statSync(compiled).mtimeMs) {
|
|
82
|
+
if (compiledMatchesSource(source, compiled, loadTypescript) === true) continue;
|
|
83
|
+
return (
|
|
84
|
+
`${rel(compiled)} is older than ${rel(source)}. Run \`npm run build:lib\` first ` +
|
|
85
|
+
'(if it emits nothing, delete tsconfig.build.tsbuildinfo and run it again).'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadSurface() {
|
|
93
|
+
const problem = staleCompileProblem();
|
|
94
|
+
if (problem !== null) throw new ExitError(2, `gen-cli-surface-manifest: ${problem}`);
|
|
95
|
+
return require(SURFACE_PATH);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** canonical -> sorted, unique aliases, across every `*_COMMAND_ALIASES` array. */
|
|
99
|
+
function aliasMap() {
|
|
100
|
+
const mod = require(ALIASES_PATH);
|
|
101
|
+
const collected = new Map();
|
|
102
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
103
|
+
if (!key.endsWith('_COMMAND_ALIASES') || !Array.isArray(value)) continue;
|
|
104
|
+
for (const entry of value) {
|
|
105
|
+
if (!entry || typeof entry.canonical !== 'string') continue;
|
|
106
|
+
const set = collected.get(entry.canonical) ?? new Set();
|
|
107
|
+
for (const alias of entry.aliases ?? []) set.add(alias);
|
|
108
|
+
collected.set(entry.canonical, set);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return new Map([...collected].map(([canonical, set]) => [canonical, [...set].sort()]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sorted = (list) => [...(list ?? [])].sort();
|
|
115
|
+
|
|
116
|
+
function flagKinds(spec) {
|
|
117
|
+
const repeatable = new Set(spec.repeatableFlags ?? []);
|
|
118
|
+
const bare = new Set(spec.bareEmptyFlags ?? []);
|
|
119
|
+
const kinds = {};
|
|
120
|
+
for (const f of spec.valueFlags ?? []) {
|
|
121
|
+
kinds[f] = repeatable.has(f) ? 'repeatable' : bare.has(f) ? 'value-or-bare' : 'value';
|
|
122
|
+
}
|
|
123
|
+
for (const f of spec.booleanFlags ?? []) kinds[f] = 'boolean';
|
|
124
|
+
for (const f of spec.optionalValueFlags ?? []) kinds[f] = 'optional-value';
|
|
125
|
+
for (const f of spec.listFlags ?? []) kinds[f] = 'list';
|
|
126
|
+
for (const f of spec.multiwordFlags ?? []) kinds[f] = 'multiword';
|
|
127
|
+
return sortedObject(kinds);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sortedObject(obj) {
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const k of Object.keys(obj).sort()) out[k] = obj[k];
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildVerbEntry(verb, spec, aliases) {
|
|
137
|
+
const entry = {
|
|
138
|
+
verb,
|
|
139
|
+
aliases: sorted(aliases.get(verb)),
|
|
140
|
+
positionals: spec.positionals,
|
|
141
|
+
flags: flagKinds(spec),
|
|
142
|
+
required: sorted(spec.requiredFlags),
|
|
143
|
+
workstreamScoped: !!spec.workstreamScoped,
|
|
144
|
+
};
|
|
145
|
+
if (spec.freePositionals) entry.freePositionals = true;
|
|
146
|
+
if (spec.rejectShortFlags) entry.rejectShortFlags = true;
|
|
147
|
+
if ((spec.eqFlags ?? []).length > 0) entry.eq = sorted(spec.eqFlags);
|
|
148
|
+
if ((spec.nonEmptyFlags ?? []).length > 0) entry.nonEmpty = sorted(spec.nonEmptyFlags);
|
|
149
|
+
if ((spec.emptyValueFlags ?? []).length > 0) entry.emptyValue = sorted(spec.emptyValueFlags);
|
|
150
|
+
if ((spec.emptyEqFlags ?? []).length > 0) entry.emptyEq = sorted(spec.emptyEqFlags);
|
|
151
|
+
if (spec.passthroughAfter) entry.passthroughAfter = spec.passthroughAfter;
|
|
152
|
+
if (spec.thirdParty) entry.thirdParty = true;
|
|
153
|
+
return entry;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildManifest() {
|
|
157
|
+
const { CLI_SURFACE, GLOBAL_FLAGS, UNDECLARED_VERBS } = loadSurface();
|
|
158
|
+
const aliases = aliasMap();
|
|
159
|
+
const flags = {};
|
|
160
|
+
for (const f of GLOBAL_FLAGS.valueFlags ?? []) flags[f] = 'value';
|
|
161
|
+
for (const f of GLOBAL_FLAGS.booleanFlags ?? []) flags[f] = 'boolean';
|
|
162
|
+
const verbs = Object.keys(CLI_SURFACE)
|
|
163
|
+
.sort()
|
|
164
|
+
.map((verb) => buildVerbEntry(verb, CLI_SURFACE[verb], aliases));
|
|
165
|
+
return {
|
|
166
|
+
schema: 1,
|
|
167
|
+
global: {
|
|
168
|
+
flags: sortedObject(flags),
|
|
169
|
+
eq: sorted(GLOBAL_FLAGS.eqFlags),
|
|
170
|
+
help: [...(GLOBAL_FLAGS.helpTokens ?? [])],
|
|
171
|
+
rejected: [...(GLOBAL_FLAGS.rejectedTokens ?? [])],
|
|
172
|
+
},
|
|
173
|
+
verbs,
|
|
174
|
+
undeclared: sorted(UNDECLARED_VERBS),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function serialize(manifest) {
|
|
179
|
+
return JSON.stringify(manifest, null, 2) + '\n';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function diffVerbs(expected, actual) {
|
|
183
|
+
const exp = new Map(expected.verbs.map((v) => [v.verb, JSON.stringify(v)]));
|
|
184
|
+
const act = new Map((actual.verbs ?? []).map((v) => [v.verb, JSON.stringify(v)]));
|
|
185
|
+
const added = [...exp.keys()].filter((k) => !act.has(k));
|
|
186
|
+
const removed = [...act.keys()].filter((k) => !exp.has(k));
|
|
187
|
+
const changed = [...exp.keys()].filter((k) => act.has(k) && act.get(k) !== exp.get(k));
|
|
188
|
+
return { added, removed, changed };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {{manifestPath?: string}} [opts]
|
|
193
|
+
* @returns {{ok: boolean, problems: Array<{kind: 'missing'|'stale'|'invalid-json', subject: string, message: string}>}}
|
|
194
|
+
*/
|
|
195
|
+
function checkReport({ manifestPath = MANIFEST_PATH } = {}) {
|
|
196
|
+
const problems = [];
|
|
197
|
+
if (!fs.existsSync(manifestPath)) {
|
|
198
|
+
problems.push({
|
|
199
|
+
kind: 'missing',
|
|
200
|
+
subject: manifestPath,
|
|
201
|
+
message: `${manifestPath} not found. Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
202
|
+
});
|
|
203
|
+
return { ok: false, problems };
|
|
204
|
+
}
|
|
205
|
+
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
206
|
+
let actual;
|
|
207
|
+
try {
|
|
208
|
+
actual = JSON.parse(text);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
problems.push({
|
|
211
|
+
kind: 'invalid-json',
|
|
212
|
+
subject: manifestPath,
|
|
213
|
+
message: `${manifestPath} is not valid JSON (${err.message}). Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
214
|
+
});
|
|
215
|
+
return { ok: false, problems };
|
|
216
|
+
}
|
|
217
|
+
const expected = buildManifest();
|
|
218
|
+
if (serialize(expected) !== text) {
|
|
219
|
+
const { added, removed, changed } = diffVerbs(expected, actual);
|
|
220
|
+
const parts = [];
|
|
221
|
+
if (added.length) parts.push(`missing verbs: ${added.join(', ')}`);
|
|
222
|
+
if (removed.length) parts.push(`removed verbs: ${removed.join(', ')}`);
|
|
223
|
+
if (changed.length) parts.push(`changed verbs: ${changed.join(', ')}`);
|
|
224
|
+
if (!parts.length) parts.push('global/undeclared sections or formatting differ');
|
|
225
|
+
const subject = [...added, ...removed, ...changed][0] ?? 'global';
|
|
226
|
+
problems.push({
|
|
227
|
+
kind: 'stale',
|
|
228
|
+
subject,
|
|
229
|
+
message: `${manifestPath} is stale (${parts.join('; ')}). Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { ok: problems.length === 0, problems };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseArgs(argv) {
|
|
236
|
+
const opts = { write: false, check: false, json: false, manifestPath: MANIFEST_PATH };
|
|
237
|
+
for (let i = 0; i < argv.length; i++) {
|
|
238
|
+
const arg = argv[i];
|
|
239
|
+
if (arg === '--write') opts.write = true;
|
|
240
|
+
else if (arg === '--check') opts.check = true;
|
|
241
|
+
else if (arg === '--json') opts.json = true;
|
|
242
|
+
else if (arg === '--manifest-path') {
|
|
243
|
+
const value = argv[i + 1];
|
|
244
|
+
if (value === undefined) throw new ExitError(2, `--manifest-path requires a path argument.\n${USAGE}`);
|
|
245
|
+
opts.manifestPath = path.resolve(value);
|
|
246
|
+
i++;
|
|
247
|
+
} else {
|
|
248
|
+
throw new ExitError(2, `unknown argument: ${arg}\n${USAGE}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (opts.write && opts.check) throw new ExitError(2, `--write and --check are mutually exclusive.\n${USAGE}`);
|
|
252
|
+
return opts;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function main() {
|
|
256
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
257
|
+
|
|
258
|
+
if (opts.write) {
|
|
259
|
+
const manifest = buildManifest();
|
|
260
|
+
fs.mkdirSync(path.dirname(opts.manifestPath), { recursive: true });
|
|
261
|
+
const tmp = `${opts.manifestPath}.${process.pid}.tmp`;
|
|
262
|
+
fs.writeFileSync(tmp, serialize(manifest), 'utf8');
|
|
263
|
+
fs.renameSync(tmp, opts.manifestPath);
|
|
264
|
+
process.stdout.write(`Wrote ${opts.manifestPath} - ${manifest.verbs.length} verb(s).\n`);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (opts.check) {
|
|
269
|
+
const report = checkReport({ manifestPath: opts.manifestPath });
|
|
270
|
+
if (opts.json) {
|
|
271
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
272
|
+
} else if (report.ok) {
|
|
273
|
+
process.stdout.write(`${opts.manifestPath} is up to date.\n`);
|
|
274
|
+
} else {
|
|
275
|
+
for (const p of report.problems) process.stderr.write(`${p.kind}: ${p.subject}: ${p.message}\n`);
|
|
276
|
+
}
|
|
277
|
+
if (!report.ok) throw new ExitError(1);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
process.stdout.write(serialize(buildManifest()));
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (require.main === module) runMain(main);
|
|
286
|
+
|
|
287
|
+
module.exports = { buildManifest, checkReport, serialize, staleCompileProblem, aliasMap, MANIFEST_PATH };
|