@seanmars/tospec 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +243 -0
- package/README.md +10 -6
- package/assets/dashboard/lib/derive.js +11 -5
- package/assets/dashboard/lib/store.js +4 -2
- package/assets/dashboard/views/change.js +5 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +42 -7
- 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.d.ts +9 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +77 -21
- 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 +3 -3
- package/dist/core/dashboard-hub.d.ts.map +1 -1
- package/dist/core/dashboard-hub.js +16 -4
- package/dist/core/dashboard-hub.js.map +1 -1
- package/dist/core/dashboard-routes.d.ts +2 -1
- package/dist/core/dashboard-routes.d.ts.map +1 -1
- package/dist/core/dashboard-routes.js +102 -33
- package/dist/core/dashboard-routes.js.map +1 -1
- 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 +116 -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 +9 -2
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +24 -4
- 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/index.d.ts +1 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +1 -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 +66 -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 +4 -1
- 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
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import { stripBom } from './parsers/requirement-text.js';
|
|
5
|
+
import { findInvalidConfigFields } from './config-schema.js';
|
|
5
6
|
export const GLOBAL_CONFIG_DIR_NAME = 'tospec';
|
|
6
7
|
export const GLOBAL_CONFIG_FILE_NAME = 'config.json';
|
|
7
8
|
export const GLOBAL_DATA_DIR_NAME = 'tospec';
|
|
@@ -78,6 +79,21 @@ export function getLegacyConfigDir(options = {}) {
|
|
|
78
79
|
export function getGlobalConfigPath() {
|
|
79
80
|
return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE_NAME);
|
|
80
81
|
}
|
|
82
|
+
// Once per line per process: init reads the config several times in one run,
|
|
83
|
+
// and the same line repeated reads as several problems. Keyed by the line, not
|
|
84
|
+
// the path, so a file that turns from one kind of invalid into another within
|
|
85
|
+
// a process still reports the new problem.
|
|
86
|
+
const issuedWarnings = new Set();
|
|
87
|
+
function warnOnce(line) {
|
|
88
|
+
if (issuedWarnings.has(line))
|
|
89
|
+
return;
|
|
90
|
+
issuedWarnings.add(line);
|
|
91
|
+
console.error(line);
|
|
92
|
+
}
|
|
93
|
+
function warnInvalidSettings(configPath, invalid) {
|
|
94
|
+
const details = invalid.map((entry) => entry.detail).join('; ');
|
|
95
|
+
warnOnce(`Warning: Invalid settings in ${configPath} (${details}), ignoring the whole file and using defaults`);
|
|
96
|
+
}
|
|
81
97
|
function readConfigFile(configPath) {
|
|
82
98
|
try {
|
|
83
99
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -90,7 +106,17 @@ function readConfigFile(configPath) {
|
|
|
90
106
|
// SyntaxError, and spreading either yields an index-keyed object that would
|
|
91
107
|
// be returned as configuration.
|
|
92
108
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
93
|
-
|
|
109
|
+
warnOnce(`Warning: ${configPath} is not a JSON object, using defaults`);
|
|
110
|
+
return { ...DEFAULT_CONFIG };
|
|
111
|
+
}
|
|
112
|
+
// The schema `config set` writes through. Without it a hand-edited
|
|
113
|
+
// `"workflows": "explore"` reached update as a string (`.filter` threw) and
|
|
114
|
+
// init as seven one-letter workflows. The whole file falls back, as an
|
|
115
|
+
// unparseable one does: dropping only the bad field would leave `custom`
|
|
116
|
+
// with no workflows, and the next update would remove every skill.
|
|
117
|
+
const invalid = findInvalidConfigFields(parsed);
|
|
118
|
+
if (invalid.length > 0) {
|
|
119
|
+
warnInvalidSettings(configPath, invalid);
|
|
94
120
|
return { ...DEFAULT_CONFIG };
|
|
95
121
|
}
|
|
96
122
|
// Loaded values win; unknown keys from older versions pass through.
|
|
@@ -99,7 +125,7 @@ function readConfigFile(configPath) {
|
|
|
99
125
|
catch (error) {
|
|
100
126
|
// A missing file is the normal case and says nothing.
|
|
101
127
|
if (error instanceof SyntaxError) {
|
|
102
|
-
|
|
128
|
+
warnOnce(`Warning: Invalid JSON in ${configPath}, using defaults`);
|
|
103
129
|
}
|
|
104
130
|
return { ...DEFAULT_CONFIG };
|
|
105
131
|
}
|
|
@@ -131,11 +157,130 @@ function resolveConfigPath(configDir, legacyConfigDir) {
|
|
|
131
157
|
export function getEffectiveConfigPath() {
|
|
132
158
|
return resolveConfigPath(getGlobalConfigDir(), getLegacyConfigDir());
|
|
133
159
|
}
|
|
134
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Merged over the defaults; a missing or invalid file yields the defaults, and
|
|
162
|
+
* so does a file with any field the schema refuses.
|
|
163
|
+
*/
|
|
135
164
|
export function getGlobalConfig() {
|
|
136
165
|
return loadGlobalConfigFrom(getGlobalConfigDir(), getLegacyConfigDir());
|
|
137
166
|
}
|
|
138
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The object the effective file holds, or `{}` when it is missing or
|
|
169
|
+
* unparseable. Without the defaults, so a caller can tell an explicit value
|
|
170
|
+
* from a default.
|
|
171
|
+
*/
|
|
172
|
+
export function getStoredGlobalConfigFields() {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(stripBom(fs.readFileSync(getEffectiveConfigPath(), 'utf-8')));
|
|
175
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Missing, or unparseable: saveGlobalConfig refuses to write over the latter.
|
|
181
|
+
}
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The file's settings, without the schema check reads apply: the base for a
|
|
186
|
+
* read-modify-write. getGlobalConfig() yields bare defaults for a file with one
|
|
187
|
+
* invalid field, so saving from it would erase every other setting the user
|
|
188
|
+
* wrote. Without the defaults too: merged in, the first `config set` of any key
|
|
189
|
+
* saved `profile: "core"` as well, and `config list` then called the default
|
|
190
|
+
* profile explicit. Reads still merge the defaults, so nothing reads the gap.
|
|
191
|
+
*/
|
|
192
|
+
export function getStoredGlobalConfig() {
|
|
193
|
+
return getStoredGlobalConfigFields();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Whether the config file a read lands on exists but cannot be used: invalid
|
|
197
|
+
* JSON, not a JSON object, or unreadable (EACCES). A missing file is not
|
|
198
|
+
* unreadable — it is the normal first-run state.
|
|
199
|
+
*/
|
|
200
|
+
export function isGlobalConfigUnreadable() {
|
|
201
|
+
const configPath = getEffectiveConfigPath();
|
|
202
|
+
let content;
|
|
203
|
+
try {
|
|
204
|
+
content = fs.readFileSync(configPath, 'utf-8');
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return error.code !== 'ENOENT';
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(stripBom(content));
|
|
211
|
+
return typeof parsed !== 'object' || parsed === null || Array.isArray(parsed);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Carries the `status[]` diagnostic the CLI's failure path reads. */
|
|
218
|
+
export class GlobalConfigUnreadableError extends Error {
|
|
219
|
+
diagnostic;
|
|
220
|
+
constructor(configPath) {
|
|
221
|
+
const message = `${configPath} cannot be parsed, so tospec refuses to overwrite it: the write would replace every setting in it with defaults.`;
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = 'GlobalConfigUnreadableError';
|
|
224
|
+
this.diagnostic = {
|
|
225
|
+
severity: 'error',
|
|
226
|
+
code: 'global_config_unreadable',
|
|
227
|
+
message,
|
|
228
|
+
fix: 'Fix the file with "tospec config edit", or replace it with defaults with "tospec config reset --all".',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* For a command whose answer comes from the file, not only its write: `config
|
|
234
|
+
* unset` on a key the defaults lack writes nothing, and would report "was not
|
|
235
|
+
* set" about a file it could not read.
|
|
236
|
+
*/
|
|
237
|
+
export function assertGlobalConfigReadable() {
|
|
238
|
+
if (isGlobalConfigUnreadable()) {
|
|
239
|
+
throw new GlobalConfigUnreadableError(getEffectiveConfigPath());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Carries the `status[]` diagnostic the CLI's failure path reads. */
|
|
243
|
+
export class GlobalConfigInvalidError extends Error {
|
|
244
|
+
diagnostic;
|
|
245
|
+
constructor(configPath, invalid) {
|
|
246
|
+
const details = invalid.map((entry) => entry.detail).join('; ');
|
|
247
|
+
const message = `${configPath} has invalid settings (${details}), so reads ignore the whole file and this write would not take effect.`;
|
|
248
|
+
super(message);
|
|
249
|
+
this.name = 'GlobalConfigInvalidError';
|
|
250
|
+
this.diagnostic = {
|
|
251
|
+
severity: 'error',
|
|
252
|
+
code: 'global_config_invalid',
|
|
253
|
+
message,
|
|
254
|
+
fix: `Repair it first with "tospec config unset ${invalid[0].field}" or "tospec config set ${invalid[0].field}", or fix the file with "tospec config edit".`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Refuses a write that leaves every invalid field as it was: reads ignore such
|
|
260
|
+
* a file, so the write would report success and change nothing, which is why
|
|
261
|
+
* an unparseable file refuses writes too. A write to an invalid field is the
|
|
262
|
+
* repair and goes through even with another one left; requiring all of them at
|
|
263
|
+
* once would refuse `set` and `unset` alike when two are bad, each pointing at
|
|
264
|
+
* the other.
|
|
265
|
+
*/
|
|
266
|
+
export function assertGlobalConfigWritable(touchedKeys) {
|
|
267
|
+
const invalid = findInvalidConfigFields(getStoredGlobalConfigFields());
|
|
268
|
+
if (invalid.length === 0)
|
|
269
|
+
return;
|
|
270
|
+
if (invalid.some((entry) => touchedKeys.includes(entry.field)))
|
|
271
|
+
return;
|
|
272
|
+
throw new GlobalConfigInvalidError(getEffectiveConfigPath(), invalid);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Refuses to write over a config file that exists but cannot be parsed. Reads
|
|
276
|
+
* fall back to defaults for such a file, so a read-modify-write (`config set`,
|
|
277
|
+
* `unset`, `profile`) would otherwise save those defaults and erase the user's
|
|
278
|
+
* whole file over one stray comma. `overwriteUnreadable` is for
|
|
279
|
+
* `config reset --all`, the one write whose purpose is to replace the file.
|
|
280
|
+
*/
|
|
281
|
+
export function saveGlobalConfig(config, options = {}) {
|
|
282
|
+
if (!options.overwriteUnreadable)
|
|
283
|
+
assertGlobalConfigReadable();
|
|
139
284
|
const configDir = getGlobalConfigDir();
|
|
140
285
|
const configPath = getGlobalConfigPath();
|
|
141
286
|
if (!fs.existsSync(configDir)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-config.js","sourceRoot":"","sources":["../../src/core/global-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"global-config.js","sourceRoot":"","sources":["../../src/core/global-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAA2B,MAAM,oBAAoB,CAAC;AAEtF,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;AAC/C,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAAC;AACrD,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAW7C,MAAM,cAAc,GAAiB;IACnC,OAAO,EAAE,MAAM;CAChB,CAAC;AAYF;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAsB,EAAE;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IAEnD,8DAA8D;IAC9D,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC;IAC1C,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAChD,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,sBAAsB,CAAC,CAAC;AAC9E,CAAC;AAED,mEAAmE;AACnE,SAAS,cAAc,CAAC,QAAyB,EAAE,GAAG,QAAkB;IACtE,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;QAC9B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAO,GAAsB,EAAE;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IACnD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAEhD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,2BAA2B;QAC3B,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;QACtC,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,oBAAoB,CAAC,CAAC;QACtE,CAAC;QACD,gCAAgC;QAChC,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;IACrF,CAAC;IAED,8BAA8B;IAC9B,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAsB,EAAE;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IAEnD,IAAI,GAAG,CAAC,eAAe,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChD,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,OAAO,OAAO;QACZ,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,sBAAsB,CAAC;QAC3D,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,sBAAsB,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,uBAAuB,CAAC,CAAC;AAClE,CAAC;AAED,6EAA6E;AAC7E,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C;AAC3C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;AAEzC,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IACrC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,mBAAmB,CAAC,UAAkB,EAAE,OAA6B;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,gCAAgC,UAAU,KAAK,OAAO,+CAA+C,CAAC,CAAC;AAClH,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB;IACxC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,6EAA6E;QAC7E,4EAA4E;QAC5E,6EAA6E;QAC7E,2DAA2D;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,2EAA2E;QAC3E,4EAA4E;QAC5E,gCAAgC;QAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,QAAQ,CAAC,YAAY,UAAU,uCAAuC,CAAC,CAAC;YACxE,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;QAC/B,CAAC;QACD,mEAAmE;QACnE,4EAA4E;QAC5E,uEAAuE;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,OAAO,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACzC,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;QAC/B,CAAC;QACD,oEAAoE;QACpE,OAAO,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,sDAAsD;QACtD,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,QAAQ,CAAC,4BAA4B,UAAU,kBAAkB,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,eAAuB;IAC7E,MAAM,UAAU,GAAG,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACjE,OAAO,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,cAAc,EAAE,CAAC;AACxF,CAAC;AAED,8DAA8D;AAC9D,SAAS,iBAAiB,CAAC,SAAiB,EAAE,eAAuB;IACnE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;IACjE,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC;IACvE,OAAO,UAAU,KAAK,UAAU,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,iBAAiB,CAAC,kBAAkB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,oBAAoB,CAAC,kBAAkB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAChC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,sBAAsB,EAAE,EAAE,OAAO,CAAC,CAAC,CAC7D,CAAC;QACF,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5E,OAAO,MAAiC,CAAC;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;IAChF,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,2BAA2B,EAAE,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAC5C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,QAAQ,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAC3C,UAAU,CAAoE;IAEvF,YAAY,UAAkB;QAC5B,MAAM,OAAO,GAAG,GAAG,UAAU,kHAAkH,CAAC;QAChJ,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,IAAI,CAAC,UAAU,GAAG;YAChB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,0BAA0B;YAChC,OAAO;YACP,GAAG,EAAE,uGAAuG;SAC7G,CAAC;IACJ,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B;IACxC,IAAI,wBAAwB,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,2BAA2B,CAAC,sBAAsB,EAAE,CAAC,CAAC;IAClE,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACxC,UAAU,CAAoE;IAEvF,YAAY,UAAkB,EAAE,OAA6B;QAC3D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,GAAG,UAAU,0BAA0B,OAAO,yEAAyE,CAAC;QACxI,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;QACvC,IAAI,CAAC,UAAU,GAAG;YAChB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO;YACP,GAAG,EAAE,6CAA6C,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,2BAA2B,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,+CAA+C;SAC7J,CAAC;IACJ,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CAAC,WAA8B;IACvE,MAAM,OAAO,GAAG,uBAAuB,CAAC,2BAA2B,EAAE,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO;IACvE,MAAM,IAAI,wBAAwB,CAAC,sBAAsB,EAAE,EAAE,OAAO,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAoB,EACpB,OAAO,GAAsC,EAAE;IAE/C,IAAI,CAAC,OAAO,CAAC,mBAAmB;QAAE,0BAA0B,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IAEzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AAChF,CAAC"}
|
package/dist/core/id.d.ts
CHANGED
|
@@ -6,4 +6,13 @@ export declare const KEBAB_ID_REGEX: RegExp;
|
|
|
6
6
|
export declare function isKebabId(value: string): boolean;
|
|
7
7
|
/** Human rendering of the grammar, shared so the wording never forks. */
|
|
8
8
|
export declare const KEBAB_ID_DESCRIPTION = "must be kebab-case with lowercase letters, numbers, and single hyphen separators";
|
|
9
|
+
export declare function isUnicodeKebabName(value: string): boolean;
|
|
10
|
+
export declare const UNICODE_KEBAB_NAME_DESCRIPTION = "is not kebab-case (lowercase letters, numbers, and single hyphen separators; letters with no case, such as \u4E2D\u6587, are allowed)";
|
|
11
|
+
/**
|
|
12
|
+
* Why `name` cannot exist as a directory on every OS, or undefined when it can.
|
|
13
|
+
* A name created on Linux or macOS that Windows refuses makes the repo fail to
|
|
14
|
+
* check out there, so the rule is Windows' even when the project never runs on
|
|
15
|
+
* it. Worded to follow the name in a sentence: `"con" is a reserved ...`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function portableNameFault(name: string): string | undefined;
|
|
9
18
|
//# sourceMappingURL=id.d.ts.map
|
package/dist/core/id.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAgC,CAAC;AAE5D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,qFACmD,CAAC"}
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAgC,CAAC;AAE5D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,qFACmD,CAAC;AAWrF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,eAAO,MAAM,8BAA8B,0IACoF,CAAC;AAShI;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUlE"}
|
package/dist/core/id.js
CHANGED
|
@@ -8,4 +8,41 @@ export function isKebabId(value) {
|
|
|
8
8
|
}
|
|
9
9
|
/** Human rendering of the grammar, shared so the wording never forks. */
|
|
10
10
|
export const KEBAB_ID_DESCRIPTION = 'must be kebab-case with lowercase letters, numbers, and single hyphen separators';
|
|
11
|
+
/**
|
|
12
|
+
* Kebab-case for names a user writes in their own language: a capability is
|
|
13
|
+
* named after a functional area, and `list --specs` already lays out names like
|
|
14
|
+
* `中文`. Letters with no case (CJK), modifier letters (the `ー` in katakana) and
|
|
15
|
+
* combining marks count as lowercase; what stays out is the ASCII grammar's
|
|
16
|
+
* exclusions — uppercase, `_`, whitespace, doubled or edge hyphens.
|
|
17
|
+
*/
|
|
18
|
+
const UNICODE_KEBAB_NAME_REGEX = /^[\p{Ll}\p{Lm}\p{Lo}\p{M}\p{Nd}]+(?:-[\p{Ll}\p{Lm}\p{Lo}\p{M}\p{Nd}]+)*$/u;
|
|
19
|
+
export function isUnicodeKebabName(value) {
|
|
20
|
+
return UNICODE_KEBAB_NAME_REGEX.test(value);
|
|
21
|
+
}
|
|
22
|
+
export const UNICODE_KEBAB_NAME_DESCRIPTION = 'is not kebab-case (lowercase letters, numbers, and single hyphen separators; letters with no case, such as 中文, are allowed)';
|
|
23
|
+
// Windows refuses these as a file or directory name in any letter case and with
|
|
24
|
+
// any extension (`con.md` is the console too). The superscript digits are in
|
|
25
|
+
// Microsoft's list as well.
|
|
26
|
+
const WINDOWS_RESERVED_NAME_REGEX = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/iu;
|
|
27
|
+
// The characters Win32 forbids in a name, plus the control range it also forbids.
|
|
28
|
+
const WINDOWS_FORBIDDEN_CHAR_REGEX = /[<>:"/\\|?*\u0000-\u001f]/u;
|
|
29
|
+
/**
|
|
30
|
+
* Why `name` cannot exist as a directory on every OS, or undefined when it can.
|
|
31
|
+
* A name created on Linux or macOS that Windows refuses makes the repo fail to
|
|
32
|
+
* check out there, so the rule is Windows' even when the project never runs on
|
|
33
|
+
* it. Worded to follow the name in a sentence: `"con" is a reserved ...`.
|
|
34
|
+
*/
|
|
35
|
+
export function portableNameFault(name) {
|
|
36
|
+
if (WINDOWS_RESERVED_NAME_REGEX.test(name))
|
|
37
|
+
return 'is a reserved device name on Windows';
|
|
38
|
+
const forbidden = WINDOWS_FORBIDDEN_CHAR_REGEX.exec(name);
|
|
39
|
+
if (forbidden) {
|
|
40
|
+
const shown = forbidden[0] < ' ' ? `U+${forbidden[0].charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()}` : forbidden[0];
|
|
41
|
+
return `contains "${shown}", which Windows does not allow in a file name`;
|
|
42
|
+
}
|
|
43
|
+
// Win32 strips a trailing dot or space, so `auth.` and `auth` name one directory.
|
|
44
|
+
if (/[. ]$/u.test(name))
|
|
45
|
+
return 'ends with a dot or a space, which Windows strips from a file name';
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
11
48
|
//# sourceMappingURL=id.js.map
|
package/dist/core/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAE5D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kFAAkF,CAAC"}
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAE5D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kFAAkF,CAAC;AAErF;;;;;;GAMG;AACH,MAAM,wBAAwB,GAAG,2EAA2E,CAAC;AAE7G,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,MAAM,8BAA8B,GACzC,6HAA6H,CAAC;AAEhI,gFAAgF;AAChF,6EAA6E;AAC7E,4BAA4B;AAC5B,MAAM,2BAA2B,GAAG,0DAA0D,CAAC;AAC/F,kFAAkF;AAClF,MAAM,4BAA4B,GAAG,4BAA4B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,sCAAsC,CAAC;IAC1F,MAAM,SAAS,GAAG,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAChI,OAAO,aAAa,KAAK,gDAAgD,CAAC;IAC5E,CAAC;IACD,kFAAkF;IAClF,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,mEAAmE,CAAC;IACpG,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/core/init.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Sets up tospec Agent Skills for Claude Code and Codex. */
|
|
2
2
|
type InitCommandOptions = {
|
|
3
3
|
tools?: string;
|
|
4
|
+
/** Accepted for compatibility and ignored: init never asks before overwriting. */
|
|
4
5
|
force?: boolean;
|
|
5
6
|
interactive?: boolean;
|
|
6
7
|
profile?: string;
|
|
@@ -11,7 +12,6 @@ type InitCommandOptions = {
|
|
|
11
12
|
export declare const INIT_FAILURE_PAYLOAD: Record<string, unknown>;
|
|
12
13
|
export declare class InitCommand {
|
|
13
14
|
private readonly toolsArg?;
|
|
14
|
-
private readonly force;
|
|
15
15
|
private readonly interactiveOption?;
|
|
16
16
|
private readonly profileOverride?;
|
|
17
17
|
private readonly json;
|
|
@@ -50,9 +50,16 @@ export declare class InitCommand {
|
|
|
50
50
|
*/
|
|
51
51
|
private createDirectoryStructure;
|
|
52
52
|
private generateSkillsAndCommands;
|
|
53
|
+
/**
|
|
54
|
+
* The canonical sweep cannot stop at .agents/: every dependent tool is a copy
|
|
55
|
+
* of it, so pruning only the canonical side leaves a tool this run did not
|
|
56
|
+
* select holding skills the profile dropped, and the two directories disagree
|
|
57
|
+
* until the next `update`. Prune only, never write: the selection still
|
|
58
|
+
* decides which tools receive content.
|
|
59
|
+
*/
|
|
60
|
+
private pruneUnselectedTools;
|
|
53
61
|
private createConfig;
|
|
54
62
|
private displaySuccessMessage;
|
|
55
|
-
private removeLegacyCommands;
|
|
56
63
|
}
|
|
57
64
|
export {};
|
|
58
65
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/core/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/core/init.ts"],"names":[],"mappings":"AAAA,6DAA6D;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/core/init.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAoD7D,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAA8B,CAAC;AAsCxF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAU;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAE/B,YAAY,OAAO,GAAE,kBAAuB,EAK3C;IAED;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;OAGG;IACH,OAAO,CAAC,IAAI;IAQZ,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2B;IAElD,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJ/C;YAEa,QAAQ;IAMtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IAiBhC,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,sBAAsB;YAYhB,gBAAgB;IAoG9B,OAAO,CAAC,eAAe;IAyDvB,OAAO,CAAC,aAAa;IAkCrB;;;;OAIG;YACW,wBAAwB;YAcxB,yBAAyB;IAgGvC;;;;;;OAMG;YACW,oBAAoB;YAqCpB,YAAY;IAgB1B,OAAO,CAAC,qBAAqB;CA8F9B"}
|
package/dist/core/init.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Sets up tospec Agent Skills for Claude Code and Codex. */
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
3
|
import { createRequire } from 'module';
|
|
5
4
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
6
5
|
import { ensureDirectoryAnchor, tospecLayoutDirs } from './project-layout.js';
|
|
@@ -8,10 +7,10 @@ import { AI_TOOLS, TOSPEC_DIR_NAME, fallbackToolIds, resolveToolSelection, } fro
|
|
|
8
7
|
import { isInteractive } from '../utils/interactive.js';
|
|
9
8
|
import { serializeConfig } from './config-prompts.js';
|
|
10
9
|
import { copyDir } from '../utils/link.js';
|
|
11
|
-
import { removeLegacySkillDirs, removeUnselectedSkillDirs, removeLegacyToolSkillLayout, getToolsWithSkillsDir, getToolStates, getSkillTemplates, writeSkillDir, findWorkflowReferenceGaps, describeWorkflowReferenceGaps, DASHBOARD_MOD_TOOL_ID, syncDashboardMod, } from './shared/index.js';
|
|
10
|
+
import { removeLegacySkillDirs, removeUnselectedSkillDirs, removeLegacyToolSkillLayout, removeLegacyCommands, getToolsWithSkillsDir, getToolStates, getConfiguredTools, getSkillTemplates, writeSkillDir, findWorkflowReferenceGaps, describeWorkflowReferenceGaps, DASHBOARD_MOD_TOOL_ID, DASHBOARD_MOD_DIR, syncDashboardMod, } from './shared/index.js';
|
|
12
11
|
import { getGlobalConfig } from './global-config.js';
|
|
13
12
|
import { runLegacyUserStateMigration } from './user-state-migration.js';
|
|
14
|
-
import { getProfileWorkflows
|
|
13
|
+
import { getProfileWorkflows } from './profiles.js';
|
|
15
14
|
import { getAvailableTools } from './available-tools.js';
|
|
16
15
|
import { resolveConfigFileInDir } from './project-config.js';
|
|
17
16
|
import { planWorkflowRules, writeWorkflowRules } from './shared/rules-generation.js';
|
|
@@ -26,15 +25,41 @@ function agentsSkillsDirFor(projectPath) {
|
|
|
26
25
|
}
|
|
27
26
|
/** The `--json` null-shape; mirrors the success payload with `init` nulled. */
|
|
28
27
|
export const INIT_FAILURE_PAYLOAD = { init: null, root: null };
|
|
28
|
+
/**
|
|
29
|
+
* A failure init can name and advise on. Carries a full `CommandDiagnostic`:
|
|
30
|
+
* `asStatus` passes one through by `typeof diagnostic.code === 'string'`, where
|
|
31
|
+
* a bare Error falls back to `init_error` with no `fix`.
|
|
32
|
+
*/
|
|
33
|
+
class InitError extends Error {
|
|
34
|
+
diagnostic;
|
|
35
|
+
constructor(code, message, fix, target) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'InitError';
|
|
38
|
+
this.diagnostic = { severity: 'error', code, message, target, fix };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A file (or anything but a directory) where the layout puts a directory makes
|
|
43
|
+
* `mkdir` fail with a bare `EEXIST`, which names neither the cause nor a next
|
|
44
|
+
* step. Checked before any prompt or write, so the run stops before asking for
|
|
45
|
+
* tools it cannot install. Parents precede children in the list, so a file at
|
|
46
|
+
* `tospec` is reported before a child path under it is ever probed.
|
|
47
|
+
*/
|
|
48
|
+
async function assertLayoutPathsAreDirectories(projectPath, tospecPath) {
|
|
49
|
+
for (const dir of [tospecPath, ...tospecLayoutDirs(tospecPath)]) {
|
|
50
|
+
if ((await FileSystemUtils.fileExists(dir)) && !(await FileSystemUtils.directoryExists(dir))) {
|
|
51
|
+
const relative = path.relative(projectPath, dir).split(path.sep).join('/');
|
|
52
|
+
throw new InitError('init_layout_path_not_directory', `"${relative}" exists and is not a directory, so init cannot create the tospec layout there.`, `Move or rename "${relative}", then rerun "tospec init".`, dir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
29
56
|
export class InitCommand {
|
|
30
57
|
toolsArg;
|
|
31
|
-
force;
|
|
32
58
|
interactiveOption;
|
|
33
59
|
profileOverride;
|
|
34
60
|
json;
|
|
35
61
|
constructor(options = {}) {
|
|
36
62
|
this.toolsArg = options.tools;
|
|
37
|
-
this.force = options.force ?? false;
|
|
38
63
|
this.interactiveOption = options.interactive;
|
|
39
64
|
this.profileOverride = options.profile;
|
|
40
65
|
this.json = options.json ?? false;
|
|
@@ -56,7 +81,7 @@ export class InitCommand {
|
|
|
56
81
|
if (!this.json) {
|
|
57
82
|
console.log(`Warning: ${message}`);
|
|
58
83
|
if (fix)
|
|
59
|
-
console.log(fix);
|
|
84
|
+
console.log(`Fix: ${fix}`);
|
|
60
85
|
}
|
|
61
86
|
}
|
|
62
87
|
/** Emitted alongside the success payload; empty means the key is omitted. */
|
|
@@ -67,6 +92,7 @@ export class InitCommand {
|
|
|
67
92
|
const tospecPath = path.join(projectPath, tospecDir);
|
|
68
93
|
// Detect without writing: even the permission probe must follow rule preflight.
|
|
69
94
|
const extendMode = await FileSystemUtils.directoryExists(tospecPath);
|
|
95
|
+
await assertLayoutPathsAreDirectories(projectPath, tospecPath);
|
|
70
96
|
const detectedTools = getAvailableTools(projectPath);
|
|
71
97
|
const canPrompt = this.canPromptInteractively();
|
|
72
98
|
if (canPrompt) {
|
|
@@ -112,12 +138,44 @@ export class InitCommand {
|
|
|
112
138
|
silent: this.json,
|
|
113
139
|
});
|
|
114
140
|
this.displaySuccessMessage(projectPath, validatedTools, results, configStatus);
|
|
141
|
+
// A warning, not progress prose: the run deleted from a tool the caller did
|
|
142
|
+
// not select, and under `--json` the envelope's `created` / `refreshed`
|
|
143
|
+
// otherwise say nothing about it.
|
|
144
|
+
if (results.prunedTools.length > 0) {
|
|
145
|
+
const removed = results.prunedTools.reduce((sum, tool) => sum + tool.removed, 0);
|
|
146
|
+
const where = results.prunedTools
|
|
147
|
+
.map((tool) => `${tool.name} (${tool.skillsDir}/skills/, ${tool.removed})`)
|
|
148
|
+
.join(', ');
|
|
149
|
+
this.warn('init_removed_deselected_skills', `Removed ${removed} skill director${removed === 1 ? 'y' : 'ies'} the profile '${initProfile}' does not select from tools this run did not set up: ${where}.`, 'To keep those workflows, add them with "tospec config profile", then run "tospec update".');
|
|
150
|
+
}
|
|
115
151
|
// After every write, so a run that dies in rule preflight never leaves
|
|
116
152
|
// advice about a state it did not reach; before the JSON envelope, or the
|
|
117
153
|
// warning is recorded into a document already on stdout.
|
|
118
154
|
if (hasResidualCodexOutput(projectPath)) {
|
|
119
155
|
this.warn('codex_residue', RESIDUAL_CODEX_NOTICE, RESIDUAL_CODEX_FIX);
|
|
120
156
|
}
|
|
157
|
+
// A tool that failed is a partial result, not a success: the envelope stays
|
|
158
|
+
// complete (`failed` names it) and the exit code says part of the run did
|
|
159
|
+
// not land, per the batch-command contract. Printed `Failed:` alone, a CI
|
|
160
|
+
// run or an agent reading exit 0 never saw it.
|
|
161
|
+
const failures = results.failedTools.map((f) => ({
|
|
162
|
+
severity: 'error',
|
|
163
|
+
code: 'init_tool_failed',
|
|
164
|
+
message: `Failed to set up ${f.name}: ${f.error.message}`,
|
|
165
|
+
target: f.toolId,
|
|
166
|
+
fix: "Fix the cause, then rerun 'tospec init'.",
|
|
167
|
+
}));
|
|
168
|
+
if (failures.length > 0) {
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
if (!this.json) {
|
|
171
|
+
for (const failure of failures) {
|
|
172
|
+
console.error(`Error: ${failure.message}`);
|
|
173
|
+
if (failure.fix)
|
|
174
|
+
console.error(`Fix: ${failure.fix}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const status = [...failures, ...this.jsonWarnings];
|
|
121
179
|
if (this.json) {
|
|
122
180
|
const globalCfg = getGlobalConfig();
|
|
123
181
|
const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
|
|
@@ -128,7 +186,9 @@ export class InitCommand {
|
|
|
128
186
|
workflows: activeWorkflows,
|
|
129
187
|
created: results.createdTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
|
|
130
188
|
refreshed: results.refreshedTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
|
|
131
|
-
|
|
189
|
+
// Keyed by tool id like `created` / `refreshed`: the display name is
|
|
190
|
+
// prose, and an agent matching it against `--tools` ids never could.
|
|
191
|
+
failed: results.failedTools.map((f) => ({ tool: f.toolId, error: f.error.message })),
|
|
132
192
|
// `created` only records that a tool was configured, so this is the
|
|
133
193
|
// only field saying what was installed, and it must count what
|
|
134
194
|
// reached a tool directory: `--tools none` writes nothing anywhere.
|
|
@@ -137,7 +197,7 @@ export class InitCommand {
|
|
|
137
197
|
: 0,
|
|
138
198
|
config: configStatus,
|
|
139
199
|
},
|
|
140
|
-
...(
|
|
200
|
+
...(status.length ? { status } : {}),
|
|
141
201
|
}, { path: projectPath, source: 'explicit' });
|
|
142
202
|
}
|
|
143
203
|
}
|
|
@@ -349,6 +409,10 @@ export class InitCommand {
|
|
|
349
409
|
const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
|
|
350
410
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
351
411
|
const skillTemplates = getSkillTemplates(workflows);
|
|
412
|
+
// Read before any sweep: an install is recognised by the very skill dirs the
|
|
413
|
+
// sweeps remove, so afterwards a pruned tool would no longer count as one.
|
|
414
|
+
const selectedIds = new Set(tools.map((tool) => tool.value));
|
|
415
|
+
const unselectedInstalled = getConfiguredTools(projectPath).filter((id) => !selectedIds.has(id));
|
|
352
416
|
// Canonical files, written once to .agents/. Gated on "any target at all" so
|
|
353
417
|
// `--tools none` writes nothing; dependency expansion guarantees .agents is
|
|
354
418
|
// present whenever anything else is.
|
|
@@ -362,9 +426,10 @@ export class InitCommand {
|
|
|
362
426
|
// on this run writing anything. Gated, `--tools none` would leave a retired
|
|
363
427
|
// skill dir loadable by every agent reading .agents/skills/, and a dropped
|
|
364
428
|
// workflow would come back on the next copy from the canonical directory.
|
|
365
|
-
await removeLegacySkillDirs(agentsSkillsDir)
|
|
366
|
-
|
|
367
|
-
removedCommandCount += await
|
|
429
|
+
const removedCanonical = (await removeLegacySkillDirs(agentsSkillsDir)) +
|
|
430
|
+
(await removeUnselectedSkillDirs(agentsSkillsDir, workflows));
|
|
431
|
+
removedCommandCount += await removeLegacyCommands(projectPath);
|
|
432
|
+
const prunedTools = await this.pruneUnselectedTools(projectPath, unselectedInstalled, workflows, removedCanonical);
|
|
368
433
|
for (const tool of tools) {
|
|
369
434
|
this.say(`Setting up ${tool.name}...`);
|
|
370
435
|
try {
|
|
@@ -397,7 +462,7 @@ export class InitCommand {
|
|
|
397
462
|
}
|
|
398
463
|
catch (error) {
|
|
399
464
|
this.say(`Failed for ${tool.name}`);
|
|
400
|
-
failedTools.push({ name: tool.name, error: error });
|
|
465
|
+
failedTools.push({ toolId: tool.value, name: tool.name, error: error });
|
|
401
466
|
}
|
|
402
467
|
}
|
|
403
468
|
return {
|
|
@@ -405,8 +470,47 @@ export class InitCommand {
|
|
|
405
470
|
refreshedTools,
|
|
406
471
|
failedTools,
|
|
407
472
|
removedCommandCount,
|
|
473
|
+
prunedTools,
|
|
408
474
|
};
|
|
409
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* The canonical sweep cannot stop at .agents/: every dependent tool is a copy
|
|
478
|
+
* of it, so pruning only the canonical side leaves a tool this run did not
|
|
479
|
+
* select holding skills the profile dropped, and the two directories disagree
|
|
480
|
+
* until the next `update`. Prune only, never write: the selection still
|
|
481
|
+
* decides which tools receive content.
|
|
482
|
+
*/
|
|
483
|
+
async pruneUnselectedTools(projectPath, toolIds, workflows, removedCanonical) {
|
|
484
|
+
const pruned = [];
|
|
485
|
+
const agentsSkillsDir = path.resolve(agentsSkillsDirFor(projectPath));
|
|
486
|
+
for (const toolId of toolIds) {
|
|
487
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
488
|
+
if (!tool?.skillsDir)
|
|
489
|
+
continue;
|
|
490
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
491
|
+
let removed;
|
|
492
|
+
if (path.resolve(skillsDir) === agentsSkillsDir) {
|
|
493
|
+
// Already swept above; counted here so the report names it.
|
|
494
|
+
removed = removedCanonical;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
removed =
|
|
498
|
+
(await removeLegacySkillDirs(skillsDir)) +
|
|
499
|
+
(await removeUnselectedSkillDirs(skillsDir, workflows));
|
|
500
|
+
// The mod goes with the last workflow, as a selected tool's sync does;
|
|
501
|
+
// with workflows left it stays untouched, since refreshing it is a write.
|
|
502
|
+
if (toolId === DASHBOARD_MOD_TOOL_ID && workflows.length === 0) {
|
|
503
|
+
const hadMod = await FileSystemUtils.directoryExists(path.join(skillsDir, DASHBOARD_MOD_DIR));
|
|
504
|
+
await syncDashboardMod(skillsDir, TOSPEC_VERSION, false);
|
|
505
|
+
if (hadMod)
|
|
506
|
+
removed += 1;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (removed > 0)
|
|
510
|
+
pruned.push({ name: tool.name, skillsDir: tool.skillsDir, removed });
|
|
511
|
+
}
|
|
512
|
+
return pruned;
|
|
513
|
+
}
|
|
410
514
|
async createConfig(tospecPath, extendMode) {
|
|
411
515
|
const configPath = path.join(tospecPath, 'config.yaml');
|
|
412
516
|
if (resolveConfigFileInDir(tospecPath)) {
|
|
@@ -494,26 +598,5 @@ export class InitCommand {
|
|
|
494
598
|
}
|
|
495
599
|
this.say();
|
|
496
600
|
}
|
|
497
|
-
async removeLegacyCommands(projectPath) {
|
|
498
|
-
let removed = 0;
|
|
499
|
-
// Every workflow ships as a skill only. Slash commands duplicated each skill
|
|
500
|
-
// under a second name in the agent's listing, so every location an earlier
|
|
501
|
-
// version wrote them to is swept.
|
|
502
|
-
const legacyPaths = [
|
|
503
|
-
path.join(projectPath, '.agents', 'commands', 'tospec'),
|
|
504
|
-
path.join(projectPath, '.claude', 'commands', 'tospec'),
|
|
505
|
-
path.join(projectPath, '.claude', 'commands', 'tosx'),
|
|
506
|
-
// Cursor support was removed; this stays so stale .cursor command files
|
|
507
|
-
// are still swept away.
|
|
508
|
-
...ALL_WORKFLOWS.map((workflow) => path.join(projectPath, '.cursor', 'commands', `tospec-${workflow}.md`)),
|
|
509
|
-
];
|
|
510
|
-
for (const legacyPath of legacyPaths) {
|
|
511
|
-
if (fs.existsSync(legacyPath)) {
|
|
512
|
-
await fs.promises.rm(legacyPath, { recursive: true, force: true });
|
|
513
|
-
removed++;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return removed;
|
|
517
|
-
}
|
|
518
601
|
}
|
|
519
602
|
//# sourceMappingURL=init.js.map
|