@seanmars/tospec 0.19.0-beta.1 → 0.19.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +530 -0
- package/README.md +71 -77
- package/assets/dashboard/app.js +14 -3
- package/assets/dashboard/style.css +7 -0
- package/assets/rules/tospec/decision.md +3 -0
- package/assets/rules/tospec/single-source-of-truth.md +19 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +254 -83
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +374 -98
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +64 -81
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +346 -230
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +41 -19
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +400 -70
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +69 -85
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +18 -21
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +49 -23
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +7 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +39 -8
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +47 -30
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +282 -108
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +6 -10
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +6 -10
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +21 -8
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +254 -95
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +4 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +90 -25
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +3 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +37 -11
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +48 -21
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +36 -33
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +10 -6
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +84 -42
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +10 -3
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +39 -40
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +26 -21
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +386 -205
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +29 -36
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +50 -58
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +105 -100
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +176 -110
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +11 -14
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +47 -29
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +44 -63
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +85 -86
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
- package/dist/core/artifact-graph/stub-detection.js +39 -0
- package/dist/core/artifact-graph/stub-detection.js.map +1 -0
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +23 -18
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +102 -43
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +8 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +25 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +22 -0
- package/dist/core/codex-residue.d.ts.map +1 -0
- package/dist/core/codex-residue.js +61 -0
- package/dist/core/codex-residue.js.map +1 -0
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +8 -53
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +49 -62
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +56 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +73 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/converters/json-converter.d.ts.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +40 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +62 -70
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +29 -11
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +232 -164
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +121 -28
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +60 -50
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +94 -66
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +25 -0
- package/dist/core/markdown-render.d.ts.map +1 -0
- package/dist/core/markdown-render.js +94 -0
- package/dist/core/markdown-render.js.map +1 -0
- package/dist/core/migrate.d.ts +32 -15
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +220 -108
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +31 -22
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +169 -54
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/planning-home.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +10 -18
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +11 -7
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +7 -8
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts +10 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +43 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts +3 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +22 -6
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +16 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/schemas/spec.schema.d.ts +2 -0
- package/dist/core/schemas/spec.schema.d.ts.map +1 -1
- package/dist/core/shared/index.d.ts +3 -8
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +3 -8
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +27 -8
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +151 -16
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +38 -53
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +40 -62
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +88 -80
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +6 -6
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +22 -23
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +166 -193
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +9 -12
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +7 -5
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +4 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -0
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +0 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +2 -2
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +26 -21
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +165 -116
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +19 -25
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -20
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +43 -7
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +50 -51
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +478 -269
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +31 -54
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +143 -100
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +15 -10
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +42 -47
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +13 -4
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +13 -4
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +10 -11
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +31 -22
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +9 -10
- package/schemas/decision/templates/decision.md +3 -1
- package/schemas/decision/templates/index.md +2 -2
- package/schemas/issue/schema.yaml +11 -2
- package/schemas/issue/templates/spec.md +37 -3
- package/schemas/sdd/schema.yaml +24 -1
- package/schemas/sdd/templates/spec.md +37 -3
package/dist/core/archive.js
CHANGED
|
@@ -2,44 +2,65 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
4
|
import { Validator } from './validation/validator.js';
|
|
5
|
+
import { issueSymbol } from './validation/types.js';
|
|
5
6
|
import { validateChangeArtifacts } from '../commands/validate.js';
|
|
6
7
|
import { isRootSelectionError, resolveRootForCommand, toRootOutput, } from './root-selection.js';
|
|
7
8
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, retireSpec, } from './specs-apply.js';
|
|
9
|
+
import { loadChangeContext, formatChangeStatus } from './artifact-graph/index.js';
|
|
10
|
+
import { buildCodeFenceMask, normalizeDocument, stripBom } from './parsers/requirement-text.js';
|
|
8
11
|
import { formatTimestamp } from '../utils/timestamp.js';
|
|
9
12
|
import { findSpecFiles } from '../utils/spec-files.js';
|
|
10
13
|
import { FileSystemUtils, isMissingPathError } from '../utils/file-system.js';
|
|
11
14
|
import { isKebabId, KEBAB_ID_DESCRIPTION } from './id.js';
|
|
12
|
-
import {
|
|
15
|
+
import { findTicketPaths, listActiveChangeDirs } from '../utils/item-discovery.js';
|
|
13
16
|
import { METADATA_FILENAME, readChangeMetadata } from '../utils/change-metadata.js';
|
|
14
17
|
import { emitSuccess, emitFailureStatus } from '../commands/shared-output.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* never assemble the timestamp themselves. Alias of the shared
|
|
18
|
-
* `formatTimestamp` (ticket filenames use the same helper).
|
|
19
|
-
*/
|
|
18
|
+
import { withFileLock } from '../utils/file-lock.js';
|
|
19
|
+
/** Alias of `formatTimestamp`: agents must never assemble archive stamps by hand. */
|
|
20
20
|
export const formatArchiveStamp = formatTimestamp;
|
|
21
|
+
/** `Conclusion: PASS` / `Conclusion: FAIL`, with an optional `(reason)` note. */
|
|
22
|
+
const SYNC_CONCLUSION_LINE = /^Conclusion:\s*(PASS|FAIL)(?:\s*\([^)]*\))?\s*$/;
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* Returns null when no conclusion is found, when the line is an unfilled
|
|
25
|
+
* template (`Conclusion: PASS | FAIL`), or when two conclusions disagree —
|
|
26
|
+
* each must leave the `--require-sync` gate shut.
|
|
27
|
+
*
|
|
28
|
+
* Fenced lines are masked because the `tospec-sync` skill documents this format
|
|
29
|
+
* *inside* a fence: a report quoting the template carries a `Conclusion: PASS`
|
|
30
|
+
* that was never a verdict.
|
|
25
31
|
*/
|
|
26
32
|
export function parseSyncReportConclusion(content) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const lines = normalizeDocument(content).split('\n');
|
|
34
|
+
const fenced = buildCodeFenceMask(lines);
|
|
35
|
+
let verdict = null;
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
if (fenced[i])
|
|
38
|
+
continue;
|
|
39
|
+
const line = lines[i].trim();
|
|
40
|
+
if (!line.startsWith('Conclusion:'))
|
|
41
|
+
continue;
|
|
42
|
+
const match = line.match(SYNC_CONCLUSION_LINE);
|
|
43
|
+
// Not "keep looking": scanning past an unparseable conclusion would let a
|
|
44
|
+
// later, well-formed line answer for a verdict this code cannot read.
|
|
45
|
+
if (!match)
|
|
46
|
+
return null;
|
|
47
|
+
const found = match[1];
|
|
48
|
+
if (verdict === null)
|
|
49
|
+
verdict = found;
|
|
50
|
+
else if (verdict !== found)
|
|
51
|
+
return null; // two conclusions, no way to pick
|
|
52
|
+
}
|
|
53
|
+
return verdict;
|
|
54
|
+
}
|
|
55
|
+
/** Whether any unfenced line opens with `Conclusion:`, parseable or not. */
|
|
56
|
+
function hasConclusionLine(content) {
|
|
57
|
+
const lines = normalizeDocument(content).split('\n');
|
|
58
|
+
const fenced = buildCodeFenceMask(lines);
|
|
59
|
+
return lines.some((line, i) => !fenced[i] && line.trim().startsWith('Conclusion:'));
|
|
38
60
|
}
|
|
39
61
|
/**
|
|
40
|
-
* JSON mode is non-interactive:
|
|
41
|
-
*
|
|
42
|
-
* status entry with a non-zero exit code.
|
|
62
|
+
* JSON mode is non-interactive: wherever the human flow would prompt or print
|
|
63
|
+
* prose, this is thrown instead and becomes a machine-readable status entry.
|
|
43
64
|
*/
|
|
44
65
|
class ArchiveBlockedError extends Error {
|
|
45
66
|
diagnostic;
|
|
@@ -68,10 +89,9 @@ function toArchiveDiagnostic(error) {
|
|
|
68
89
|
};
|
|
69
90
|
}
|
|
70
91
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* with EPERM or EXDEV. Exported for migrate's two-phase promotion.
|
|
92
|
+
* On Windows fs.rename() often fails with EPERM when the directory is non-empty
|
|
93
|
+
* or another process has it open (IDE, watcher, antivirus), so EPERM/EXDEV fall
|
|
94
|
+
* back to copy-then-remove. Exported for migrate's two-phase promotion.
|
|
75
95
|
*/
|
|
76
96
|
export async function moveDirectory(src, dest) {
|
|
77
97
|
try {
|
|
@@ -80,9 +100,8 @@ export async function moveDirectory(src, dest) {
|
|
|
80
100
|
catch (err) {
|
|
81
101
|
const code = err?.code;
|
|
82
102
|
if (code === 'EPERM' || code === 'EXDEV') {
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// leaves those copied links dangling (notably on Windows).
|
|
103
|
+
// verbatimSymlinks: otherwise Node rewrites links to absolute paths under
|
|
104
|
+
// `src`, and removing `src` below leaves the copies dangling.
|
|
86
105
|
await fs.cp(src, dest, { recursive: true, verbatimSymlinks: true });
|
|
87
106
|
await fs.rm(src, { recursive: true, force: true });
|
|
88
107
|
}
|
|
@@ -92,16 +111,8 @@ export async function moveDirectory(src, dest) {
|
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
113
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* archived change path (project-root-relative), keeping the referenced filename
|
|
98
|
-
* (proposal.md or task.md). No-op if the line is absent or has no path.
|
|
99
|
-
*/
|
|
100
|
-
/**
|
|
101
|
-
* Appends a `## Related Decisions` section to the archived ticket linking the ADRs the
|
|
102
|
-
* change's metadata records (`decisions`), so the permanent archive record can
|
|
103
|
-
* be traced back to the decisions that drove it. Best-effort: unreadable
|
|
104
|
-
* metadata or no decisions → no section, never blocks the archive.
|
|
114
|
+
* Links the ADRs in the change's metadata from the archived ticket. Best-effort:
|
|
115
|
+
* unreadable metadata or no decisions → no section, never blocks the archive.
|
|
105
116
|
*/
|
|
106
117
|
async function appendDecisionLinks(ticketFile, archivedChangeDir, projectRoot) {
|
|
107
118
|
let decisions;
|
|
@@ -115,32 +126,34 @@ async function appendDecisionLinks(ticketFile, archivedChangeDir, projectRoot) {
|
|
|
115
126
|
return;
|
|
116
127
|
// Ticket lives in tospec/tickets/archive/, decisions in tospec/decisions/.
|
|
117
128
|
const lines = decisions.map((d) => `- [${d}](../../decisions/${d})`);
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
// than block the archive.
|
|
123
|
-
let existing;
|
|
129
|
+
// `appendFile` never reads the file, so a hardcoded '\n' left a CRLF ticket
|
|
130
|
+
// mixed. A read failure falls back to '\n' rather than returning: `appendFile`
|
|
131
|
+
// needs no read permission, and bailing would drop the section.
|
|
132
|
+
let existing = '';
|
|
124
133
|
try {
|
|
125
134
|
existing = await fs.readFile(ticketFile, 'utf-8');
|
|
126
135
|
}
|
|
127
|
-
catch {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
136
|
+
catch { }
|
|
130
137
|
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
131
138
|
const block = ['', '## Related Decisions', '', ...lines, ''].join(eol);
|
|
132
139
|
await fs.appendFile(ticketFile, block);
|
|
133
140
|
}
|
|
134
141
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
142
|
+
* Repoints an archived ticket's `ref:` frontmatter at the change's new path,
|
|
143
|
+
* keeping the referenced filename. No-op when the line is absent.
|
|
144
|
+
*
|
|
145
|
+
* Line endings survive untouched because the pattern excludes line terminators
|
|
146
|
+
* (`[^/\r\n]+`, no `s` flag), so `replace` only rewrites within a line. Do not
|
|
138
147
|
* "simplify" this by splitting and rejoining the file.
|
|
139
148
|
*/
|
|
140
149
|
async function retargetTicketRef(ticketFile, archiveName) {
|
|
141
|
-
const
|
|
150
|
+
const raw = await fs.readFile(ticketFile, 'utf-8');
|
|
151
|
+
// Compared against `raw`, not the stripped text, so a ticket whose only
|
|
152
|
+
// difference is a BOM still gets written back without one: this rewrite is
|
|
153
|
+
// where tospec re-emits the file, and what it emits is UTF-8 without a BOM.
|
|
154
|
+
const content = stripBom(raw);
|
|
142
155
|
const updated = content.replace(/^(ref:[ \t]*).*\/([^/\r\n]+)[ \t]*$/m, `$1tospec/changes/archive/${archiveName}/$2`);
|
|
143
|
-
if (updated !==
|
|
156
|
+
if (updated !== raw) {
|
|
144
157
|
await fs.writeFile(ticketFile, updated);
|
|
145
158
|
}
|
|
146
159
|
}
|
|
@@ -160,47 +173,70 @@ export async function moveFile(src, dest) {
|
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Returned by the locked merge when a human-mode failure path already printed
|
|
178
|
+
* and set the exit code. Distinct from `null` so the caller cannot forget to
|
|
179
|
+
* handle it.
|
|
180
|
+
*/
|
|
181
|
+
const writeTotalsSentinel = Symbol('archive-merge-aborted');
|
|
163
182
|
export class ArchiveCommand {
|
|
164
183
|
async execute(changeName, options = {}) {
|
|
165
184
|
const json = !!options.json;
|
|
166
|
-
// Same root resolution adapter as every other command (report 4.3).
|
|
167
185
|
const root = await resolveRootForCommand(options, {
|
|
168
186
|
json,
|
|
169
|
-
|
|
187
|
+
// An implicit root means no ancestor holds `tospec/`, so archiving can
|
|
188
|
+
// only fail — and with a vaguer message than the shared no_tospec_root
|
|
189
|
+
// diagnostic.
|
|
190
|
+
allowImplicitRoot: false,
|
|
191
|
+
failurePayload: { archive: null, root: null },
|
|
170
192
|
});
|
|
171
193
|
if (!root) {
|
|
172
194
|
return;
|
|
173
195
|
}
|
|
174
196
|
if (json) {
|
|
197
|
+
// A gate overridden by --yes did not fail, so exit 0 stands and its
|
|
198
|
+
// warning rides on `status` — prose on stdout would corrupt the JSON.
|
|
199
|
+
const jsonWarnings = [];
|
|
175
200
|
try {
|
|
176
|
-
const result = await this.run(changeName, options, root, true);
|
|
201
|
+
const result = await this.run(changeName, options, root, true, jsonWarnings);
|
|
177
202
|
if (!result) {
|
|
178
203
|
return;
|
|
179
204
|
}
|
|
180
|
-
emitSuccess({ archive: result }, toRootOutput(root));
|
|
205
|
+
emitSuccess({ archive: result, ...(jsonWarnings.length ? { status: jsonWarnings } : {}) }, toRootOutput(root));
|
|
181
206
|
}
|
|
182
207
|
catch (error) {
|
|
183
|
-
|
|
208
|
+
// Gates overridden before the failure still happened, and a run that
|
|
209
|
+
// skipped validation *and* then failed is when the reader needs to know.
|
|
210
|
+
this.printJsonFailure(root, toArchiveDiagnostic(error), jsonWarnings);
|
|
184
211
|
}
|
|
185
212
|
return;
|
|
186
213
|
}
|
|
187
|
-
await this.run(changeName, options, root, false);
|
|
214
|
+
await this.run(changeName, options, root, false, []);
|
|
188
215
|
}
|
|
189
|
-
printJsonFailure(root, diagnostic) {
|
|
190
|
-
emitFailureStatus
|
|
216
|
+
printJsonFailure(root, diagnostic, jsonWarnings = []) {
|
|
217
|
+
// emitFailureStatus appends the error to any `status` already in the
|
|
218
|
+
// payload, so the warnings keep their order and precede the failure.
|
|
219
|
+
emitFailureStatus({
|
|
220
|
+
archive: null,
|
|
221
|
+
...(root ? { root: toRootOutput(root) } : {}),
|
|
222
|
+
...(jsonWarnings.length ? { status: jsonWarnings } : {}),
|
|
223
|
+
}, diagnostic);
|
|
191
224
|
}
|
|
192
225
|
/**
|
|
193
|
-
* One archive gate
|
|
194
|
-
* human mode prompts; `--yes` downgrades
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* The gates used to be written out longhand at each site and had to stay in
|
|
198
|
-
* step by hand for the "every blocked path throws in JSON mode" invariant.
|
|
226
|
+
* One archive gate, so the "every blocked path throws in JSON mode" invariant
|
|
227
|
+
* lives in one place. JSON mode throws; human mode prompts; `--yes` downgrades
|
|
228
|
+
* either to a warning. False means the user declined.
|
|
199
229
|
*/
|
|
200
230
|
async gate(opts) {
|
|
201
231
|
if (opts.yes) {
|
|
202
|
-
if (
|
|
232
|
+
if (opts.json) {
|
|
233
|
+
// No `fix`: every gate's fix ends in "or rerun with --yes", which tells
|
|
234
|
+
// a reader who already passed `--yes` to do what they just did.
|
|
235
|
+
opts.warnings.push({ severity: 'warning', code: opts.code, message: opts.message });
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
203
238
|
console.error(opts.warning);
|
|
239
|
+
}
|
|
204
240
|
return true;
|
|
205
241
|
}
|
|
206
242
|
if (opts.json) {
|
|
@@ -215,45 +251,78 @@ export class ArchiveCommand {
|
|
|
215
251
|
return true;
|
|
216
252
|
}
|
|
217
253
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
254
|
+
* Required artifacts the change never produced, in schema order. Reads the
|
|
255
|
+
* same graph `tospec status` does, so the two cannot disagree about what
|
|
256
|
+
* "required" means; `skipped` and `optional` are legitimate absences.
|
|
257
|
+
*
|
|
258
|
+
* An unloadable graph returns nothing rather than throwing: this gate must not
|
|
259
|
+
* become a second way for archive to fail. Whatever broke the load is
|
|
260
|
+
* validation's to report.
|
|
261
|
+
*/
|
|
262
|
+
findMissingRequiredArtifacts(projectRoot, changeName, changeDir) {
|
|
263
|
+
try {
|
|
264
|
+
// Schema left undefined: loadChangeContext reads it from the change's own
|
|
265
|
+
// metadata, as `status` does.
|
|
266
|
+
const status = formatChangeStatus(loadChangeContext(projectRoot, changeName, undefined, { changeDir }));
|
|
267
|
+
return status.artifacts
|
|
268
|
+
.filter((artifact) => !artifact.optional && artifact.status !== 'done' && artifact.status !== 'skipped')
|
|
269
|
+
// A stub's file is on disk holding the template, so reporting it like an
|
|
270
|
+
// absent one sends the reader looking for a file that is right there.
|
|
271
|
+
.map((artifact) => (artifact.status === 'stub' ? `${artifact.id} (stub)` : artifact.id));
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Shared archive flow. Human mode prompts and returns null on cancellation;
|
|
279
|
+
* JSON mode keeps stdout clean and throws from every blocked path.
|
|
221
280
|
*/
|
|
222
|
-
async run(changeName, options, root, json) {
|
|
281
|
+
async run(changeName, options, root, json, jsonWarnings) {
|
|
223
282
|
const changesDir = root.changesDir;
|
|
224
283
|
const archiveDir = root.archiveDir;
|
|
225
284
|
const mainSpecsDir = root.specsDir;
|
|
226
|
-
// Check if changes directory exists
|
|
227
285
|
try {
|
|
228
286
|
await fs.access(changesDir);
|
|
229
287
|
}
|
|
230
|
-
catch {
|
|
231
|
-
|
|
288
|
+
catch (error) {
|
|
289
|
+
// Only an absent directory is "run init"; a directory that exists but
|
|
290
|
+
// cannot be entered (EACCES) is a different problem with a different fix.
|
|
291
|
+
if (!isMissingPathError(error))
|
|
292
|
+
throw error;
|
|
293
|
+
throw new ArchiveBlockedError('changes_dir_missing', 'No Tospec changes directory found.', "Run 'tospec init' first.");
|
|
232
294
|
}
|
|
233
|
-
//
|
|
295
|
+
// Captured before the interactive branch overwrites `changeName`: the kebab
|
|
296
|
+
// guard below applies to the argument only, never to what the picker
|
|
297
|
+
// returned. An empty string is the absence of a name.
|
|
298
|
+
const nameFromArgument = typeof changeName === 'string' && changeName.length > 0;
|
|
234
299
|
if (!changeName) {
|
|
235
300
|
if (json) {
|
|
236
301
|
throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: archive --json is non-interactive.', 'tospec archive <change-name> --json');
|
|
237
302
|
}
|
|
303
|
+
// Without a terminal the picker cannot be answered: it would paint its
|
|
304
|
+
// menu into the pipe and then read EOF as "nothing chosen".
|
|
305
|
+
if (!process.stdin.isTTY) {
|
|
306
|
+
throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required when stdin is not a terminal.', 'tospec archive <change-name>');
|
|
307
|
+
}
|
|
238
308
|
const selectedChange = await this.selectChange(changesDir, root.path);
|
|
239
309
|
if (!selectedChange) {
|
|
310
|
+
// The same outcome as Ctrl-C on the confirmation prompt, so the same
|
|
311
|
+
// exit code: an abort the user chose is not a successful run.
|
|
240
312
|
console.error('No change selected. Aborting.');
|
|
313
|
+
process.exitCode = 130;
|
|
241
314
|
return null;
|
|
242
315
|
}
|
|
243
316
|
changeName = selectedChange;
|
|
244
317
|
}
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
// make an archivable change unarchivable.
|
|
249
|
-
if (!isKebabId(changeName)) {
|
|
318
|
+
// The picker lists real directories, some predating this constraint;
|
|
319
|
+
// rejecting one the CLI just offered would make it unarchivable.
|
|
320
|
+
if (nameFromArgument && !isKebabId(changeName)) {
|
|
250
321
|
throw new ArchiveBlockedError('archive_change_name_invalid', `Change name '${changeName}' is not a valid change id — it ${KEBAB_ID_DESCRIPTION}.`);
|
|
251
322
|
}
|
|
252
323
|
const changeDir = path.join(changesDir, changeName);
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
// branch was dead code, and the bare catch also rewrote genuine IO failures
|
|
256
|
-
// (EACCES, EIO) as "not found". Only a missing path may answer that.
|
|
324
|
+
// Only a missing path may answer "not found" — a bare catch here rewrote
|
|
325
|
+
// genuine IO failures (EACCES, EIO) as a nonexistent change.
|
|
257
326
|
const stat = await fs.stat(changeDir).catch((error) => {
|
|
258
327
|
if (isMissingPathError(error))
|
|
259
328
|
return null;
|
|
@@ -265,14 +334,10 @@ export class ArchiveCommand {
|
|
|
265
334
|
? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
|
|
266
335
|
: `Change '${changeName}' not found. No active changes exist in this root.`);
|
|
267
336
|
}
|
|
268
|
-
// Reserve the archive name before anything is written.
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// deltas, and REMOVED/RENAMED then took their "already synced" branch with
|
|
273
|
-
// counts that no longer described reality. The stamp is computed once here
|
|
274
|
-
// and reused for the move below, so the directory that was checked is the
|
|
275
|
-
// directory that gets created.
|
|
337
|
+
// Reserve the archive name before anything is written. Checking after the
|
|
338
|
+
// merge left specs applied with the change still in tospec/changes/, so a
|
|
339
|
+
// retry re-applied the same deltas. The stamp is computed once and reused
|
|
340
|
+
// below, so the checked directory is the one made.
|
|
276
341
|
const archiveName = `${formatArchiveStamp(new Date())}-${changeName}`;
|
|
277
342
|
const archivePath = path.join(archiveDir, archiveName);
|
|
278
343
|
const archiveTargetStat = await fs.stat(archivePath).catch((error) => {
|
|
@@ -283,40 +348,53 @@ export class ArchiveCommand {
|
|
|
283
348
|
if (archiveTargetStat) {
|
|
284
349
|
throw new ArchiveBlockedError('archive_target_exists', `Archive '${archiveName}' already exists.`);
|
|
285
350
|
}
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
// actually reflect reality — is the sync agent's job, not ours).
|
|
351
|
+
// The machine-checkable half of tospec-sync; whether the report's content
|
|
352
|
+
// reflects reality is the sync agent's job, not ours.
|
|
289
353
|
if (options.requireSync) {
|
|
290
354
|
const syncReportPath = path.join(changeDir, 'sync-report.md');
|
|
291
355
|
let syncReportContent;
|
|
292
356
|
try {
|
|
293
357
|
syncReportContent = await fs.readFile(syncReportPath, 'utf-8');
|
|
294
358
|
}
|
|
295
|
-
catch {
|
|
296
|
-
|
|
359
|
+
catch (error) {
|
|
360
|
+
// "Run tospec-sync" is the fix for an absent report only; an unreadable
|
|
361
|
+
// one (EACCES) would be re-written by sync and still not be readable.
|
|
362
|
+
if (!isMissingPathError(error))
|
|
363
|
+
throw error;
|
|
364
|
+
throw new ArchiveBlockedError('sync_report_missing', `sync-report.md not found for change '${changeName}'.`, 'Run tospec-sync first, or omit --require-sync.');
|
|
297
365
|
}
|
|
298
366
|
const conclusion = parseSyncReportConclusion(syncReportContent);
|
|
299
367
|
if (conclusion !== 'PASS') {
|
|
300
|
-
|
|
368
|
+
// Three states, three fixes: FAIL means requirements to resolve; no
|
|
369
|
+
// `Conclusion:` line means the sync report was never finished; a line
|
|
370
|
+
// that is there but unreadable (an unfilled `PASS | FAIL` template, or
|
|
371
|
+
// two verdicts that disagree) means the report needs editing, not
|
|
372
|
+
// another apply pass.
|
|
373
|
+
const found = conclusion ?? (hasConclusionLine(syncReportContent) ? 'unparseable Conclusion line' : 'no Conclusion line');
|
|
374
|
+
throw new ArchiveBlockedError('sync_report_failed', `sync-report.md for change '${changeName}' does not report a PASS conclusion (found: ${found}).`, conclusion === 'FAIL'
|
|
375
|
+
? 'Resolve the flagged Requirement(s) via tospec-apply, then re-run tospec-sync.'
|
|
376
|
+
: 'Finish the report with a single `Conclusion: PASS` or `Conclusion: FAIL` line outside any code fence, or re-run tospec-sync.');
|
|
301
377
|
}
|
|
302
378
|
}
|
|
303
379
|
const skipValidation = options.validate === false || options.noValidate === true;
|
|
304
|
-
//
|
|
305
|
-
//
|
|
380
|
+
// The exact pipeline `tospec validate` runs, minus the archive preflight —
|
|
381
|
+
// that is a dry run of the merge performed below, which raises
|
|
382
|
+
// archive_spec_update_failed itself.
|
|
306
383
|
if (!skipValidation) {
|
|
307
|
-
const report = await validateChangeArtifacts(root.path, changeDir
|
|
384
|
+
const report = await validateChangeArtifacts(root.path, changeDir, {
|
|
385
|
+
archivePreflight: false,
|
|
386
|
+
});
|
|
308
387
|
if (!json && report.issues.length > 0) {
|
|
309
388
|
console.error(`\nValidation issues for change '${changeName}':`);
|
|
310
389
|
for (const issue of report.issues) {
|
|
311
|
-
const symbol = issue.level
|
|
390
|
+
const symbol = issueSymbol(issue.level);
|
|
312
391
|
console.error(` ${symbol} ${issue.path}: ${issue.message}`);
|
|
313
392
|
}
|
|
314
393
|
}
|
|
315
394
|
if (!report.valid) {
|
|
316
|
-
// --skip-specs is honoured
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// which only buries the question.
|
|
395
|
+
// --skip-specs is honoured long after this gate, so a change with no
|
|
396
|
+
// deltas fails here regardless. Point at the marker that resolves it,
|
|
397
|
+
// not at --no-validate.
|
|
320
398
|
const noDeltas = report.issues.some((i) => /at least one delta/i.test(i.message));
|
|
321
399
|
const deltaFix = noDeltas
|
|
322
400
|
? ` If this change genuinely has no behavioral effect, declare it once by setting \`skip_specs: true\` in the change's ${METADATA_FILENAME} — do not invent a requirement to satisfy validation.`
|
|
@@ -337,10 +415,14 @@ export class ArchiveCommand {
|
|
|
337
415
|
json,
|
|
338
416
|
yes: !!options.yes,
|
|
339
417
|
code: 'archive_confirmation_required',
|
|
340
|
-
|
|
418
|
+
// States the condition, not the remedy: `message` is reused verbatim
|
|
419
|
+
// when `--yes` overrides the gate. Remedies belong in `fix`, which an
|
|
420
|
+
// overridden gate drops.
|
|
421
|
+
message: `Validation is disabled for change '${changeName}' (--no-validate).`,
|
|
341
422
|
fix: 'tospec archive <change-name> --json --no-validate --yes',
|
|
342
423
|
prompt: '⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)',
|
|
343
424
|
warning: '\n⚠️ WARNING: Skipping validation may archive invalid specs.',
|
|
425
|
+
warnings: jsonWarnings,
|
|
344
426
|
});
|
|
345
427
|
if (!proceed)
|
|
346
428
|
return null;
|
|
@@ -350,9 +432,31 @@ export class ArchiveCommand {
|
|
|
350
432
|
console.error(`Affected files: ${changeDir}`);
|
|
351
433
|
}
|
|
352
434
|
}
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
435
|
+
// Validation checks the content of files that exist, so it cannot catch a
|
|
436
|
+
// change archiving with no proposal and no design. Archive is the last
|
|
437
|
+
// recoverable moment. Overridable, because a schema artifact is a planning
|
|
438
|
+
// obligation rather than a correctness one.
|
|
439
|
+
const missingArtifacts = this.findMissingRequiredArtifacts(root.path, changeName, changeDir);
|
|
440
|
+
if (missingArtifacts.length > 0) {
|
|
441
|
+
const names = missingArtifacts.join(', ');
|
|
442
|
+
// "missing or incomplete": the list holds both absent files and unfilled
|
|
443
|
+
// templates, and calling the second "missing" sends readers hunting for a
|
|
444
|
+
// file already on disk.
|
|
445
|
+
const proceed = await this.gate({
|
|
446
|
+
json,
|
|
447
|
+
yes: !!options.yes,
|
|
448
|
+
code: 'archive_artifacts_incomplete',
|
|
449
|
+
message: `Change '${changeName}' is missing or has incomplete required artifact(s): ${names}.`,
|
|
450
|
+
fix: `Run tospec status --change ${changeName} for the next step, or rerun with --yes.`,
|
|
451
|
+
prompt: `Warning: missing or incomplete required artifact(s): ${names}. Continue?`,
|
|
452
|
+
warning: `Warning: missing or incomplete required artifact(s): ${names}. Continuing due to --yes flag.`,
|
|
453
|
+
warnings: jsonWarnings,
|
|
454
|
+
});
|
|
455
|
+
if (!proceed)
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
// Pass the resolved root rather than re-deriving it from changesDir: the
|
|
459
|
+
// '..','..' hop silently encodes the tospec/changes layout.
|
|
356
460
|
const progress = await getTaskProgressForChange(changesDir, changeName, root.path);
|
|
357
461
|
if (!json) {
|
|
358
462
|
const status = formatTaskStatus(progress);
|
|
@@ -368,6 +472,7 @@ export class ArchiveCommand {
|
|
|
368
472
|
fix: 'Create the schema-tracked tasks file with checkboxes, or rerun with --yes.',
|
|
369
473
|
prompt: 'Warning: no tracked task checkboxes were found. Continue?',
|
|
370
474
|
warning: 'Warning: no tracked task checkboxes were found. Continuing due to --yes flag.',
|
|
475
|
+
warnings: jsonWarnings,
|
|
371
476
|
});
|
|
372
477
|
if (!proceed)
|
|
373
478
|
return null;
|
|
@@ -381,28 +486,42 @@ export class ArchiveCommand {
|
|
|
381
486
|
fix: 'Complete the tasks or rerun with --yes.',
|
|
382
487
|
prompt: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
|
383
488
|
warning: `Warning: ${incompleteTasks} incomplete task(s) found. Continuing due to --yes flag.`,
|
|
489
|
+
warnings: jsonWarnings,
|
|
384
490
|
});
|
|
385
491
|
if (!proceed)
|
|
386
492
|
return null;
|
|
387
493
|
}
|
|
388
|
-
// Handle spec updates unless skipSpecs flag is set
|
|
389
494
|
let specsUpdated = false;
|
|
390
|
-
let totals;
|
|
495
|
+
let totals = { added: 0, modified: 0, removed: 0, renamed: 0, retired: 0 };
|
|
391
496
|
if (options.skipSpecs) {
|
|
392
|
-
|
|
497
|
+
// `--skip-specs` is for changes with no deltas, so pointing it at one that
|
|
498
|
+
// has them is user error — and archiving in silence made it unrecoverable.
|
|
499
|
+
// A warning, not a block, because the flag is an explicit instruction.
|
|
500
|
+
const discarded = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
501
|
+
if (discarded.length > 0) {
|
|
502
|
+
// source is <changeDir>/specs/<capability>/spec.md.
|
|
503
|
+
const listed = discarded
|
|
504
|
+
.map((update) => path.basename(path.dirname(update.source)))
|
|
505
|
+
.join(', ');
|
|
506
|
+
const message = `--skip-specs discarded ${discarded.length} delta spec(s); these capabilities were not merged into the main specs: ${listed}.`;
|
|
507
|
+
if (json) {
|
|
508
|
+
jsonWarnings.push({ severity: 'warning', code: 'archive_deltas_discarded', message });
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
console.log(`Warning: ${message}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else if (!json) {
|
|
393
515
|
console.log('Skipping spec updates (--skip-specs flag provided).');
|
|
394
516
|
}
|
|
395
517
|
}
|
|
396
518
|
else {
|
|
397
|
-
// Find specs to update
|
|
398
519
|
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
399
520
|
if (specUpdates.length === 0) {
|
|
400
|
-
// findSpecUpdates only reads specs/<capability>/spec.md
|
|
401
|
-
// rejects anything else
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
// this guard exists to make impossible. Match on spec.md only: a stray
|
|
405
|
-
// NOTES.md under specs/ is not a delta and must not block an archive.
|
|
521
|
+
// findSpecUpdates only reads specs/<capability>/spec.md, and validation
|
|
522
|
+
// rejects anything else — so delta files here mean --no-validate, and
|
|
523
|
+
// reporting success would drop their requirements silently. Matches
|
|
524
|
+
// spec.md only: a stray NOTES.md is not a delta.
|
|
406
525
|
const orphans = await findSpecFiles(path.join(changeDir, 'specs'));
|
|
407
526
|
if (orphans.length > 0) {
|
|
408
527
|
const listed = orphans
|
|
@@ -435,96 +554,162 @@ export class ArchiveCommand {
|
|
|
435
554
|
}
|
|
436
555
|
}
|
|
437
556
|
if (shouldUpdateSpecs) {
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
557
|
+
// Everything to the last write is one critical section: the merge
|
|
558
|
+
// reads each main spec, builds the new text in memory and only then
|
|
559
|
+
// writes, so two concurrent archives both read the pre-merge text and
|
|
560
|
+
// the second erases the first one's requirement.
|
|
561
|
+
//
|
|
562
|
+
// The lock starts here, not at the top of `run`: a lock held across
|
|
563
|
+
// the picker or the confirmation prompt above would block every other
|
|
564
|
+
// process until someone returns to the terminal.
|
|
565
|
+
const locked = await withFileLock(path.join(root.path, 'tospec', '.archive.lock'), async () => {
|
|
566
|
+
// Prepare every update before writing any of them.
|
|
567
|
+
const prepared = [];
|
|
568
|
+
// Held back until every spec is written. A notice describes what the
|
|
569
|
+
// merge did ("wrote a TBD placeholder"), so surfacing it from a run
|
|
570
|
+
// that then aborted with "No files were changed" contradicts itself.
|
|
571
|
+
const mergeNotices = [];
|
|
572
|
+
try {
|
|
573
|
+
for (const update of specUpdates) {
|
|
574
|
+
const built = await buildUpdatedSpec(update, changeName, { silent: true });
|
|
575
|
+
mergeNotices.push(...built.notices);
|
|
576
|
+
prepared.push({ update, rebuilt: built.rebuilt, crlf: built.crlf, retired: built.retired, counts: built.counts });
|
|
577
|
+
}
|
|
444
578
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
579
|
+
catch (err) {
|
|
580
|
+
if (json) {
|
|
581
|
+
throw new ArchiveBlockedError('archive_spec_update_failed', String(err.message || err), 'Fix the change delta specs and rerun. No files were changed.');
|
|
582
|
+
}
|
|
583
|
+
console.error(String(err.message || err));
|
|
584
|
+
console.error('Aborted. No files were changed.');
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
return writeTotalsSentinel;
|
|
449
587
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
else if (issue.level === 'WARNING')
|
|
476
|
-
console.error(` ⚠ ${issue.message}`);
|
|
588
|
+
// Validate every rebuilt spec before writing any of them, so a
|
|
589
|
+
// late validation failure really does leave all targets unchanged.
|
|
590
|
+
if (!skipValidation) {
|
|
591
|
+
for (const p of prepared) {
|
|
592
|
+
// A retired capability has no requirements left, so validating it
|
|
593
|
+
// would always fail on "at least one requirement". It is deleted,
|
|
594
|
+
// not written.
|
|
595
|
+
if (p.retired)
|
|
596
|
+
continue;
|
|
597
|
+
const specName = path.basename(path.dirname(p.update.target));
|
|
598
|
+
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
|
599
|
+
if (!report.valid) {
|
|
600
|
+
if (json) {
|
|
601
|
+
throw new ArchiveBlockedError('archive_spec_validation_failed', `Rebuilt spec for '${specName}' failed validation. No files were changed.`, `Run ${`tospec validate ${specName}`} after fixing the change deltas.`);
|
|
602
|
+
}
|
|
603
|
+
console.error(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`);
|
|
604
|
+
for (const issue of report.issues) {
|
|
605
|
+
if (issue.level === 'ERROR')
|
|
606
|
+
console.error(` ✗ ${issue.message}`);
|
|
607
|
+
else if (issue.level === 'WARNING')
|
|
608
|
+
console.error(` ⚠ ${issue.message}`);
|
|
609
|
+
}
|
|
610
|
+
console.error('Aborted. No files were changed.');
|
|
611
|
+
process.exitCode = 1;
|
|
612
|
+
return writeTotalsSentinel;
|
|
477
613
|
}
|
|
478
|
-
console.error('Aborted. No files were changed.');
|
|
479
|
-
process.exitCode = 1;
|
|
480
|
-
return null;
|
|
481
614
|
}
|
|
482
615
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
616
|
+
const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0, retired: 0 };
|
|
617
|
+
for (const p of prepared) {
|
|
618
|
+
if (p.retired) {
|
|
619
|
+
await retireSpec(p.update, mainSpecsDir, { silent: json });
|
|
620
|
+
writeTotals.retired += 1;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
|
|
624
|
+
silent: json,
|
|
625
|
+
crlf: p.crlf,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
writeTotals.added += p.counts.added;
|
|
629
|
+
writeTotals.modified += p.counts.modified;
|
|
630
|
+
writeTotals.removed += p.counts.removed;
|
|
631
|
+
writeTotals.renamed += p.counts.renamed;
|
|
490
632
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
633
|
+
// Now true: the files the notices talk about exist on disk. In JSON
|
|
634
|
+
// mode a dangling REMOVED used to be an invisible no-op, hence the
|
|
635
|
+
// envelope entry rather than silence.
|
|
636
|
+
for (const n of mergeNotices) {
|
|
637
|
+
if (json) {
|
|
638
|
+
jsonWarnings.push({ severity: 'warning', code: n.code, message: n.message });
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
console.log(`Warning: ${n.message}`);
|
|
642
|
+
}
|
|
496
643
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
644
|
+
if (!json) {
|
|
645
|
+
console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}` +
|
|
646
|
+
(writeTotals.retired ? `, retired ${writeTotals.retired}` : ''));
|
|
647
|
+
console.log('Specs updated successfully.');
|
|
648
|
+
}
|
|
649
|
+
return { totals: writeTotals };
|
|
650
|
+
}, { operation: `archive ${changeName}` });
|
|
651
|
+
// The human-mode failure paths above already printed and set the exit code.
|
|
652
|
+
if (locked === writeTotalsSentinel)
|
|
653
|
+
return null;
|
|
502
654
|
specsUpdated = true;
|
|
503
|
-
totals =
|
|
504
|
-
if (!json) {
|
|
505
|
-
console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}` +
|
|
506
|
-
(writeTotals.retired ? `, retired ${writeTotals.retired}` : ''));
|
|
507
|
-
console.log('Specs updated successfully.');
|
|
508
|
-
}
|
|
655
|
+
totals = locked.totals;
|
|
509
656
|
}
|
|
510
657
|
}
|
|
511
658
|
}
|
|
512
|
-
//
|
|
513
|
-
// before any spec was written; see the reservation above.
|
|
659
|
+
// Name and collision were settled at the reservation above.
|
|
514
660
|
await fs.mkdir(archiveDir, { recursive: true });
|
|
515
|
-
// Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
|
|
516
661
|
await moveDirectory(changeDir, archivePath);
|
|
517
|
-
// The
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
662
|
+
// The ticket lives in the global tospec/tickets/ index, outside the change
|
|
663
|
+
// dir, so it moves alongside to keep the active index to in-flight changes.
|
|
664
|
+
const ticketPaths = await findTicketPaths(root.path, changeName);
|
|
665
|
+
// Newest wins: a second ticket means the change was recreated, so the later
|
|
666
|
+
// stamp describes the change being archived. The rest are reported, not
|
|
667
|
+
// cleaned up — archive must not silently remove a record.
|
|
668
|
+
const ticketPath = ticketPaths[ticketPaths.length - 1];
|
|
669
|
+
if (ticketPaths.length > 1) {
|
|
670
|
+
const stranded = ticketPaths
|
|
671
|
+
.slice(0, -1)
|
|
672
|
+
.map((file) => path.basename(file))
|
|
673
|
+
.join(', ');
|
|
674
|
+
const message = `${ticketPaths.length} active tickets name change '${changeName}'; archived ${path.basename(ticketPath)} and left the rest in tospec/tickets/: ${stranded}.`;
|
|
675
|
+
if (json) {
|
|
676
|
+
jsonWarnings.push({ severity: 'warning', code: 'ticket_duplicates_left', message });
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
console.log(`Warning: ${message}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
521
682
|
if (ticketPath) {
|
|
522
683
|
const ticketArchiveDir = path.join(root.path, 'tospec', 'tickets', 'archive');
|
|
523
684
|
await fs.mkdir(ticketArchiveDir, { recursive: true });
|
|
524
685
|
const archivedTicketPath = path.join(ticketArchiveDir, path.basename(ticketPath));
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
686
|
+
// `moveFile` overwrites, and two tickets share a filename when the same
|
|
687
|
+
// change name is created twice in one second with an archive between.
|
|
688
|
+
// Left in place and reported rather than renamed around: inventing a
|
|
689
|
+
// second filename would make the index disagree with its frontmatter.
|
|
690
|
+
const archivedTicketExists = await fs
|
|
691
|
+
.stat(archivedTicketPath)
|
|
692
|
+
.then(() => true)
|
|
693
|
+
.catch((error) => {
|
|
694
|
+
if (isMissingPathError(error))
|
|
695
|
+
return false;
|
|
696
|
+
throw error;
|
|
697
|
+
});
|
|
698
|
+
if (archivedTicketExists) {
|
|
699
|
+
const message = `tospec/tickets/archive/${path.basename(archivedTicketPath)} already exists, so this change's ticket ` +
|
|
700
|
+
`was left in tospec/tickets/ rather than overwriting it. Rename one of the two — they describe different changes.`;
|
|
701
|
+
if (json) {
|
|
702
|
+
jsonWarnings.push({ severity: 'warning', code: 'ticket_archive_collision', message });
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
console.log(`Warning: ${message}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
await moveFile(ticketPath, archivedTicketPath);
|
|
710
|
+
await retargetTicketRef(archivedTicketPath, archiveName);
|
|
711
|
+
await appendDecisionLinks(archivedTicketPath, archivePath, root.path);
|
|
712
|
+
}
|
|
528
713
|
}
|
|
529
714
|
if (!json) {
|
|
530
715
|
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
@@ -534,7 +719,7 @@ export class ArchiveCommand {
|
|
|
534
719
|
archivedAs: archiveName,
|
|
535
720
|
path: archivePath,
|
|
536
721
|
specsUpdated,
|
|
537
|
-
|
|
722
|
+
totals,
|
|
538
723
|
};
|
|
539
724
|
}
|
|
540
725
|
async selectChange(changesDir, projectRoot) {
|
|
@@ -544,7 +729,7 @@ export class ArchiveCommand {
|
|
|
544
729
|
console.log('No active changes found.');
|
|
545
730
|
return null;
|
|
546
731
|
}
|
|
547
|
-
//
|
|
732
|
+
// Task progress is decoration; on any failure the plain names still work.
|
|
548
733
|
let choices = changeDirs.map(name => ({ name, value: name }));
|
|
549
734
|
try {
|
|
550
735
|
const progressList = [];
|
|
@@ -559,19 +744,15 @@ export class ArchiveCommand {
|
|
|
559
744
|
value: p.id
|
|
560
745
|
}));
|
|
561
746
|
}
|
|
562
|
-
catch {
|
|
563
|
-
// If anything fails, fall back to simple names
|
|
564
|
-
choices = changeDirs.map(name => ({ name, value: name }));
|
|
565
|
-
}
|
|
747
|
+
catch { }
|
|
566
748
|
try {
|
|
567
|
-
|
|
749
|
+
return await select({
|
|
568
750
|
message: 'Select a change to archive',
|
|
569
751
|
choices
|
|
570
752
|
});
|
|
571
|
-
return answer;
|
|
572
753
|
}
|
|
573
|
-
catch
|
|
574
|
-
//
|
|
754
|
+
catch {
|
|
755
|
+
// Ctrl+C.
|
|
575
756
|
return null;
|
|
576
757
|
}
|
|
577
758
|
}
|