universal-dev-standards 6.8.0 → 6.10.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/bin/uds.js +12 -2
- package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
- package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
- package/bundled/ai/standards/code-review.ai.yaml +13 -3
- package/bundled/ai/standards/commit-message.ai.yaml +8 -4
- package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
- package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
- package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
- package/bundled/ai/standards/logging.ai.yaml +2 -2
- package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
- package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
- package/bundled/ai/standards/security-standards.ai.yaml +2 -2
- package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
- package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
- package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
- package/bundled/core/acceptance-criteria-traceability.md +5 -2
- package/bundled/core/adr-standards.md +26 -2
- package/bundled/core/agent-communication-protocol.md +8 -0
- package/bundled/core/branch-completion.md +8 -0
- package/bundled/core/change-batching-standards.md +8 -0
- package/bundled/core/code-review-checklist.md +5 -2
- package/bundled/core/context-aware-loading.md +1 -1
- package/bundled/core/deferred-item-exit.md +254 -0
- package/bundled/core/execution-history.md +8 -0
- package/bundled/core/feature-discovery-standards.md +5 -1
- package/bundled/core/governance-layer.md +114 -2
- package/bundled/core/pipeline-integration-standards.md +8 -0
- package/bundled/core/retrospective-standards.md +4 -2
- package/bundled/core/reverse-engineering-standards.md +81 -2
- package/bundled/core/spec-driven-development.md +8 -2
- package/bundled/core/tech-debt-standards.md +67 -8
- package/bundled/core/turn-completion-integrity.md +196 -0
- package/bundled/core/workflow-enforcement.md +8 -0
- package/bundled/core/workflow-state-protocol.md +8 -0
- package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
- package/bundled/hooks/check-logging-standard.mjs +59 -0
- package/bundled/hooks/check-turn-completion.mjs +233 -0
- package/bundled/hooks/inject-standards.mjs +183 -0
- package/bundled/hooks/telemetry-wrapper.mjs +77 -0
- package/bundled/hooks/turn-completion/detect.mjs +99 -0
- package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
- package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
- package/bundled/hooks/validate-commit-msg.mjs +104 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +73 -3
- package/bundled/locales/zh-CN/CLAUDE.md +1 -1
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
- package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
- package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-CN/core/execution-history.md +7 -0
- package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
- package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
- package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +8 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +25 -15
- package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
- package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +74 -3
- package/bundled/locales/zh-TW/CLAUDE.md +1 -1
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
- package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
- package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
- package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
- package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
- package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
- package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
- package/bundled/locales/zh-TW/core/data-contract.md +2 -2
- package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
- package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
- package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
- package/bundled/locales/zh-TW/core/execution-history.md +7 -0
- package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
- package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
- package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
- package/bundled/locales/zh-TW/core/incident-response.md +2 -2
- package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
- package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
- package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
- package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
- package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
- package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
- package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
- package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
- package/bundled/locales/zh-TW/core/runbook.md +2 -2
- package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
- package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
- package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
- package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
- package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
- package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
- package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
- package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +8 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +25 -15
- package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
- package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
- package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
- package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
- package/bundled/skills/brainstorm-assistant/guide.md +9 -6
- package/bundled/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/skills/commands/brainstorm.md +12 -9
- package/bundled/skills/commands/config.md +0 -1
- package/bundled/skills/commands/init.md +2 -3
- package/bundled/skills/commit-standards/SKILL.md +2 -0
- package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/skills/dev-methodology/SKILL.md +4 -0
- package/bundled/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/skills/project-discovery/SKILL.md +1 -0
- package/bundled/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/skills/release-standards/SKILL.md +3 -0
- package/bundled/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/templates/.ai-context.yaml.template +194 -0
- package/bundled/templates/CLAUDE.md.template +145 -0
- package/bundled/templates/DESIGN.md +237 -0
- package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
- package/bundled/templates/SKILL-CANDIDATES.md +39 -0
- package/bundled/templates/gates/check-error-exit.mjs +309 -0
- package/bundled/templates/mcp-config.json +10 -0
- package/bundled/templates/methodology-template.yaml +209 -0
- package/bundled/templates/migration-template.md +408 -0
- package/bundled/templates/requirement-checklist.md +410 -0
- package/bundled/templates/requirement-document-template.md +591 -0
- package/bundled/templates/requirement-template.md +881 -0
- package/bundled/templates/reverse-spec-template.md +409 -0
- package/bundled/templates/test-case-template.md +74 -0
- package/bundled/templates/test-plan-template.md +74 -0
- package/package.json +12 -9
- package/src/commands/audit.js +82 -0
- package/src/commands/check.js +227 -34
- package/src/commands/config.js +19 -19
- package/src/commands/init.js +174 -20
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +537 -43
- package/src/compilers/claude-code-compiler.js +4 -1
- package/src/config/ai-agent-paths.js +62 -17
- package/src/core/constants.js +42 -11
- package/src/core/manifest.js +201 -3
- package/src/core/paths.js +2 -2
- package/src/i18n/messages.js +42 -29
- package/src/installers/hooks-installer.js +167 -75
- package/src/installers/integration-installer.js +13 -9
- package/src/installers/skills-installer.js +4 -4
- package/src/installers/standards-installer.js +3 -3
- package/src/prompts/init.js +47 -18
- package/src/reconciler/diff-engine.js +57 -2
- package/src/reconciler/plan-executor.js +31 -11
- package/src/utils/detector.js +21 -1
- package/src/utils/effect-boundary.js +1093 -0
- package/src/utils/hasher.js +166 -1
- package/src/utils/hook-stats.js +1 -1
- package/src/utils/integration-generator.js +288 -82
- package/src/utils/reference-sync.js +107 -8
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/src/utils/yaml-generator.js +51 -9
- package/standards-registry.json +32 -9
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
3
3
|
import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquirer/prompts';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
6
|
-
import { join, basename } from 'path';
|
|
7
|
-
import { readManifest, writeManifest, copyStandard, isInitialized } from '../utils/copier.js';
|
|
8
|
-
import { getRepositoryInfo, getAllStandards, getStandardSource } from '../utils/registry.js';
|
|
9
|
-
import { computeFileHash,
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join, basename, dirname, relative } from 'path';
|
|
7
|
+
import { readManifest, writeManifest, copyStandard, isInitialized, getRepoRoot } from '../utils/copier.js';
|
|
8
|
+
import { getRepositoryInfo, getAllStandards, getShippableFilenames, getStandardSource } from '../utils/registry.js';
|
|
9
|
+
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes } from '../utils/hasher.js';
|
|
10
10
|
import {
|
|
11
11
|
writeIntegrationFile,
|
|
12
12
|
getToolFilePath,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
getCommandsDirForAgent
|
|
43
43
|
} from '../config/ai-agent-paths.js';
|
|
44
44
|
import { getMarketplaceSkillsInfo } from '../utils/github.js';
|
|
45
|
+
import { detectAITools } from '../utils/detector.js';
|
|
45
46
|
import {
|
|
46
47
|
promptSkillsInstallLocation,
|
|
47
48
|
promptCommandsInstallation
|
|
@@ -55,8 +56,111 @@ import {
|
|
|
55
56
|
} from '../reconciler/index.js';
|
|
56
57
|
import { restoreSingleFile } from './check.js';
|
|
57
58
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
58
|
-
import { resolveIntegrationFile } from '../core/constants.js';
|
|
59
|
-
import {
|
|
59
|
+
import { resolveIntegrationFile, SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
60
|
+
import {
|
|
61
|
+
mergeInstalledNames,
|
|
62
|
+
recordFileProvenance,
|
|
63
|
+
forgetFileProvenance,
|
|
64
|
+
establishProvenance,
|
|
65
|
+
isProvenanceEstablished
|
|
66
|
+
} from '../core/manifest.js';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How aggressive `uds update` is about deleting standards it no longer ships.
|
|
70
|
+
*
|
|
71
|
+
* XSPEC-384 §4 leaves the staging strategy for R2 undecided, so this is the
|
|
72
|
+
* most conservative of the three options on the table and it lives in one named
|
|
73
|
+
* place rather than being spread through the deletion path. Changing this
|
|
74
|
+
* constant is the whole change of default; the alternatives are:
|
|
75
|
+
*
|
|
76
|
+
* 'report-only' list what would go, delete nothing without --prune (current)
|
|
77
|
+
* 'ask' list, and prompt in interactive sessions only
|
|
78
|
+
* 'auto' delete as part of a normal update
|
|
79
|
+
*
|
|
80
|
+
* It starts at 'report-only' because the change is adopter-visible in the
|
|
81
|
+
* dangerous direction: files that survive today start disappearing. A default
|
|
82
|
+
* that deletes must not arrive in a patch release, and must not arrive at all
|
|
83
|
+
* before someone decides it should.
|
|
84
|
+
*/
|
|
85
|
+
export const PRUNE_POLICY = 'report-only';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decide whether this run is allowed to delete.
|
|
89
|
+
*
|
|
90
|
+
* `--prune` is deliberately its own flag rather than a use of `--yes`. `--yes`
|
|
91
|
+
* means "don't ask me about the thing I already asked for"; it is set in CI and
|
|
92
|
+
* in scripts, and letting it also authorise deletion would enable the
|
|
93
|
+
* destructive path silently in exactly the unattended environments that cannot
|
|
94
|
+
* notice it. Non-interactive runs therefore never delete unless --prune is
|
|
95
|
+
* explicit. (XSPEC-384 R3)
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} plan - Result of planStandardsRemovals
|
|
98
|
+
* @param {Object} options - Command options
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
export function shouldPrune(plan, options = {}) {
|
|
102
|
+
if (plan.remove.length === 0) return false;
|
|
103
|
+
if (options.prune) return true;
|
|
104
|
+
return PRUNE_POLICY === 'auto';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Print the removal plan, including its denominator. (XSPEC-384 R3)
|
|
109
|
+
*
|
|
110
|
+
* The counts are not decoration. A report that lists only the files it intends
|
|
111
|
+
* to touch is bit-for-bit identical to a report from a scan that only *saw*
|
|
112
|
+
* those files — which is the failure the previous code shipped for months,
|
|
113
|
+
* silently, because nothing ever printed how many files it had considered.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} plan - Result of planStandardsRemovals
|
|
116
|
+
* @param {boolean} provenanceWasEstablished - Whether ownership was already known
|
|
117
|
+
* @param {boolean} willDelete - Whether this run is going to act on the plan
|
|
118
|
+
*/
|
|
119
|
+
export function reportRemovalPlan(plan, provenanceWasEstablished, willDelete = false) {
|
|
120
|
+
if (plan.scanned === 0) return;
|
|
121
|
+
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(chalk.gray(
|
|
124
|
+
` .standards/ examined: ${plan.scanned} file(s) — ` +
|
|
125
|
+
`${plan.census.udsOwned} written by UDS, ` +
|
|
126
|
+
`${plan.census.foreign} not ours, ` +
|
|
127
|
+
`${plan.census.unknown} ownership unknown, ` +
|
|
128
|
+
`${plan.excluded.length} excluded`
|
|
129
|
+
));
|
|
130
|
+
for (const { path: p, reason } of plan.excluded) {
|
|
131
|
+
console.log(chalk.gray(` excluded ${p} (${reason})`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!provenanceWasEstablished) {
|
|
135
|
+
console.log(chalk.gray(
|
|
136
|
+
' first run with ownership tracking: files installed before this ' +
|
|
137
|
+
'release cannot be attributed yet, so none will be removed.'
|
|
138
|
+
));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (plan.remove.length === 0) {
|
|
142
|
+
if (plan.census.foreign > 0 || plan.census.unknown > 0) {
|
|
143
|
+
console.log(chalk.gray(
|
|
144
|
+
` kept ${plan.census.foreign + plan.census.unknown} file(s) UDS did not write`
|
|
145
|
+
));
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.yellow(
|
|
152
|
+
` ${plan.remove.length} file(s) are no longer shipped by UDS` +
|
|
153
|
+
(willDelete ? ' and will be removed:' : ':')
|
|
154
|
+
));
|
|
155
|
+
for (const { path: p, reason } of plan.remove) {
|
|
156
|
+
console.log(chalk.yellow(` - ${p} (${reason})`));
|
|
157
|
+
}
|
|
158
|
+
console.log(chalk.gray(
|
|
159
|
+
willDelete
|
|
160
|
+
? ' Removing now (--prune). Files UDS did not write are never touched.'
|
|
161
|
+
: ' Not deleted. Re-run with --prune to remove them.'
|
|
162
|
+
));
|
|
163
|
+
}
|
|
60
164
|
|
|
61
165
|
/**
|
|
62
166
|
* Determine the correct target directory for a standard file.
|
|
@@ -170,7 +274,7 @@ function buildInstallCommand(pm, tag) {
|
|
|
170
274
|
*/
|
|
171
275
|
async function updateCliAndExit(useBeta = false) {
|
|
172
276
|
const msg = t().commands.update;
|
|
173
|
-
const spinner =
|
|
277
|
+
const spinner = createSpinner(msg.updatingCli).start();
|
|
174
278
|
|
|
175
279
|
try {
|
|
176
280
|
// Command is hardcoded - no user input, safe from injection
|
|
@@ -477,6 +581,23 @@ export async function updateCommand(options) {
|
|
|
477
581
|
if (versionComparison > 0) {
|
|
478
582
|
console.log(chalk.gray(` ${msg.newerVersion.replace('{version}', currentVersion)}`));
|
|
479
583
|
}
|
|
584
|
+
|
|
585
|
+
// Still forget records of files that are gone and no longer shipped. An
|
|
586
|
+
// adopter who followed MIGRATION-v6 §2 is on the current version by
|
|
587
|
+
// definition — telling them to run `uds update --prune` and then returning
|
|
588
|
+
// before anything is examined is how the complaint became unclearable.
|
|
589
|
+
const retiredOnLatest = pruneRetiredHashes(manifest, projectPath);
|
|
590
|
+
if (retiredOnLatest.length > 0) {
|
|
591
|
+
console.log();
|
|
592
|
+
console.log(chalk.gray(
|
|
593
|
+
` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredOnLatest.length)}`
|
|
594
|
+
));
|
|
595
|
+
for (const path of retiredOnLatest) {
|
|
596
|
+
console.log(chalk.gray(` - ${path}`));
|
|
597
|
+
}
|
|
598
|
+
writeManifest(manifest, projectPath);
|
|
599
|
+
}
|
|
600
|
+
|
|
480
601
|
console.log();
|
|
481
602
|
return;
|
|
482
603
|
}
|
|
@@ -538,7 +659,7 @@ export async function updateCommand(options) {
|
|
|
538
659
|
|
|
539
660
|
// Perform update
|
|
540
661
|
console.log();
|
|
541
|
-
const spinner =
|
|
662
|
+
const spinner = createSpinner(msg.updatingStandards).start();
|
|
542
663
|
|
|
543
664
|
const results = {
|
|
544
665
|
updated: [],
|
|
@@ -546,6 +667,21 @@ export async function updateCommand(options) {
|
|
|
546
667
|
errors: []
|
|
547
668
|
};
|
|
548
669
|
|
|
670
|
+
// Paths UDS actually wrote this run, taken from the copy result rather than
|
|
671
|
+
// recomputed from the source name. Recomputing is how `fileHashes` ended up
|
|
672
|
+
// with five `.standards/options/<name>.ai.yaml` entries for files that live
|
|
673
|
+
// at `.standards/options/<category>/<name>.ai.yaml`; a provenance record
|
|
674
|
+
// built the same way would claim files that do not exist and disown ones
|
|
675
|
+
// that do. (XSPEC-384 R1)
|
|
676
|
+
const writtenPaths = [];
|
|
677
|
+
const recordWritten = (result) => {
|
|
678
|
+
if (!result?.success || !result.data) return;
|
|
679
|
+
const abs = typeof result.data === 'string' ? result.data : null;
|
|
680
|
+
if (!abs) return;
|
|
681
|
+
const rel = relative(projectPath, abs).replace(/\\/g, '/');
|
|
682
|
+
if (rel && !rel.startsWith('..')) writtenPaths.push(rel);
|
|
683
|
+
};
|
|
684
|
+
|
|
549
685
|
// Update standards
|
|
550
686
|
const updateFormat = manifest.format || 'ai';
|
|
551
687
|
const allStdsUpdate = getAllStandards();
|
|
@@ -561,6 +697,7 @@ export async function updateCommand(options) {
|
|
|
561
697
|
const result = await copyStandard(sourcePath, getStandardTargetDir(sourcePath), projectPath);
|
|
562
698
|
if (result.success) {
|
|
563
699
|
results.updated.push(std);
|
|
700
|
+
recordWritten(result);
|
|
564
701
|
} else {
|
|
565
702
|
results.errors.push(`${std}: ${result.error}`);
|
|
566
703
|
}
|
|
@@ -572,6 +709,7 @@ export async function updateCommand(options) {
|
|
|
572
709
|
const result = await copyStandard(ext, '.standards', projectPath);
|
|
573
710
|
if (result.success) {
|
|
574
711
|
results.updated.push(ext);
|
|
712
|
+
recordWritten(result);
|
|
575
713
|
} else {
|
|
576
714
|
results.errors.push(`${ext}: ${result.error}`);
|
|
577
715
|
}
|
|
@@ -593,7 +731,7 @@ export async function updateCommand(options) {
|
|
|
593
731
|
}
|
|
594
732
|
|
|
595
733
|
if (shouldInstallNew) {
|
|
596
|
-
const newSpinner =
|
|
734
|
+
const newSpinner = createSpinner(msg.installingNewStandards).start();
|
|
597
735
|
let newCount = 0;
|
|
598
736
|
|
|
599
737
|
for (const ns of newStandards) {
|
|
@@ -601,6 +739,7 @@ export async function updateCommand(options) {
|
|
|
601
739
|
if (result.success) {
|
|
602
740
|
manifest.standards.push(ns.source);
|
|
603
741
|
results.updated.push(ns.source);
|
|
742
|
+
recordWritten(result);
|
|
604
743
|
newCount++;
|
|
605
744
|
} else {
|
|
606
745
|
results.errors.push(`${ns.source}: ${result.error}`);
|
|
@@ -613,7 +752,7 @@ export async function updateCommand(options) {
|
|
|
613
752
|
|
|
614
753
|
// Update integrations (unless --standards-only)
|
|
615
754
|
if (!options.standardsOnly && manifest.integrations && manifest.integrations.length > 0) {
|
|
616
|
-
const intSpinner =
|
|
755
|
+
const intSpinner = createSpinner(msg.syncingIntegrations).start();
|
|
617
756
|
|
|
618
757
|
// Build installed standards list
|
|
619
758
|
// Raw, not basename()d. Resolution needs the registry (an ID is not a
|
|
@@ -691,6 +830,7 @@ export async function updateCommand(options) {
|
|
|
691
830
|
installedAt: new Date().toISOString()
|
|
692
831
|
};
|
|
693
832
|
}
|
|
833
|
+
refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
|
|
694
834
|
}
|
|
695
835
|
}
|
|
696
836
|
|
|
@@ -751,6 +891,20 @@ export async function updateCommand(options) {
|
|
|
751
891
|
manifest.fileHashes = {};
|
|
752
892
|
}
|
|
753
893
|
|
|
894
|
+
// The set of `.standards/` files the *current* registry says should exist.
|
|
895
|
+
//
|
|
896
|
+
// Built by accumulating inside the loops that already resolve each entry,
|
|
897
|
+
// rather than by a second function that re-derives the same paths. A second
|
|
898
|
+
// derivation is a second copy, and the two would answer "where does this
|
|
899
|
+
// standard live" differently the moment either is edited — which is the
|
|
900
|
+
// failure mode XSPEC-384 is about, not a shape to reproduce. (XSPEC-384 R2)
|
|
901
|
+
const desiredStandardsFiles = new Set();
|
|
902
|
+
|
|
903
|
+
// Standards whose ID no longer resolves against the registry. Collected here
|
|
904
|
+
// instead of being silently `continue`d, because "skipped" and "deleted
|
|
905
|
+
// upstream" were previously indistinguishable — issue #165. (XSPEC-384 R2)
|
|
906
|
+
const unresolvedStandardIds = [];
|
|
907
|
+
|
|
754
908
|
// Update hashes for standards (handles both ID format and legacy path format)
|
|
755
909
|
const hashFormat = manifest.format || 'ai';
|
|
756
910
|
const allStdsHash = getAllStandards();
|
|
@@ -759,7 +913,13 @@ export async function updateCommand(options) {
|
|
|
759
913
|
let resolvedPath = std;
|
|
760
914
|
if (!std.includes('/') && !std.includes('.')) {
|
|
761
915
|
const entry = allStdsHash.find(r => r.id === std);
|
|
762
|
-
if (!entry)
|
|
916
|
+
if (!entry) {
|
|
917
|
+
// Unrecognized ID: either a standard removed from the registry, or a
|
|
918
|
+
// stale AI tool name an old CLI wrote into `standards`. Either way it
|
|
919
|
+
// is no longer a thing we ship, so it must not enter the desired set.
|
|
920
|
+
unresolvedStandardIds.push(std);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
763
923
|
const src = getStandardSource(entry, hashFormat);
|
|
764
924
|
if (src) resolvedPath = src;
|
|
765
925
|
}
|
|
@@ -767,6 +927,10 @@ export async function updateCommand(options) {
|
|
|
767
927
|
const relativePath = (resolvedPath.includes('options/')
|
|
768
928
|
? join('.standards', 'options', fileName)
|
|
769
929
|
: join('.standards', fileName)).replace(/\\/g, '/');
|
|
930
|
+
// Added before the hash check: a file that exists but failed to hash is
|
|
931
|
+
// still wanted. Nominating it for deletion because hashing failed would
|
|
932
|
+
// turn a transient read error into data loss.
|
|
933
|
+
desiredStandardsFiles.add(relativePath);
|
|
770
934
|
const fullPath = join(projectPath, relativePath);
|
|
771
935
|
const hashInfo = computeFileHash(fullPath);
|
|
772
936
|
if (hashInfo) {
|
|
@@ -779,6 +943,7 @@ export async function updateCommand(options) {
|
|
|
779
943
|
if (typeof ext !== 'string') continue;
|
|
780
944
|
const fileName = basename(ext);
|
|
781
945
|
const relativePath = join('.standards', fileName).replace(/\\/g, '/');
|
|
946
|
+
desiredStandardsFiles.add(relativePath);
|
|
782
947
|
const fullPath = join(projectPath, relativePath);
|
|
783
948
|
const hashInfo = computeFileHash(fullPath);
|
|
784
949
|
if (hashInfo) {
|
|
@@ -795,21 +960,79 @@ export async function updateCommand(options) {
|
|
|
795
960
|
}
|
|
796
961
|
}
|
|
797
962
|
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
963
|
+
// Record what UDS wrote this run, then decide what (if anything) may go.
|
|
964
|
+
// (XSPEC-384 R1/R2/R3)
|
|
965
|
+
const provenanceWasEstablished = isProvenanceEstablished(manifest);
|
|
966
|
+
let provenanceCarrier = manifest;
|
|
967
|
+
for (const written of [...writtenPaths, ...results.integrations]) {
|
|
968
|
+
provenanceCarrier = recordFileProvenance(provenanceCarrier, written, 'update');
|
|
969
|
+
}
|
|
970
|
+
manifest.provenance = provenanceCarrier.provenance;
|
|
971
|
+
|
|
972
|
+
// Drop `standards` entries the registry no longer knows about. Previously
|
|
973
|
+
// these were `continue`d in three separate loops and left in the manifest
|
|
974
|
+
// forever, so `upstream.version` advanced to "latest" while the manifest
|
|
975
|
+
// still listed standards that had not existed for releases. (issue #165)
|
|
976
|
+
if (unresolvedStandardIds.length > 0) {
|
|
977
|
+
manifest.standards = manifest.standards.filter(s => !unresolvedStandardIds.includes(s));
|
|
978
|
+
console.log();
|
|
979
|
+
console.log(chalk.yellow(
|
|
980
|
+
`⚠ ${unresolvedStandardIds.length} manifest entr${unresolvedStandardIds.length === 1 ? 'y is' : 'ies are'} no longer in the registry and ${unresolvedStandardIds.length === 1 ? 'was' : 'were'} dropped:`
|
|
981
|
+
));
|
|
982
|
+
for (const id of unresolvedStandardIds) {
|
|
983
|
+
console.log(chalk.gray(` - ${id}`));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const removalPlan = planStandardsRemovals(projectPath, manifest, desiredStandardsFiles);
|
|
988
|
+
// Decide before reporting, so the report states what this run will actually
|
|
989
|
+
// do. Reporting first meant printing "Not deleted. Re-run with --prune" on a
|
|
990
|
+
// run that was about to delete — a message contradicting the very action it
|
|
991
|
+
// preceded, which is worse than no message.
|
|
992
|
+
const mayPrune = shouldPrune(removalPlan, options);
|
|
993
|
+
reportRemovalPlan(removalPlan, provenanceWasEstablished, mayPrune);
|
|
994
|
+
|
|
995
|
+
if (mayPrune && removalPlan.remove.length > 0) {
|
|
996
|
+
for (const { path: orphan } of removalPlan.remove) {
|
|
804
997
|
try {
|
|
805
998
|
unlinkSync(join(projectPath, orphan));
|
|
806
999
|
results.updated.push(`removed: ${orphan}`);
|
|
1000
|
+
delete manifest.fileHashes[orphan];
|
|
1001
|
+
// Deleting the file is how we give up the claim on that path, so a
|
|
1002
|
+
// file the user later creates under the same name is not inherited.
|
|
1003
|
+
manifest.provenance = forgetFileProvenance(manifest, orphan).provenance;
|
|
807
1004
|
} catch {
|
|
808
1005
|
// Ignore removal errors
|
|
809
1006
|
}
|
|
810
1007
|
}
|
|
811
1008
|
}
|
|
812
1009
|
|
|
1010
|
+
// Records of files that are already gone and that UDS no longer ships. Not
|
|
1011
|
+
// part of the removal plan above: that plan walks what is on disk, and these
|
|
1012
|
+
// are precisely the ones that are not. Reported rather than done quietly,
|
|
1013
|
+
// because a manifest shrinking without explanation is the shape this whole
|
|
1014
|
+
// release is about.
|
|
1015
|
+
const retiredHashes = pruneRetiredHashes(manifest, projectPath);
|
|
1016
|
+
if (retiredHashes.length > 0) {
|
|
1017
|
+
console.log();
|
|
1018
|
+
console.log(chalk.gray(
|
|
1019
|
+
` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredHashes.length)}`
|
|
1020
|
+
));
|
|
1021
|
+
for (const path of retiredHashes) {
|
|
1022
|
+
console.log(chalk.gray(` - ${path}`));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Provenance is only a complete account of what we own once a run has
|
|
1027
|
+
// written the whole installed set. Until then every unrecorded file is
|
|
1028
|
+
// `unknown` rather than `foreign`, and nothing gets deleted on the strength
|
|
1029
|
+
// of an absent record. Establishing it here — after the writes, not before —
|
|
1030
|
+
// is what makes the first upgrade conservative by construction rather than
|
|
1031
|
+
// by a flag someone has to remember. (XSPEC-384 §4)
|
|
1032
|
+
if (results.errors.length === 0) {
|
|
1033
|
+
manifest.provenance = establishProvenance(manifest).provenance;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
813
1036
|
// Migrate display_language: if manifest options is missing display_language,
|
|
814
1037
|
// attempt to backfill from ~/.udsrc ui.language so the project honours the
|
|
815
1038
|
// language the user already configured globally.
|
|
@@ -921,7 +1144,7 @@ export async function updateCommand(options) {
|
|
|
921
1144
|
}
|
|
922
1145
|
|
|
923
1146
|
if (shouldRestore) {
|
|
924
|
-
const restoreSpinner =
|
|
1147
|
+
const restoreSpinner = createSpinner((msg.restoringMissing || 'Restoring missing files...')).start();
|
|
925
1148
|
const checkMsg = t().commands.check;
|
|
926
1149
|
let restoredCount = 0;
|
|
927
1150
|
|
|
@@ -988,7 +1211,7 @@ export async function updateCommand(options) {
|
|
|
988
1211
|
|
|
989
1212
|
// Install Skills if user agreed
|
|
990
1213
|
if (installSkills.length > 0) {
|
|
991
|
-
const skillSpinner =
|
|
1214
|
+
const skillSpinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
|
|
992
1215
|
const skillsLocale = resolveLocale(manifest, projectPath, options);
|
|
993
1216
|
const skillResult = await installSkillsToMultipleAgents(installSkills, null, projectPath, skillsLocale);
|
|
994
1217
|
|
|
@@ -1036,7 +1259,7 @@ export async function updateCommand(options) {
|
|
|
1036
1259
|
|
|
1037
1260
|
// Update outdated Skills if user agreed
|
|
1038
1261
|
if (updateSkills.length > 0) {
|
|
1039
|
-
const updateSpinner =
|
|
1262
|
+
const updateSpinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
|
|
1040
1263
|
const updateLocale = resolveLocale(manifest, projectPath, options);
|
|
1041
1264
|
const updateResult = await installSkillsToMultipleAgents(updateSkills, null, projectPath, updateLocale);
|
|
1042
1265
|
|
|
@@ -1074,7 +1297,7 @@ export async function updateCommand(options) {
|
|
|
1074
1297
|
|
|
1075
1298
|
// Install Commands if user agreed
|
|
1076
1299
|
if (installCommands.length > 0) {
|
|
1077
|
-
const cmdSpinner =
|
|
1300
|
+
const cmdSpinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
|
|
1078
1301
|
const cmdLocale = resolveLocale(manifest, projectPath, options);
|
|
1079
1302
|
const cmdResult = await installCommandsToMultipleAgents(installCommands, null, projectPath, cmdLocale);
|
|
1080
1303
|
|
|
@@ -1105,7 +1328,7 @@ export async function updateCommand(options) {
|
|
|
1105
1328
|
|
|
1106
1329
|
// Update outdated Commands if user agreed
|
|
1107
1330
|
if (updateCommands.length > 0) {
|
|
1108
|
-
const updateCmdSpinner =
|
|
1331
|
+
const updateCmdSpinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
|
|
1109
1332
|
const updateCmdLocale = resolveLocale(manifest, projectPath, options);
|
|
1110
1333
|
const updateCmdResult = await installCommandsToMultipleAgents(updateCommands, null, projectPath, updateCmdLocale);
|
|
1111
1334
|
|
|
@@ -1181,7 +1404,7 @@ export async function updateCommand(options) {
|
|
|
1181
1404
|
let hasChanges = false;
|
|
1182
1405
|
|
|
1183
1406
|
if (missingSkills.length > 0) {
|
|
1184
|
-
const spinner =
|
|
1407
|
+
const spinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
|
|
1185
1408
|
const result = await installSkillsToMultipleAgents(missingSkills, null, projectPath, skillsLocale);
|
|
1186
1409
|
if (!manifest.skills) manifest.skills = {};
|
|
1187
1410
|
manifest.skills.installed = true;
|
|
@@ -1202,7 +1425,7 @@ export async function updateCommand(options) {
|
|
|
1202
1425
|
}
|
|
1203
1426
|
|
|
1204
1427
|
if (outdatedSkills.length > 0) {
|
|
1205
|
-
const spinner =
|
|
1428
|
+
const spinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
|
|
1206
1429
|
const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
|
|
1207
1430
|
if (!manifest.skills) manifest.skills = {};
|
|
1208
1431
|
manifest.skills.version = repoInfo.skills.version;
|
|
@@ -1226,7 +1449,7 @@ export async function updateCommand(options) {
|
|
|
1226
1449
|
}
|
|
1227
1450
|
|
|
1228
1451
|
if (missingCommands.length > 0) {
|
|
1229
|
-
const spinner =
|
|
1452
|
+
const spinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
|
|
1230
1453
|
const result = await installCommandsToMultipleAgents(missingCommands, null, projectPath, skillsLocale);
|
|
1231
1454
|
if (!manifest.commands) manifest.commands = {};
|
|
1232
1455
|
manifest.commands.installed = true;
|
|
@@ -1246,7 +1469,7 @@ export async function updateCommand(options) {
|
|
|
1246
1469
|
}
|
|
1247
1470
|
|
|
1248
1471
|
if (outdatedCommands.length > 0) {
|
|
1249
|
-
const spinner =
|
|
1472
|
+
const spinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
|
|
1250
1473
|
const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
|
|
1251
1474
|
if (!manifest.commands) manifest.commands = {};
|
|
1252
1475
|
manifest.commands.version = repoInfo.skills.version;
|
|
@@ -1366,11 +1589,58 @@ export async function updateCommand(options) {
|
|
|
1366
1589
|
|
|
1367
1590
|
console.log();
|
|
1368
1591
|
|
|
1592
|
+
// 錯誤訊息單一出口閘門:問過才寫,而且從不覆寫。
|
|
1593
|
+
await offerErrorExitGate(projectPath, options);
|
|
1594
|
+
|
|
1369
1595
|
// Exit explicitly to prevent hanging.
|
|
1370
1596
|
// T11: a partial update exits non-zero so CI / scripts can detect the failure.
|
|
1371
1597
|
process.exit(updateIncomplete ? 1 : 0);
|
|
1372
1598
|
}
|
|
1373
1599
|
|
|
1600
|
+
/**
|
|
1601
|
+
* 提供錯誤訊息單一出口閘門——**顯示它是什麼、問過、才寫,而且絕不覆寫既有檔案。**
|
|
1602
|
+
*
|
|
1603
|
+
* 🔴 為什麼不自動寫:`uds` 是別人裝進自己 repo 的工具。未經同意往陌生人的專案
|
|
1604
|
+
* 寫一支可執行腳本,比它要修的那個問題更糟——那會變成信任問題,而不是 bug。
|
|
1605
|
+
* 採用者說不要,我們就不寫,而且下次 `uds check` 只會再提一次,不會偷偷放進去。
|
|
1606
|
+
*
|
|
1607
|
+
* ⚠️ 已存在就直接跳過,連問都不問:那個檔案裡的 CONFIG 是採用者自己填的,
|
|
1608
|
+
* 我們沒有立場拿範本蓋掉它。要更新內容是另一件事,不在這裡做。
|
|
1609
|
+
*/
|
|
1610
|
+
async function offerErrorExitGate(projectPath, options) {
|
|
1611
|
+
const dest = join(projectPath, 'scripts', 'check-error-exit.mjs');
|
|
1612
|
+
if (existsSync(dest)) return; // 他們自己的檔案
|
|
1613
|
+
if (!existsSync(join(projectPath, 'src'))) return; // 沒有原始碼就沒有這個問題
|
|
1614
|
+
|
|
1615
|
+
const src = join(getRepoRoot(), 'templates', 'gates', 'check-error-exit.mjs');
|
|
1616
|
+
if (!existsSync(src)) return; // 範本沒出貨——沉默,不要假裝提供了
|
|
1617
|
+
|
|
1618
|
+
console.log(chalk.bold(' 錯誤訊息單一出口閘門'));
|
|
1619
|
+
console.log(chalk.gray(' 防的是:每個呼叫端各自把錯誤回應拼成給人看的字串。'));
|
|
1620
|
+
console.log(chalk.gray(' 第一處是實作,第二處開始就會各寫各的,而畫面上只剩一句 Bad Request。'));
|
|
1621
|
+
console.log(chalk.gray(` 要寫入:scripts/check-error-exit.mjs(純 Node、零相依、${Math.round(readFileSync(src, 'utf-8').length / 1024)}KB)`));
|
|
1622
|
+
console.log(chalk.gray(' 寫入後它會 exit 2 直到你填好 CONFIG——那是刻意的,'));
|
|
1623
|
+
console.log(chalk.gray(' 一支在新 repo 裡靜靜回綠的閘門跟一支沒裝的無從分辨。'));
|
|
1624
|
+
console.log();
|
|
1625
|
+
|
|
1626
|
+
const ok = await confirmOrFail({
|
|
1627
|
+
message: '要把這道閘門寫進 scripts/check-error-exit.mjs 嗎?',
|
|
1628
|
+
defaultValue: false, // 🔴 預設不寫。沉默不是同意。
|
|
1629
|
+
options
|
|
1630
|
+
});
|
|
1631
|
+
if (!ok) {
|
|
1632
|
+
console.log(chalk.gray(' 跳過。`uds check` 之後會再提醒,但不會自己寫進去。'));
|
|
1633
|
+
console.log();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1638
|
+
writeFileSync(dest, readFileSync(src, 'utf-8'), 'utf-8');
|
|
1639
|
+
console.log(chalk.green(' ✓ 已寫入 scripts/check-error-exit.mjs'));
|
|
1640
|
+
console.log(chalk.gray(' 下一步:填好檔頭的 CONFIG,然後把它接進你的 CI 或 pre-commit。'));
|
|
1641
|
+
console.log();
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1374
1644
|
/**
|
|
1375
1645
|
* Regenerate integration files for all configured AI tools
|
|
1376
1646
|
* Reusable core logic that can be called from both updateIntegrationsOnly and configureCommand
|
|
@@ -1452,6 +1722,7 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1452
1722
|
installedAt: now
|
|
1453
1723
|
};
|
|
1454
1724
|
}
|
|
1725
|
+
refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
|
|
1455
1726
|
}
|
|
1456
1727
|
}
|
|
1457
1728
|
|
|
@@ -1575,7 +1846,7 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1575
1846
|
return;
|
|
1576
1847
|
}
|
|
1577
1848
|
|
|
1578
|
-
const spinner =
|
|
1849
|
+
const spinner = createSpinner(msg.regeneratingIntegrations).start();
|
|
1579
1850
|
|
|
1580
1851
|
// Use reusable regeneration function
|
|
1581
1852
|
const results = regenerateIntegrations(projectPath, manifest);
|
|
@@ -1604,6 +1875,188 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1604
1875
|
process.exit(0);
|
|
1605
1876
|
}
|
|
1606
1877
|
|
|
1878
|
+
/**
|
|
1879
|
+
* Rebuild `manifest.integrationConfigs` from what the manifest already records.
|
|
1880
|
+
*
|
|
1881
|
+
* 🔴 Only `uds init`'s interactive flow and `--sync-refs` itself ever wrote this
|
|
1882
|
+
* key; no update or reconcile path backfills it, so a project created with
|
|
1883
|
+
* `uds init -y` carries `{}` from the first day (reproduced on a clean 6.9.0
|
|
1884
|
+
* project, 2026-09-16). `--sync-refs` then refuses to run — and `uds check` was
|
|
1885
|
+
* recommending exactly that command. Everything the config holds is derivable
|
|
1886
|
+
* from the manifest, so derive it instead of demanding the user re-init.
|
|
1887
|
+
*
|
|
1888
|
+
* Existing entries are never overwritten: they may hold choices (categories,
|
|
1889
|
+
* content mode) that this derivation cannot know.
|
|
1890
|
+
*
|
|
1891
|
+
* @param {Object} manifest - Manifest object (mutated)
|
|
1892
|
+
* @returns {number} How many entries were added
|
|
1893
|
+
*/
|
|
1894
|
+
export function backfillIntegrationConfigs(manifest) {
|
|
1895
|
+
if (!manifest.integrationConfigs) manifest.integrationConfigs = {};
|
|
1896
|
+
|
|
1897
|
+
const tools = manifest.aiTools?.length ? manifest.aiTools : [];
|
|
1898
|
+
const byFile = new Map();
|
|
1899
|
+
for (const tool of tools) {
|
|
1900
|
+
const file = resolveIntegrationFile(tool) || getToolFilePath(tool);
|
|
1901
|
+
if (file && !byFile.has(file)) byFile.set(file, tool);
|
|
1902
|
+
}
|
|
1903
|
+
// Integrations can be recorded as file names (`CLAUDE.md`) or tool keys.
|
|
1904
|
+
for (const entry of manifest.integrations || []) {
|
|
1905
|
+
const file = resolveIntegrationFile(entry) || (String(entry).includes('.') ? String(entry) : null);
|
|
1906
|
+
if (!file) continue;
|
|
1907
|
+
if (!byFile.has(file)) {
|
|
1908
|
+
const tool = getToolFromPath(file) || resolveToolKeyFromEntry(entry);
|
|
1909
|
+
if (tool) byFile.set(file, tool);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
let added = 0;
|
|
1914
|
+
for (const [file, tool] of byFile) {
|
|
1915
|
+
if (manifest.integrationConfigs[file]) continue;
|
|
1916
|
+
manifest.integrationConfigs[file] = {
|
|
1917
|
+
...buildToolIntegrationConfig(manifest, tool),
|
|
1918
|
+
format: manifest.format || 'ai',
|
|
1919
|
+
backfilledAt: new Date().toISOString()
|
|
1920
|
+
};
|
|
1921
|
+
added++;
|
|
1922
|
+
}
|
|
1923
|
+
return added;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function resolveToolKeyFromEntry(entry) {
|
|
1927
|
+
const key = String(entry).replace(/\.[^.]+$/, '').toLowerCase();
|
|
1928
|
+
return SUPPORTED_AI_TOOLS?.[key] ? key : null;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* If the manifest already tracks this file, make its record describe what is on
|
|
1933
|
+
* disk now.
|
|
1934
|
+
*
|
|
1935
|
+
* Every path that writes AGENTS.md recorded `integrationBlockHashes` and none
|
|
1936
|
+
* recorded `fileHashes`, so a project whose manifest tracked the file (an older
|
|
1937
|
+
* install, or one where AGENTS.md was written as a tool's integration file) got
|
|
1938
|
+
* `⚠ AGENTS.md (modified)` from `uds check` the moment UDS itself regenerated
|
|
1939
|
+
* it. UDS wrote the file and did not record what it wrote.
|
|
1940
|
+
*
|
|
1941
|
+
* Conditional on purpose. `uds init` does not enrol AGENTS.md in `fileHashes`,
|
|
1942
|
+
* and adopters are expected to extend that summary — a path that started
|
|
1943
|
+
* tracking it would convert every legitimate edit into a reported fault. This
|
|
1944
|
+
* only ever refreshes a record that already exists.
|
|
1945
|
+
*
|
|
1946
|
+
* @param {Object} manifest - Project manifest (mutated)
|
|
1947
|
+
* @param {string} projectPath - Project root
|
|
1948
|
+
* @param {string} relativePath - Path as UDS reports it
|
|
1949
|
+
* @returns {boolean} Whether a record was refreshed
|
|
1950
|
+
*/
|
|
1951
|
+
export function refreshTrackedFileHash(manifest, projectPath, relativePath) {
|
|
1952
|
+
if (!manifest?.fileHashes || !relativePath) return false;
|
|
1953
|
+
const normalized = String(relativePath).replace(/\\/g, '/');
|
|
1954
|
+
if (!(normalized in manifest.fileHashes)) return false;
|
|
1955
|
+
|
|
1956
|
+
const hashInfo = computeFileHash(join(projectPath, normalized));
|
|
1957
|
+
if (!hashInfo) return false;
|
|
1958
|
+
|
|
1959
|
+
manifest.fileHashes[normalized] = { ...hashInfo, installedAt: new Date().toISOString() };
|
|
1960
|
+
return true;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Forget `fileHashes` records for files that are gone AND that UDS no longer
|
|
1965
|
+
* ships.
|
|
1966
|
+
*
|
|
1967
|
+
* `--prune` deletes files it finds while walking `.standards/`. A file the
|
|
1968
|
+
* adopter already removed by hand is not in that walk, so its record survived
|
|
1969
|
+
* every prune — and the record is what made `uds check` report the completed
|
|
1970
|
+
* migration as seven faults. Following MIGRATION-v6 §2 therefore produced a
|
|
1971
|
+
* complaint that no command could clear.
|
|
1972
|
+
*
|
|
1973
|
+
* This is not a deletion. The file is already absent and no registry entry can
|
|
1974
|
+
* ever resolve to it again, so the record cannot refer to anything. Both facts
|
|
1975
|
+
* are required: a missing file UDS still ships is a real fault and stays; a
|
|
1976
|
+
* descoped standard still on disk is `--prune`'s business, gated on ownership,
|
|
1977
|
+
* and is left alone here.
|
|
1978
|
+
*
|
|
1979
|
+
* @param {Object} manifest - Project manifest (mutated)
|
|
1980
|
+
* @param {string} projectPath - Project root
|
|
1981
|
+
* @param {{shippable?: Set<string>}} [deps] - Injection point for tests
|
|
1982
|
+
* @returns {string[]} Paths whose records were dropped, sorted
|
|
1983
|
+
*/
|
|
1984
|
+
export function pruneRetiredHashes(manifest, projectPath, deps = {}) {
|
|
1985
|
+
if (!manifest?.fileHashes) return [];
|
|
1986
|
+
|
|
1987
|
+
let shippable;
|
|
1988
|
+
try {
|
|
1989
|
+
shippable = deps.shippable || getShippableFilenames();
|
|
1990
|
+
} catch {
|
|
1991
|
+
return [];
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// A shippable set this small means the registry did not load, not that UDS
|
|
1995
|
+
// stopped shipping everything. Without this arm a broken install would empty
|
|
1996
|
+
// the manifest and every downstream check would go quiet at the same time.
|
|
1997
|
+
if (!shippable || shippable.size < 50) return [];
|
|
1998
|
+
|
|
1999
|
+
const dropped = [];
|
|
2000
|
+
for (const recorded of Object.keys(manifest.fileHashes)) {
|
|
2001
|
+
const normalized = recorded.replace(/\\/g, '/');
|
|
2002
|
+
if (!normalized.startsWith('.standards/')) continue;
|
|
2003
|
+
const fileName = normalized.split('/').pop();
|
|
2004
|
+
if (shippable.has(fileName)) continue;
|
|
2005
|
+
if (existsSync(join(projectPath, recorded))) continue;
|
|
2006
|
+
delete manifest.fileHashes[recorded];
|
|
2007
|
+
dropped.push(recorded);
|
|
2008
|
+
}
|
|
2009
|
+
return dropped.sort();
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Build the config `--sync-refs` regenerates an integration file from.
|
|
2014
|
+
*
|
|
2015
|
+
* The stored `integrationConfigs[file]` is a snapshot taken when the file was
|
|
2016
|
+
* installed. Its `categories` are the thing sync-refs exists to update, and its
|
|
2017
|
+
* `installedStandards` is the thing that must never be trusted: it is not
|
|
2018
|
+
* refreshed by any path, so on a project upgraded across majors it still lists
|
|
2019
|
+
* standards that stopped shipping, and whatever it lists becomes the file's
|
|
2020
|
+
* content. That is how a manifest holding 73 standards produced a CLAUDE.md
|
|
2021
|
+
* announcing 76, which `uds check` then flagged against the same manifest.
|
|
2022
|
+
*
|
|
2023
|
+
* So: manifest-derived fields win, the caller's freshly computed categories win
|
|
2024
|
+
* over both, and anything the manifest knows nothing about (timestamps, fields
|
|
2025
|
+
* a future version adds) is carried through untouched.
|
|
2026
|
+
*
|
|
2027
|
+
* `outputLanguage` is the one place the stored value still gets a say — a
|
|
2028
|
+
* manifest with no `output_language` should not silently reset a project that
|
|
2029
|
+
* chose one before the option was recorded there.
|
|
2030
|
+
*
|
|
2031
|
+
* @param {Object} manifest - Project manifest
|
|
2032
|
+
* @param {string} toolName - Tool key (e.g. 'claude-code')
|
|
2033
|
+
* @param {Object} storedConfig - The existing integrationConfigs entry
|
|
2034
|
+
* @param {string[]} expectedCategories - Categories computed from current standards
|
|
2035
|
+
* @returns {Object} Config for writeIntegrationFile
|
|
2036
|
+
*/
|
|
2037
|
+
export function refreshIntegrationConfig(manifest, toolName, storedConfig = {}, expectedCategories = []) {
|
|
2038
|
+
const outputLanguage =
|
|
2039
|
+
manifest.options?.output_language
|
|
2040
|
+
|| manifest.options?.commit_language
|
|
2041
|
+
|| storedConfig.outputLanguage
|
|
2042
|
+
|| storedConfig.commitLanguage
|
|
2043
|
+
|| 'english';
|
|
2044
|
+
|
|
2045
|
+
const fresh = buildToolIntegrationConfig(
|
|
2046
|
+
{ ...manifest, options: { ...(manifest.options || {}), output_language: outputLanguage } },
|
|
2047
|
+
toolName
|
|
2048
|
+
);
|
|
2049
|
+
|
|
2050
|
+
return {
|
|
2051
|
+
...storedConfig,
|
|
2052
|
+
...fresh,
|
|
2053
|
+
tool: toolName,
|
|
2054
|
+
categories: expectedCategories,
|
|
2055
|
+
outputLanguage,
|
|
2056
|
+
format: manifest.format || storedConfig.format || 'ai'
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1607
2060
|
/**
|
|
1608
2061
|
* Sync integration file references based on manifest standards
|
|
1609
2062
|
* @param {string} projectPath - Project path
|
|
@@ -1615,6 +2068,16 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1615
2068
|
console.log(chalk.cyan(msg.syncingRefs));
|
|
1616
2069
|
console.log();
|
|
1617
2070
|
|
|
2071
|
+
// Rebuild the config when it is absent rather than sending the user away —
|
|
2072
|
+
// the manifest holds everything it is derived from.
|
|
2073
|
+
if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
|
|
2074
|
+
const filled = backfillIntegrationConfigs(manifest);
|
|
2075
|
+
if (filled > 0) {
|
|
2076
|
+
console.log(chalk.gray(` ${(msg.integrationConfigsBackfilled || 'Rebuilt integration config for {count} file(s) from the manifest.').replace('{count}', filled)}`));
|
|
2077
|
+
console.log();
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1618
2081
|
// Check if integrationConfigs exists
|
|
1619
2082
|
if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
|
|
1620
2083
|
console.log(chalk.yellow(msg.noIntegrationConfigs));
|
|
@@ -1667,14 +2130,9 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1667
2130
|
continue;
|
|
1668
2131
|
}
|
|
1669
2132
|
|
|
1670
|
-
// Regenerate the integration file with updated categories
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
tool: toolName,
|
|
1674
|
-
categories: expectedCategories,
|
|
1675
|
-
// Pass output_language for dynamic commit standards generation
|
|
1676
|
-
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || config.outputLanguage || config.commitLanguage || 'english'
|
|
1677
|
-
};
|
|
2133
|
+
// Regenerate the integration file with updated categories, from the
|
|
2134
|
+
// manifest rather than from the snapshot stored beside it.
|
|
2135
|
+
const newConfig = refreshIntegrationConfig(manifest, toolName, config, expectedCategories);
|
|
1678
2136
|
|
|
1679
2137
|
if (plan) {
|
|
1680
2138
|
console.log(chalk.yellow(` ~ ${integrationPath}: categories would change to ${expectedCategories.join(', ') || '(none)'} (dry run — nothing is written)`));
|
|
@@ -1845,7 +2303,35 @@ async function updateSkillsOnly(projectPath, manifest, options) {
|
|
|
1845
2303
|
// Convert legacy format to new format
|
|
1846
2304
|
const legacyLocation = manifest.skills.location;
|
|
1847
2305
|
if (legacyLocation === 'user' || legacyLocation === 'project') {
|
|
1848
|
-
|
|
2306
|
+
// 🔴 A legacy manifest records a LOCATION, never a tool — and this used to
|
|
2307
|
+
// fill the gap with `claude-code`, unconditionally. For everyone who ran the
|
|
2308
|
+
// old `uds init -y` in a codex or opencode repo, that guess is wrong and
|
|
2309
|
+
// self-perpetuating: the fix to `init.js` would never reach them, because
|
|
2310
|
+
// update would keep re-installing to `.claude/skills/` on the strength of a
|
|
2311
|
+
// tool name nobody ever recorded. (XSPEC-408 §18.3 — Codex does not read
|
|
2312
|
+
// that directory; measured, two arms, 2026-09-08.)
|
|
2313
|
+
//
|
|
2314
|
+
// So re-derive from what is actually detected in the repo, and keep
|
|
2315
|
+
// `claude-code` only as the fallback for when nothing is detected — which is
|
|
2316
|
+
// exactly the case the old line was written for.
|
|
2317
|
+
let detected = {};
|
|
2318
|
+
try {
|
|
2319
|
+
detected = detectAITools(projectPath) || {};
|
|
2320
|
+
} catch {
|
|
2321
|
+
// Detection failing puts us in the "we do not know" case, which is exactly
|
|
2322
|
+
// what the old unconditional `claude-code` line was for. Never let a probe
|
|
2323
|
+
// for extra information break the update that would have worked without it.
|
|
2324
|
+
detected = {};
|
|
2325
|
+
}
|
|
2326
|
+
const detectedSkillsTools = Object.entries(detected)
|
|
2327
|
+
.filter(([, present]) => present)
|
|
2328
|
+
.map(([key]) => (key === 'claudeCode' ? 'claude-code' : key === 'geminiCli' ? 'gemini-cli' : key))
|
|
2329
|
+
.filter(agent => {
|
|
2330
|
+
const config = getAgentConfig(agent);
|
|
2331
|
+
return Boolean(config?.supportsSkills && config?.skills);
|
|
2332
|
+
});
|
|
2333
|
+
const agents = detectedSkillsTools.length > 0 ? detectedSkillsTools : ['claude-code'];
|
|
2334
|
+
for (const agent of agents) skillsInstallations.push({ agent, level: legacyLocation });
|
|
1849
2335
|
} else if (legacyLocation === 'marketplace') {
|
|
1850
2336
|
console.log(chalk.yellow(msg.skillsViaMarketplace || 'Skills installed via Marketplace'));
|
|
1851
2337
|
console.log(chalk.gray(` ${msg.updateViaMarketplace || 'Update through Plugin Marketplace'}`));
|
|
@@ -1895,7 +2381,7 @@ async function updateSkillsOnly(projectPath, manifest, options) {
|
|
|
1895
2381
|
return;
|
|
1896
2382
|
}
|
|
1897
2383
|
|
|
1898
|
-
const spinner =
|
|
2384
|
+
const spinner = createSpinner(msg.installingSkills || 'Installing Skills...').start();
|
|
1899
2385
|
|
|
1900
2386
|
const skillsLocaleForUpdate = resolveLocale(manifest, projectPath, options);
|
|
1901
2387
|
const result = await installSkillsToMultipleAgents(
|
|
@@ -1913,8 +2399,16 @@ async function updateSkillsOnly(projectPath, manifest, options) {
|
|
|
1913
2399
|
}).join(', ');
|
|
1914
2400
|
|
|
1915
2401
|
if (result.totalErrors === 0) {
|
|
2402
|
+
// 🔴 `{count}` is worded "AI tools" in every locale, and this passed
|
|
2403
|
+
// `totalInstalled` — the number of skill FILES written, summed across targets.
|
|
2404
|
+
// A two-tool update of 55 skills printed "Updated Skills for 165 AI tools".
|
|
2405
|
+
// It was invisible while this branch only ran from the interactive flow; the
|
|
2406
|
+
// fix to `init.js` makes it the common path, so it stops being invisible.
|
|
1916
2407
|
spinner.succeed((msg.skillsUpdated || 'Updated Skills in {count} locations: {locations}')
|
|
1917
|
-
|
|
2408
|
+
// Counted from the same list that builds `locations`, so the number and the
|
|
2409
|
+
// names can never disagree — and it does not depend on the shape of the
|
|
2410
|
+
// installer's return value.
|
|
2411
|
+
.replace('{count}', fileBasedInstallations.length)
|
|
1918
2412
|
.replace('{locations}', locations));
|
|
1919
2413
|
} else {
|
|
1920
2414
|
spinner.warn((msg.skillsUpdatedWithErrors || 'Updated Skills with {errors} errors')
|
|
@@ -2011,7 +2505,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
|
|
|
2011
2505
|
}
|
|
2012
2506
|
console.log();
|
|
2013
2507
|
|
|
2014
|
-
const spinner =
|
|
2508
|
+
const spinner = createSpinner(msg.installingCommands || 'Installing commands...').start();
|
|
2015
2509
|
|
|
2016
2510
|
const commandsLocale = resolveLocale(manifest, projectPath, options);
|
|
2017
2511
|
const result = await installCommandsToMultipleAgents(
|
|
@@ -2508,7 +3002,7 @@ async function handleRollback(projectPath) {
|
|
|
2508
3002
|
* Handle --plan: show what the reconciler would do without executing.
|
|
2509
3003
|
*/
|
|
2510
3004
|
async function handlePlan(projectPath, options) {
|
|
2511
|
-
const spinner =
|
|
3005
|
+
const spinner = createSpinner('Calculating reconciliation plan...').start();
|
|
2512
3006
|
|
|
2513
3007
|
const result = await reconcilerPlan(projectPath, { force: false });
|
|
2514
3008
|
|
|
@@ -2583,7 +3077,7 @@ async function handleReconcile(projectPath, options, { force }) {
|
|
|
2583
3077
|
}
|
|
2584
3078
|
|
|
2585
3079
|
// Execute
|
|
2586
|
-
const spinner =
|
|
3080
|
+
const spinner = createSpinner('Applying reconciliation plan...').start();
|
|
2587
3081
|
|
|
2588
3082
|
const result = await reconcile(projectPath, {
|
|
2589
3083
|
force,
|