@seanmars/tospec 0.23.0 → 0.26.0-beta
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 +281 -0
- package/README.md +27 -9
- package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
- package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
- package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
- package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
- package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
- package/assets/dashboard/app.js +218 -1180
- package/assets/dashboard/index.html +31 -22
- package/assets/dashboard/lib/charts.js +355 -0
- package/assets/dashboard/lib/derive.js +375 -0
- package/assets/dashboard/lib/dom.js +121 -0
- package/assets/dashboard/lib/routes.js +52 -0
- package/assets/dashboard/lib/store.js +326 -0
- package/assets/dashboard/style.css +3103 -664
- package/assets/dashboard/views/alerts.js +170 -0
- package/assets/dashboard/views/change.js +234 -0
- package/assets/dashboard/views/docs.js +214 -0
- package/assets/dashboard/views/home.js +357 -0
- package/assets/dashboard/views/project.js +168 -0
- package/assets/dashboard/views/session.js +270 -0
- package/assets/dashboard/views/sidebar.js +131 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +116 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard-relay.d.ts +11 -0
- package/dist/commands/dashboard-relay.d.ts.map +1 -0
- package/dist/commands/dashboard-relay.js +88 -0
- package/dist/commands/dashboard-relay.js.map +1 -0
- package/dist/commands/dashboard.d.ts +69 -100
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +420 -598
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +258 -0
- package/dist/core/dashboard-hub.d.ts.map +1 -0
- package/dist/core/dashboard-hub.js +655 -0
- package/dist/core/dashboard-hub.js.map +1 -0
- package/dist/core/dashboard-routes.d.ts +57 -0
- package/dist/core/dashboard-routes.d.ts.map +1 -0
- package/dist/core/dashboard-routes.js +398 -0
- package/dist/core/dashboard-routes.js.map +1 -0
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +119 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +10 -3
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +25 -5
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- 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 +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/dashboard-mod.d.ts +24 -0
- package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
- package/dist/core/shared/dashboard-mod.js +44 -0
- package/dist/core/shared/dashboard-mod.js.map +1 -0
- package/dist/core/shared/index.d.ts +2 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +69 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +6 -2
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
- package/assets/dashboard/chart.umd.js +0 -14
package/dist/commands/config.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crossSpawn from 'cross-spawn';
|
|
2
|
+
import { splitCommandLine } from '../utils/command-line.js';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
|
-
import { getGlobalConfigPath,
|
|
5
|
+
import { getGlobalConfigPath, getGlobalConfig, getStoredGlobalConfig, getStoredGlobalConfigFields, saveGlobalConfig, assertGlobalConfigReadable, assertGlobalConfigWritable, } from '../core/global-config.js';
|
|
5
6
|
import * as yaml from 'yaml';
|
|
6
|
-
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig,
|
|
7
|
+
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, findInvalidConfigFields, } from '../core/config-schema.js';
|
|
7
8
|
import { ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
|
|
8
|
-
import { describeWorkflowReferenceGaps, findWorkflowReferenceGaps, } from '../core/shared/skill-generation.js';
|
|
9
|
+
import { closeWorkflowReferences, describeWorkflowReferenceGaps, findWorkflowReferenceGaps, } from '../core/shared/skill-generation.js';
|
|
9
10
|
import { TOSPEC_DIR_NAME } from '../core/config.js';
|
|
10
11
|
import { stripBom } from '../core/parsers/requirement-text.js';
|
|
11
12
|
import { UpdateCommand } from '../core/update.js';
|
|
12
|
-
import {
|
|
13
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
14
|
+
import { isPromptCancellationError, asErrorMessage, CliAbortError, emitSuccess, emitFailureStatus, formatLabeled, } from './shared-output.js';
|
|
13
15
|
/**
|
|
14
16
|
* The `--json` null-shapes, one per subcommand, keyed by the data key each
|
|
15
17
|
* success payload carries. Shared by the commander-level failure table in
|
|
@@ -106,6 +108,41 @@ function ignoredWorkflowsDiagnostic(config) {
|
|
|
106
108
|
fix: 'Run "tospec config profile custom" to apply it, or "tospec config unset workflows" to drop it.',
|
|
107
109
|
};
|
|
108
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The fix for a `custom` profile with no workflows list. The `set` example is
|
|
113
|
+
* the reference closure, not a fixed few ids: the old `explore,grill,propose`
|
|
114
|
+
* named siblings it left out, so following it drew
|
|
115
|
+
* `config_workflow_reference_missing` on the next `config list`. Seeded with
|
|
116
|
+
* `explore`, the entry point the old example started from; the closure decides
|
|
117
|
+
* the rest.
|
|
118
|
+
*/
|
|
119
|
+
function workflowsListFix() {
|
|
120
|
+
const example = closeWorkflowReferences(['explore']).join(',');
|
|
121
|
+
return [
|
|
122
|
+
'Run "tospec config profile core" to install every workflow, or pick a subset with "tospec config profile".',
|
|
123
|
+
`To set the list directly: tospec config set workflows "${example}"`,
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The finding that the `custom` profile has no workflows to install, or null.
|
|
128
|
+
* `config profile custom` refuses that state, but `set` and `unset` are raw
|
|
129
|
+
* writes and stay permissive — so they have to say what the write means, or
|
|
130
|
+
* the next `init` / `update` installs zero skills unannounced. Any write that
|
|
131
|
+
* can reach the state checks it: `set profile custom`, `set workflows ""` and
|
|
132
|
+
* `unset workflows` alike.
|
|
133
|
+
*/
|
|
134
|
+
function profileNeedsWorkflowsDiagnostic(config) {
|
|
135
|
+
if (config.profile !== 'custom')
|
|
136
|
+
return null;
|
|
137
|
+
if (config.workflows && config.workflows.length > 0)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
code: 'config_profile_needs_workflows',
|
|
142
|
+
message: 'Profile "custom" needs a workflows list, and none is configured, so init and update install no skills.',
|
|
143
|
+
fix: workflowsListFix(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
109
146
|
/**
|
|
110
147
|
* The finding that the selected workflows' own instructions name workflows the
|
|
111
148
|
* selection leaves out, or null. Raised here as well as at install time because
|
|
@@ -171,21 +208,14 @@ export function diffProfileState(before, after) {
|
|
|
171
208
|
};
|
|
172
209
|
}
|
|
173
210
|
/**
|
|
174
|
-
*
|
|
211
|
+
* Whether the loaded profile came from the file rather than the defaults —
|
|
175
212
|
* `getGlobalConfig()` has merged the two. Reads the file actually loaded, not
|
|
176
|
-
* the write target: the two differ until a legacy install migrates.
|
|
213
|
+
* the write target: the two differ until a legacy install migrates. A file
|
|
214
|
+
* with an invalid field is ignored whole, so nothing in it counts.
|
|
177
215
|
*/
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (fs.existsSync(configPath)) {
|
|
182
|
-
return JSON.parse(stripBom(fs.readFileSync(configPath, 'utf-8')));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// Unreadable: treat every value as a default rather than fail a listing.
|
|
187
|
-
}
|
|
188
|
-
return {};
|
|
216
|
+
function isProfileExplicit() {
|
|
217
|
+
const stored = getStoredGlobalConfigFields();
|
|
218
|
+
return findInvalidConfigFields(stored).length === 0 && stored.profile !== undefined;
|
|
189
219
|
}
|
|
190
220
|
/**
|
|
191
221
|
* The workflows a profile resolves to — `core` expands to the full set, any
|
|
@@ -209,14 +239,16 @@ export function registerConfigCommand(program) {
|
|
|
209
239
|
const opts = thisCommand.opts();
|
|
210
240
|
if (opts.scope && opts.scope !== 'global') {
|
|
211
241
|
const message = 'Project-local config is not yet implemented';
|
|
242
|
+
const fix = 'Omit --scope, or pass --scope global.';
|
|
212
243
|
// The subcommand's --json, not the parent's: `config --scope project
|
|
213
244
|
// get x --json` used to abort with nothing on stdout, so an agent saw
|
|
214
245
|
// exit 1 and no document at all.
|
|
215
246
|
if (actionCommand.opts().json) {
|
|
216
|
-
emitFailureStatus(CONFIG_FAILURE_PAYLOADS[actionCommand.name()] ?? CONFIG_SCOPE_FAILURE_PAYLOAD, { severity: 'error', code: 'config_scope_unsupported', message, fix
|
|
247
|
+
emitFailureStatus(CONFIG_FAILURE_PAYLOADS[actionCommand.name()] ?? CONFIG_SCOPE_FAILURE_PAYLOAD, { severity: 'error', code: 'config_scope_unsupported', message, fix });
|
|
217
248
|
}
|
|
218
249
|
else {
|
|
219
250
|
console.error(`Error: ${message}`);
|
|
251
|
+
console.error(`Fix: ${fix}`);
|
|
220
252
|
}
|
|
221
253
|
// Not `process.exit(1)`: exiting here kills the process before stderr
|
|
222
254
|
// has flushed.
|
|
@@ -245,7 +277,6 @@ export function registerConfigCommand(program) {
|
|
|
245
277
|
// `config set --allow-unknown` writes any top-level key, so spreading
|
|
246
278
|
// lets user data shadow the envelope's reserved fields (`version`,
|
|
247
279
|
// `root`, `status`).
|
|
248
|
-
// tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md
|
|
249
280
|
//
|
|
250
281
|
// `profileSettings` mirrors the human branch below, the only other place
|
|
251
282
|
// saying which workflows `profile: "core"` resolves to.
|
|
@@ -257,14 +288,13 @@ export function registerConfigCommand(program) {
|
|
|
257
288
|
config,
|
|
258
289
|
profileSettings: {
|
|
259
290
|
profile: config.profile ?? 'core',
|
|
260
|
-
explicit:
|
|
291
|
+
explicit: isProfileExplicit(),
|
|
261
292
|
workflows: resolveListedWorkflows(config),
|
|
262
293
|
},
|
|
263
294
|
...(listStatus.length ? { status: listStatus } : {}),
|
|
264
295
|
});
|
|
265
296
|
}
|
|
266
297
|
else {
|
|
267
|
-
const rawConfig = rawGlobalConfig();
|
|
268
298
|
console.log(yaml.stringify(config).trimEnd());
|
|
269
299
|
// The dump above is the stored config, so under a profile that ignores
|
|
270
300
|
// `workflows` it and the resolved list below are two different answers
|
|
@@ -273,7 +303,7 @@ export function registerConfigCommand(program) {
|
|
|
273
303
|
if (ignored) {
|
|
274
304
|
console.log(` ^ ${ignored.message}`);
|
|
275
305
|
}
|
|
276
|
-
const profileSource =
|
|
306
|
+
const profileSource = isProfileExplicit() ? '(explicit)' : '(default)';
|
|
277
307
|
console.log(`\nProfile settings:`);
|
|
278
308
|
console.log(` profile: ${config.profile} ${profileSource}`);
|
|
279
309
|
const workflows = resolveListedWorkflows(config);
|
|
@@ -286,14 +316,14 @@ export function registerConfigCommand(program) {
|
|
|
286
316
|
}
|
|
287
317
|
if (ignored?.fix) {
|
|
288
318
|
console.log();
|
|
289
|
-
console.log(ignored.fix);
|
|
319
|
+
console.log(`Fix: ${ignored.fix}`);
|
|
290
320
|
}
|
|
291
321
|
const referenceGap = workflowReferenceDiagnostic(config);
|
|
292
322
|
if (referenceGap) {
|
|
293
323
|
console.log();
|
|
294
324
|
console.log(`Warning: ${referenceGap.message}`);
|
|
295
325
|
if (referenceGap.fix)
|
|
296
|
-
console.log(referenceGap.fix);
|
|
326
|
+
console.log(`Fix: ${referenceGap.fix}`);
|
|
297
327
|
}
|
|
298
328
|
}
|
|
299
329
|
});
|
|
@@ -325,28 +355,24 @@ export function registerConfigCommand(program) {
|
|
|
325
355
|
return;
|
|
326
356
|
}
|
|
327
357
|
console.error(`Error: Invalid configuration key "${key}".${reason}`);
|
|
328
|
-
|
|
329
|
-
console.error(line);
|
|
358
|
+
console.error(formatLabeled('Fix', fixLines.join('\n')));
|
|
330
359
|
process.exitCode = 1;
|
|
331
360
|
return;
|
|
332
361
|
}
|
|
333
362
|
const config = getGlobalConfig();
|
|
334
363
|
const value = getNestedValue(config, key);
|
|
335
364
|
if (value === undefined) {
|
|
365
|
+
// `workflows` is a list, so the generic `<value>` placeholder hides the
|
|
366
|
+
// one thing a caller needs: the comma-separated form.
|
|
367
|
+
const fix = key === 'workflows'
|
|
368
|
+
? workflowsListFix()
|
|
369
|
+
: `tospec config set ${key} <value>`;
|
|
336
370
|
if (options.json) {
|
|
337
|
-
emitFailureStatus({ get: null }, {
|
|
338
|
-
severity: 'error',
|
|
339
|
-
code: 'config_error',
|
|
340
|
-
message: `Key "${key}" is not set.`,
|
|
341
|
-
// `workflows` is a list, so the generic `<value>` placeholder
|
|
342
|
-
// hides the one thing a caller needs: the comma-separated form.
|
|
343
|
-
fix: key === 'workflows'
|
|
344
|
-
? `tospec config set workflows "${ALL_WORKFLOWS.slice(0, 3).join(',')}"`
|
|
345
|
-
: `tospec config set ${key} <value>`,
|
|
346
|
-
});
|
|
371
|
+
emitFailureStatus({ get: null }, { severity: 'error', code: 'config_error', message: `Key "${key}" is not set.`, fix });
|
|
347
372
|
return;
|
|
348
373
|
}
|
|
349
374
|
console.error(`Error: Key "${key}" is not set.`);
|
|
375
|
+
console.error(formatLabeled('Fix', fix));
|
|
350
376
|
process.exitCode = 1;
|
|
351
377
|
return;
|
|
352
378
|
}
|
|
@@ -376,10 +402,9 @@ export function registerConfigCommand(program) {
|
|
|
376
402
|
emitFailureStatus(CONFIG_FAILURE_PAYLOADS.set, { severity: 'error', code: 'config_error', message, ...(fix ? { fix } : {}) });
|
|
377
403
|
return;
|
|
378
404
|
}
|
|
379
|
-
console.error(
|
|
405
|
+
console.error(formatLabeled('Error', message));
|
|
380
406
|
if (fix)
|
|
381
|
-
|
|
382
|
-
console.error(line);
|
|
407
|
+
console.error(formatLabeled('Fix', fix));
|
|
383
408
|
process.exitCode = 1;
|
|
384
409
|
};
|
|
385
410
|
const allowUnknown = Boolean(options.allowUnknown);
|
|
@@ -396,14 +421,22 @@ export function registerConfigCommand(program) {
|
|
|
396
421
|
fail(`Invalid configuration key "${key}".${reason}`, fixLines.join('\n'));
|
|
397
422
|
return;
|
|
398
423
|
}
|
|
399
|
-
const
|
|
400
|
-
|
|
424
|
+
const rootKey = key.split('.')[0];
|
|
425
|
+
assertGlobalConfigWritable([rootKey]);
|
|
426
|
+
const config = getStoredGlobalConfig();
|
|
427
|
+
const parsedValue = coerceValue(value, options.string || false, key);
|
|
428
|
+
// A repeated id installs nothing extra, but `config list` and the profile
|
|
429
|
+
// summary would count it twice. Dropped rather than refused, as `init
|
|
430
|
+
// --tools` drops a repeated tool: what `explore,explore` asks for is clear.
|
|
431
|
+
const coercedValue = key === 'workflows' && Array.isArray(parsedValue) ? [...new Set(parsedValue)] : parsedValue;
|
|
401
432
|
// Validate a copy before touching the config that is saved.
|
|
402
433
|
const newConfig = JSON.parse(JSON.stringify(config));
|
|
403
434
|
setNestedValue(newConfig, key, coercedValue);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
435
|
+
// This key's refusals only: when the write repairs one of two invalid
|
|
436
|
+
// fields, the other still fails, and that is not about this value.
|
|
437
|
+
const refused = findInvalidConfigFields(newConfig).filter((entry) => entry.field === rootKey);
|
|
438
|
+
if (refused.length > 0) {
|
|
439
|
+
fail(`Invalid configuration - ${refused.map((entry) => entry.detail).join('; ')}`);
|
|
407
440
|
return;
|
|
408
441
|
}
|
|
409
442
|
// The schema types `workflows` as string[], so a misspelled id passes it
|
|
@@ -422,14 +455,16 @@ export function registerConfigCommand(program) {
|
|
|
422
455
|
// discards it. Said alongside the confirmation, not instead of it: the
|
|
423
456
|
// value genuinely was stored, it is just not what `update` acts on.
|
|
424
457
|
const profile = config.profile ?? 'core';
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
458
|
+
// An empty list is no list, the same rule `ignoredWorkflowsDiagnostic`
|
|
459
|
+
// applies, so there is nothing to call unused.
|
|
460
|
+
const workflowsIgnored = key === 'workflows' &&
|
|
461
|
+
profile !== 'custom' &&
|
|
462
|
+
!(Array.isArray(coercedValue) && coercedValue.length === 0);
|
|
463
|
+
// Keyed on the written root, so an unrelated `set` under an already
|
|
464
|
+
// empty custom profile does not repeat the warning on every write.
|
|
465
|
+
const profileNeedsWorkflows = rootKey === 'profile' || rootKey === 'workflows'
|
|
466
|
+
? profileNeedsWorkflowsDiagnostic(config)
|
|
467
|
+
: null;
|
|
433
468
|
const warnings = [];
|
|
434
469
|
if (workflowsIgnored) {
|
|
435
470
|
warnings.push({
|
|
@@ -439,14 +474,8 @@ export function registerConfigCommand(program) {
|
|
|
439
474
|
fix: 'Run "tospec config profile custom" to make it take effect.',
|
|
440
475
|
});
|
|
441
476
|
}
|
|
442
|
-
if (profileNeedsWorkflows)
|
|
443
|
-
warnings.push(
|
|
444
|
-
severity: 'warning',
|
|
445
|
-
code: 'config_profile_needs_workflows',
|
|
446
|
-
message: 'Profile "custom" needs a workflows list, and none is configured, so init and update install no skills.',
|
|
447
|
-
fix: `tospec config set workflows "${ALL_WORKFLOWS.slice(0, 3).join(',')}"`,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
477
|
+
if (profileNeedsWorkflows)
|
|
478
|
+
warnings.push(profileNeedsWorkflows);
|
|
450
479
|
if (options.json) {
|
|
451
480
|
emitSuccess({
|
|
452
481
|
set: { key, value: coercedValue },
|
|
@@ -454,12 +483,18 @@ export function registerConfigCommand(program) {
|
|
|
454
483
|
});
|
|
455
484
|
return;
|
|
456
485
|
}
|
|
457
|
-
|
|
486
|
+
// `String([])` is empty, which reads as a missing value; `(none)` is
|
|
487
|
+
// what `config list` prints for the same empty list.
|
|
488
|
+
const displayValue = typeof coercedValue === 'string'
|
|
489
|
+
? `"${coercedValue}"`
|
|
490
|
+
: Array.isArray(coercedValue) && coercedValue.length === 0
|
|
491
|
+
? '(none)'
|
|
492
|
+
: String(coercedValue);
|
|
458
493
|
console.log(`Set ${key} = ${displayValue}`);
|
|
459
494
|
for (const warning of warnings) {
|
|
460
495
|
console.error(`Warning: ${warning.message}`);
|
|
461
496
|
if (warning.fix)
|
|
462
|
-
console.error(warning.fix);
|
|
497
|
+
console.error(formatLabeled('Fix', warning.fix));
|
|
463
498
|
}
|
|
464
499
|
});
|
|
465
500
|
configCmd
|
|
@@ -467,17 +502,49 @@ export function registerConfigCommand(program) {
|
|
|
467
502
|
.description('Remove a key (revert to default)')
|
|
468
503
|
.option('--json', 'Output as JSON')
|
|
469
504
|
.action((key, options) => {
|
|
470
|
-
|
|
505
|
+
assertGlobalConfigReadable();
|
|
506
|
+
const config = getStoredGlobalConfig();
|
|
507
|
+
// Unlike `get`, no --allow-unknown here: a key the file already holds is
|
|
508
|
+
// removed even when this build does not know it, so a key written with
|
|
509
|
+
// `set --allow-unknown`, or left behind by an older schema, stays
|
|
510
|
+
// removable without a flag. Only an unknown key the file lacks is
|
|
511
|
+
// refused; a typo there would otherwise report success.
|
|
512
|
+
const keyValidation = validateConfigKeyPath(key);
|
|
513
|
+
if (!keyValidation.valid &&
|
|
514
|
+
(hasUnsafeKeySegment(key) || getNestedValue(config, key) === undefined)) {
|
|
515
|
+
const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
|
|
516
|
+
const message = `Invalid configuration key "${key}".${reason}`;
|
|
517
|
+
const fix = 'Use "tospec config list" to see available keys.';
|
|
518
|
+
if (options.json) {
|
|
519
|
+
emitFailureStatus(CONFIG_FAILURE_PAYLOADS.unset, { severity: 'error', code: 'config_error', message, fix });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
console.error(formatLabeled('Error', message));
|
|
523
|
+
console.error(formatLabeled('Fix', fix));
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
assertGlobalConfigWritable([key.split('.')[0]]);
|
|
471
528
|
const existed = deleteNestedValue(config, key);
|
|
472
529
|
if (existed)
|
|
473
530
|
saveGlobalConfig(config);
|
|
531
|
+
// Only removing `workflows` can leave `custom` empty: removing `profile`
|
|
532
|
+
// reverts to `core`, which ignores the list.
|
|
533
|
+
const warning = key.split('.')[0] === 'workflows'
|
|
534
|
+
? profileNeedsWorkflowsDiagnostic(config)
|
|
535
|
+
: null;
|
|
474
536
|
if (options.json) {
|
|
475
537
|
// Success either way: unsetting an absent key is the state the caller
|
|
476
538
|
// asked for. `existed` is what says whether anything changed.
|
|
477
|
-
emitSuccess({ unset: { key, existed } });
|
|
539
|
+
emitSuccess({ unset: { key, existed }, ...(warning ? { status: [warning] } : {}) });
|
|
478
540
|
return;
|
|
479
541
|
}
|
|
480
542
|
console.log(existed ? `Unset ${key} (reverted to default)` : `Key "${key}" was not set`);
|
|
543
|
+
if (warning) {
|
|
544
|
+
console.error(`Warning: ${warning.message}`);
|
|
545
|
+
if (warning.fix)
|
|
546
|
+
console.error(formatLabeled('Fix', warning.fix));
|
|
547
|
+
}
|
|
481
548
|
});
|
|
482
549
|
configCmd
|
|
483
550
|
.command('reset')
|
|
@@ -497,8 +564,7 @@ export function registerConfigCommand(program) {
|
|
|
497
564
|
});
|
|
498
565
|
return;
|
|
499
566
|
}
|
|
500
|
-
console.error('Error
|
|
501
|
-
console.error('Usage: tospec config reset --all [-y]');
|
|
567
|
+
console.error(formatLabeled('Error', '--all flag is required for reset\nUsage: tospec config reset --all [-y]'));
|
|
502
568
|
process.exitCode = 1;
|
|
503
569
|
return;
|
|
504
570
|
}
|
|
@@ -514,6 +580,14 @@ export function registerConfigCommand(program) {
|
|
|
514
580
|
});
|
|
515
581
|
return;
|
|
516
582
|
}
|
|
583
|
+
// Without a terminal the prompt reads stdin's EOF as a cancel, and the run
|
|
584
|
+
// ended in exit 130, the code that means the user pressed Ctrl-C.
|
|
585
|
+
if (!options.yes && !isInteractive()) {
|
|
586
|
+
console.error(formatLabeled('Error', 'Cannot confirm the reset: no interactive terminal.'));
|
|
587
|
+
console.error(formatLabeled('Fix', 'tospec config reset --all --yes'));
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
517
591
|
if (!options.yes) {
|
|
518
592
|
const { confirm } = await import('@inquirer/prompts');
|
|
519
593
|
let confirmed;
|
|
@@ -536,7 +610,14 @@ export function registerConfigCommand(program) {
|
|
|
536
610
|
return;
|
|
537
611
|
}
|
|
538
612
|
}
|
|
539
|
-
|
|
613
|
+
// The one write allowed over an unparseable file: replacing it is the
|
|
614
|
+
// point, and without it a broken config has no recovery from the CLI.
|
|
615
|
+
//
|
|
616
|
+
// An empty object, not DEFAULT_CONFIG: written out, the defaults became
|
|
617
|
+
// stored values, and `list` labelled the reset profile "(explicit)". Not
|
|
618
|
+
// a deleted file either: with none here, reads fall back to a legacy
|
|
619
|
+
// config and the reset would bring its settings back.
|
|
620
|
+
saveGlobalConfig({}, { overwriteUnreadable: true });
|
|
540
621
|
if (options.json) {
|
|
541
622
|
emitSuccess({ config: getGlobalConfig() });
|
|
542
623
|
return;
|
|
@@ -549,32 +630,51 @@ export function registerConfigCommand(program) {
|
|
|
549
630
|
.action(async () => {
|
|
550
631
|
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
551
632
|
if (!editor) {
|
|
552
|
-
console.error('Error
|
|
553
|
-
console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
|
|
554
|
-
console.error('Example: export EDITOR=vim');
|
|
633
|
+
console.error(formatLabeled('Error', 'No editor configured\nSet the EDITOR or VISUAL environment variable to your preferred editor\nExample: export EDITOR=vim'));
|
|
555
634
|
process.exitCode = 1;
|
|
556
635
|
return;
|
|
557
636
|
}
|
|
558
637
|
const configPath = getGlobalConfigPath();
|
|
638
|
+
// Runs only when the write target is absent, so it overwrites nothing; the
|
|
639
|
+
// unreadable guard would otherwise send a user whose only file is a broken
|
|
640
|
+
// legacy one back to this very command.
|
|
641
|
+
//
|
|
642
|
+
// Seeded empty for the reason `reset` writes `{}`: a seeded default is a
|
|
643
|
+
// stored value, so closing the editor unchanged left the profile
|
|
644
|
+
// "(explicit)" though nobody had chosen it.
|
|
559
645
|
if (!fs.existsSync(configPath)) {
|
|
560
|
-
saveGlobalConfig({
|
|
646
|
+
saveGlobalConfig({}, { overwriteUnreadable: true });
|
|
647
|
+
}
|
|
648
|
+
// `EDITOR="code --wait"` is a command line, not a program name: spawned
|
|
649
|
+
// whole, it was looked up as a file literally named "code --wait" and
|
|
650
|
+
// failed with ENOENT. Split here, never handed to a shell — `shell: true`
|
|
651
|
+
// would also run whatever else the variable contains.
|
|
652
|
+
const [command, ...editorArgs] = splitCommandLine(editor);
|
|
653
|
+
if (!command) {
|
|
654
|
+
console.error('Error: EDITOR is set but names no program.');
|
|
655
|
+
process.exitCode = 1;
|
|
656
|
+
return;
|
|
561
657
|
}
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
await new Promise((resolve
|
|
568
|
-
child
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
child.on('error', reject);
|
|
658
|
+
// cross-spawn, not child_process.spawn: on Windows `code` is `code.cmd`,
|
|
659
|
+
// which only cmd.exe can run, and reaching it safely means cmd.exe's
|
|
660
|
+
// quoting rules (`%`, `^`, `&`, `"`) for every argument, the config path
|
|
661
|
+
// included. That escaping is the library's whole purpose; a hand-rolled
|
|
662
|
+
// copy is the BatBadBut bug class Node itself had to patch.
|
|
663
|
+
const outcome = await new Promise((resolve) => {
|
|
664
|
+
const child = crossSpawn(command, [...editorArgs, configPath], { stdio: 'inherit' });
|
|
665
|
+
child.on('close', (code) => resolve({ code }));
|
|
666
|
+
child.on('error', (error) => resolve({ error }));
|
|
577
667
|
});
|
|
668
|
+
if ('error' in outcome) {
|
|
669
|
+
console.error(`Error: Could not start editor "${command}": ${outcome.error.message}`);
|
|
670
|
+
process.exitCode = 1;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (outcome.code !== 0) {
|
|
674
|
+
console.error(`Error: Editor "${command}" exited with code ${outcome.code}.`);
|
|
675
|
+
process.exitCode = 1;
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
578
678
|
try {
|
|
579
679
|
const rawConfig = fs.readFileSync(configPath, 'utf-8');
|
|
580
680
|
// `config edit` just handed this file to the user's editor, and a
|
|
@@ -593,8 +693,7 @@ export function registerConfigCommand(program) {
|
|
|
593
693
|
console.error(`Error: Config file not found at ${configPath}`);
|
|
594
694
|
}
|
|
595
695
|
else if (error instanceof SyntaxError) {
|
|
596
|
-
console.error(
|
|
597
|
-
console.error(error.message);
|
|
696
|
+
console.error(formatLabeled('Error', `Invalid JSON in ${configPath}\n${error.message}`));
|
|
598
697
|
}
|
|
599
698
|
else {
|
|
600
699
|
console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -615,7 +714,7 @@ export function registerConfigCommand(program) {
|
|
|
615
714
|
}
|
|
616
715
|
console.error(`Error: ${message}`);
|
|
617
716
|
if (fix)
|
|
618
|
-
console.error(
|
|
717
|
+
console.error(formatLabeled('Fix', fix));
|
|
619
718
|
process.exitCode = 1;
|
|
620
719
|
};
|
|
621
720
|
/** The resolved state, so a caller need not re-read it with `config list`. */
|
|
@@ -629,7 +728,8 @@ export function registerConfigCommand(program) {
|
|
|
629
728
|
// The stored list is dropped, not rewritten to ALL_WORKFLOWS: rewriting it
|
|
630
729
|
// manufactures the state `ignoredWorkflowsDiagnostic` warns about.
|
|
631
730
|
if (preset === 'core') {
|
|
632
|
-
|
|
731
|
+
assertGlobalConfigWritable(['profile', 'workflows']);
|
|
732
|
+
const config = getStoredGlobalConfig();
|
|
633
733
|
config.profile = 'core';
|
|
634
734
|
delete config.workflows;
|
|
635
735
|
saveGlobalConfig(config);
|
|
@@ -639,11 +739,17 @@ export function registerConfigCommand(program) {
|
|
|
639
739
|
// The command `config set workflows` and `config list` both name as the
|
|
640
740
|
// way to make a workflow list take effect.
|
|
641
741
|
if (preset === 'custom') {
|
|
642
|
-
|
|
742
|
+
// Before the read: an unparseable file reads as the defaults, which have
|
|
743
|
+
// no workflows list, so the refusal below would send the user to
|
|
744
|
+
// `config set workflows` — a write the same file then refuses.
|
|
745
|
+
assertGlobalConfigReadable();
|
|
746
|
+
// An invalid `workflows` is refused here, not counted as a list below.
|
|
747
|
+
assertGlobalConfigWritable(['profile']);
|
|
748
|
+
const config = getStoredGlobalConfig();
|
|
643
749
|
// Without a list, `custom` resolves to zero workflows and the next
|
|
644
750
|
// `tospec update` deletes every installed skill directory.
|
|
645
751
|
if (!config.workflows || config.workflows.length === 0) {
|
|
646
|
-
fail('Profile "custom" needs a workflows list, and none is configured.',
|
|
752
|
+
fail('Profile "custom" needs a workflows list, and none is configured.', workflowsListFix());
|
|
647
753
|
return;
|
|
648
754
|
}
|
|
649
755
|
config.profile = 'custom';
|
|
@@ -663,13 +769,20 @@ export function registerConfigCommand(program) {
|
|
|
663
769
|
return;
|
|
664
770
|
}
|
|
665
771
|
if (!process.stdout.isTTY) {
|
|
666
|
-
console.error('Interactive mode required. Use `tospec config profile core|custom` or set config via environment/flags.');
|
|
772
|
+
console.error('Error: Interactive mode required. Use `tospec config profile core|custom` or set config via environment/flags.');
|
|
667
773
|
process.exitCode = 1;
|
|
668
774
|
return;
|
|
669
775
|
}
|
|
776
|
+
// Before the picker: the save at its end would refuse the file anyway, after
|
|
777
|
+
// the user had answered every prompt against defaults shown as "current".
|
|
778
|
+
assertGlobalConfigReadable();
|
|
670
779
|
const { select, checkbox, confirm } = await import('@inquirer/prompts');
|
|
671
780
|
try {
|
|
781
|
+
// Shown from what reads resolve, saved onto what the file holds: for a
|
|
782
|
+
// file with an invalid field the two differ, and saving the defaults
|
|
783
|
+
// would erase the user's other settings.
|
|
672
784
|
const config = getGlobalConfig();
|
|
785
|
+
const stored = getStoredGlobalConfig();
|
|
673
786
|
const currentState = resolveCurrentProfileState(config);
|
|
674
787
|
console.log('\nCurrent profile settings');
|
|
675
788
|
console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
|
|
@@ -745,14 +858,14 @@ export function registerConfigCommand(program) {
|
|
|
745
858
|
console.log(` ${line}`);
|
|
746
859
|
}
|
|
747
860
|
console.log();
|
|
748
|
-
|
|
861
|
+
stored.profile = nextState.profile;
|
|
749
862
|
// Same rule as the `core` preset above: a list stored under `core` is an
|
|
750
863
|
// ignored key every later `config list` warns about.
|
|
751
864
|
if (nextState.profile === 'core')
|
|
752
|
-
delete
|
|
865
|
+
delete stored.workflows;
|
|
753
866
|
else
|
|
754
|
-
|
|
755
|
-
saveGlobalConfig(
|
|
867
|
+
stored.workflows = nextState.workflows;
|
|
868
|
+
saveGlobalConfig(stored);
|
|
756
869
|
const projectDir = process.cwd();
|
|
757
870
|
const tospecDir = path.join(projectDir, TOSPEC_DIR_NAME);
|
|
758
871
|
if (fs.existsSync(tospecDir)) {
|
|
@@ -768,8 +881,8 @@ export function registerConfigCommand(program) {
|
|
|
768
881
|
console.log('Run `tospec update` in your other projects to apply.');
|
|
769
882
|
}
|
|
770
883
|
catch (error) {
|
|
771
|
-
console.error(
|
|
772
|
-
console.error('
|
|
884
|
+
console.error(`Error: \`tospec update\` failed: ${asErrorMessage(error)}`);
|
|
885
|
+
console.error('Fix: Run `tospec update` manually to apply the profile changes.');
|
|
773
886
|
process.exitCode = 1;
|
|
774
887
|
}
|
|
775
888
|
return;
|