universal-dev-standards 6.14.0-beta.2 → 6.14.0-beta.4
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/bundled/ai/standards/ai-response-navigation.ai.yaml +43 -3
- package/bundled/ai/standards/checkin-standards.ai.yaml +25 -6
- package/bundled/ai/standards/open-work-tracking.ai.yaml +4 -1
- package/bundled/ai/standards/pipeline-security-gates.ai.yaml +5 -1
- package/bundled/core/ai-response-navigation.md +128 -12
- package/bundled/core/open-work-tracking.md +1 -1
- package/bundled/extensions/frameworks/fat-free-patterns.md +937 -0
- package/bundled/extensions/languages/csharp-style.md +464 -0
- package/bundled/extensions/languages/php/fat-free-patterns.md +915 -0
- package/bundled/extensions/languages/php/php-style.md +693 -0
- package/bundled/extensions/languages/php-style.md +700 -0
- package/bundled/extensions/locales/zh-cn.md +717 -0
- package/bundled/extensions/locales/zh-tw.md +717 -0
- package/bundled/locales/COVERAGE.md +5 -4
- package/bundled/locales/zh-CN/CHANGELOG.md +44 -3
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-CN/skills/README.md +1 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +44 -3
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-TW/core/open-work-tracking.md +3 -3
- package/bundled/locales/zh-TW/skills/README.md +1 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/skills/README.md +1 -0
- package/bundled/skills/comprehension-ladder/SKILL.md +283 -0
- package/bundled/skills/comprehension-ladder/eval-cases.md +255 -0
- package/package.json +2 -2
- package/src/commands/check.js +9 -0
- package/src/commands/init.js +100 -27
- package/src/commands/uninstall.js +144 -30
- package/src/commands/update.js +62 -3
- package/src/core/install-records.js +191 -0
- package/src/i18n/messages.js +39 -6
- package/src/installers/hooks-installer.js +61 -30
- package/src/installers/integration-installer.js +5 -1
- package/src/installers/standards-installer.js +16 -23
- package/src/reconciler/plan-executor.js +10 -11
- package/src/uninstallers/hook-uninstaller.js +219 -33
- package/src/uninstallers/integration-uninstaller.js +35 -5
- package/src/utils/copier.js +57 -0
- package/src/utils/git-hooks.js +139 -7
- package/src/utils/hasher.js +36 -0
- package/src/utils/integration-generator.js +16 -6
- package/src/utils/legacy-hook-migration.js +112 -0
- package/src/utils/locale.js +19 -0
- package/src/utils/open-work-tracking.mjs +124 -23
- package/standards-registry.json +21 -7
package/src/utils/git-hooks.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* adopters (asiaostrich-telemetry-server, asiaostrich-telemetry-client,
|
|
12
12
|
* machine-setup): all three have `.husky/pre-commit` calling `npx uds check`,
|
|
13
13
|
* none has `core.hooksPath` set, and the hook has never once run.
|
|
14
|
+
* (That `npx uds check` form was itself replaced — see buildPreCommitBlock.)
|
|
14
15
|
*
|
|
15
16
|
* Fix: set `core.hooksPath` directly ourselves. This is exactly what husky's
|
|
16
17
|
* own `index.js` does internally (verified against husky ^9.1.7's source:
|
|
@@ -94,7 +95,7 @@ export function wireGitHooksPath(projectPath, targetRelDir) {
|
|
|
94
95
|
return {
|
|
95
96
|
wired: false,
|
|
96
97
|
reason: `git core.hooksPath is already set to "${configured}"`,
|
|
97
|
-
hint: `UDS will not override an existing core.hooksPath. Confirm the hook script there also runs \`
|
|
98
|
+
hint: `UDS will not override an existing core.hooksPath. Confirm the hook script there also runs \`universal-dev-standards check\` (the installed CLI — not the short name "uds", which on the npm registry is an unrelated package), or switch it yourself: git config --local core.hooksPath ${targetRelDir}`
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -107,7 +108,7 @@ export function wireGitHooksPath(projectPath, targetRelDir) {
|
|
|
107
108
|
return {
|
|
108
109
|
wired: false,
|
|
109
110
|
reason: '.git/hooks/pre-commit already exists',
|
|
110
|
-
hint: 'UDS will not overwrite or bypass an existing .git/hooks/pre-commit. Add "
|
|
111
|
+
hint: 'UDS will not overwrite or bypass an existing .git/hooks/pre-commit. Add a "universal-dev-standards check" line to it manually (it needs the UDS CLI installed in this project or on PATH), or remove it and re-run `uds init`.'
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -126,10 +127,133 @@ export function wireGitHooksPath(projectPath, targetRelDir) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// What UDS writes into `.husky/pre-commit`.
|
|
132
|
+
//
|
|
133
|
+
// 🔴 This used to be the single line `npx uds check`. `npx` resolves a bare
|
|
134
|
+
// command name from node_modules/.bin and PATH first, and only when neither has
|
|
135
|
+
// it does it go to the npm registry — and the registry package named `uds` is
|
|
136
|
+
// NOT this project (it is an unrelated package by another maintainer, no
|
|
137
|
+
// `bin`, last published 2022). So a clone with no UDS installed had a commit
|
|
138
|
+
// hook that asked npm for a stranger's package by name. It failed today only
|
|
139
|
+
// because that package has no executable; the day its owner publishes one with a
|
|
140
|
+
// `uds` bin, every adopter's every commit runs the stranger's code.
|
|
141
|
+
//
|
|
142
|
+
// Measured 2026-09-30 against a local fake registry that logs each request
|
|
143
|
+
// (npm 10.9.9, 11.20.0, 12.1.0 — identical): `npx --no-install uds` STILL fetches
|
|
144
|
+
// the package metadata for `uds` (GET /uds), so `--no-install` is not a fix; and
|
|
145
|
+
// `npx --no-install --package=universal-dev-standards uds` does not find a
|
|
146
|
+
// GLOBAL install at all (it goes to the registry for our own name and fails).
|
|
147
|
+
// Neither is usable, so the hook does not involve npm at all: it looks in this
|
|
148
|
+
// project's node_modules/.bin, then on PATH, and it looks for the bin named
|
|
149
|
+
// after the full package name — `universal-dev-standards` is a name only this
|
|
150
|
+
// project can publish, `uds` is a name anyone's package can also claim.
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/** First line of the block UDS writes (also the marker older UDS versions wrote). */
|
|
154
|
+
export const UDS_HOOK_MARKER = '# UDS Standard Check';
|
|
155
|
+
/** Last line of the block — lets uninstall remove the whole block, not guess line by line. */
|
|
156
|
+
export const UDS_HOOK_END_MARKER = '# End UDS Standard Check';
|
|
157
|
+
/** The bin every UDS release has shipped since the rename; named after the package on purpose. */
|
|
158
|
+
export const UDS_BIN_NAME = 'universal-dev-standards';
|
|
159
|
+
/** A hook line that runs the UDS check — the old short form or the current full-name form. */
|
|
160
|
+
export const UDS_CHECK_COMMAND_RE = /\b(?:uds|universal-dev-standards)\s+check\b/;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Any place a hook script runs the bare name `uds` through a package runner
|
|
164
|
+
* (npx, bunx, pnpm dlx, yarn dlx, npm exec ...). Broader than the two exact lines UDS
|
|
165
|
+
* ever wrote (see legacy-hook-migration.js): this finds lines to TELL the
|
|
166
|
+
* adopter about, not lines UDS may change.
|
|
167
|
+
*/
|
|
168
|
+
export const BARE_UDS_RUNNER_RE = /\b(?:npx|bunx|pnpx|pnpm\s+dlx|yarn\s+dlx|npm\s+exec|npm\s+x)\s+(?:-\S+\s+)*uds\b/;
|
|
169
|
+
|
|
170
|
+
/** Does this hook script still run the bare name `uds` through a package runner? */
|
|
171
|
+
export function hasBareUdsRunner(content) {
|
|
172
|
+
return typeof content === 'string' && content.split('\n').some((l) => BARE_UDS_RUNNER_RE.test(l));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Does hook script `content` run the UDS check, in either the old or the current form? */
|
|
176
|
+
export function hookRunsUdsCheck(content) {
|
|
177
|
+
return typeof content === 'string' && UDS_CHECK_COMMAND_RE.test(content);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The block `uds init` appends to `.husky/pre-commit` (and `uds update` swaps in
|
|
182
|
+
* for the old one-liner). POSIX sh; runs in git-bash on Windows.
|
|
183
|
+
*
|
|
184
|
+
* - The subshell keeps the PATH change from leaking into the adopter's own
|
|
185
|
+
* commands in the same hook file.
|
|
186
|
+
* - `|| exit $?` makes both outcomes stop the commit even when the adopter has
|
|
187
|
+
* put more commands after this block. Without it the "not installed" branch
|
|
188
|
+
* would be a silent skip of the check, which is what this must never be.
|
|
189
|
+
* - Not installed: say what is missing and how to fix it, exit non-zero. It
|
|
190
|
+
* neither skips the check nor downloads anything.
|
|
191
|
+
*
|
|
192
|
+
* @param {{args?: string, hookFile?: string}} [opts]
|
|
193
|
+
* args — extra arguments to keep on the check command
|
|
194
|
+
* (e.g. `--standard checkin-standards`, which older UDS versions wrote).
|
|
195
|
+
* hookFile — the file the block is written into, named in the "blocked" message so
|
|
196
|
+
* it points at the file the adopter actually has (default `.husky/pre-commit`;
|
|
197
|
+
* the native hook passes `.git/hooks/pre-commit`).
|
|
198
|
+
* @returns {string} the block, LF line endings, ending with a newline
|
|
199
|
+
*/
|
|
200
|
+
export function buildPreCommitBlock({ args = '', hookFile = '.husky/pre-commit' } = {}) {
|
|
201
|
+
const cmd = args ? `${UDS_BIN_NAME} check ${args}` : `${UDS_BIN_NAME} check`;
|
|
202
|
+
return [
|
|
203
|
+
UDS_HOOK_MARKER,
|
|
204
|
+
'# Runs the UDS CLI installed in this project (node_modules/.bin) or on PATH.',
|
|
205
|
+
'# It never asks npm to fetch anything: the short name "uds" on the npm registry',
|
|
206
|
+
'# belongs to an unrelated package, and npx would download and run it.',
|
|
207
|
+
'(',
|
|
208
|
+
' PATH="$PWD/node_modules/.bin:$PATH"',
|
|
209
|
+
` if command -v ${UDS_BIN_NAME} >/dev/null 2>&1; then`,
|
|
210
|
+
` ${cmd}`,
|
|
211
|
+
' exit $?',
|
|
212
|
+
' fi',
|
|
213
|
+
` echo "[UDS] Pre-commit check cannot run: the UDS CLI (${UDS_BIN_NAME}) is not installed." >&2`,
|
|
214
|
+
` echo "[UDS] Install it: npm install --save-dev ${UDS_BIN_NAME} (or: npm install -g ${UDS_BIN_NAME})" >&2`,
|
|
215
|
+
` echo "[UDS] This commit is blocked until it is installed, or until you remove this block from ${hookFile}." >&2`,
|
|
216
|
+
' exit 1',
|
|
217
|
+
') || exit $?',
|
|
218
|
+
UDS_HOOK_END_MARKER,
|
|
219
|
+
''
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove the block buildPreCommitBlock() writes — from the marker line through
|
|
225
|
+
* the end-marker line, inclusive — and nothing else. A block whose end marker is
|
|
226
|
+
* gone (the adopter cut into it) is left alone: what remains of it is no longer
|
|
227
|
+
* provably UDS's, and the caller's line-level fallback deals with the lines it can name.
|
|
228
|
+
* @returns {{content: string, removed: boolean}}
|
|
229
|
+
*/
|
|
230
|
+
export function stripUdsHookBlock(content) {
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
const out = [];
|
|
233
|
+
let removed = false;
|
|
234
|
+
for (let i = 0; i < lines.length; i++) {
|
|
235
|
+
if (lines[i].trim() === UDS_HOOK_MARKER) {
|
|
236
|
+
let end = -1;
|
|
237
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
238
|
+
if (lines[j].trim() === UDS_HOOK_END_MARKER) { end = j; break; }
|
|
239
|
+
// Another marker before an end marker means this block is not well-formed.
|
|
240
|
+
if (lines[j].trim() === UDS_HOOK_MARKER) break;
|
|
241
|
+
}
|
|
242
|
+
if (end !== -1) {
|
|
243
|
+
i = end;
|
|
244
|
+
removed = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
out.push(lines[i]);
|
|
249
|
+
}
|
|
250
|
+
return { content: out.join('\n'), removed };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Does `filePath` contain a hook line UDS manages (the old one-liner or the current block)? */
|
|
130
254
|
function hasUdsMarker(filePath) {
|
|
131
255
|
try {
|
|
132
|
-
return readFileSync(filePath, 'utf-8')
|
|
256
|
+
return hookRunsUdsCheck(readFileSync(filePath, 'utf-8'));
|
|
133
257
|
} catch {
|
|
134
258
|
return false;
|
|
135
259
|
}
|
|
@@ -225,7 +349,7 @@ export function stripLegacyHuskyShLine(content) {
|
|
|
225
349
|
* @param {string} projectPath
|
|
226
350
|
* @returns {{relevant: boolean, wired?: boolean, hookFile?: string,
|
|
227
351
|
* configuredHooksPath?: string|null, effectiveHooksDir?: string|null,
|
|
228
|
-
* legacyV8?: boolean, missingShebang?: boolean}}
|
|
352
|
+
* legacyV8?: boolean, missingShebang?: boolean, legacyBareUds?: boolean}}
|
|
229
353
|
* `relevant: false` means there is nothing UDS-managed to report on (no
|
|
230
354
|
* hook file, or hooks-dir could not be determined — never guess "unwired"
|
|
231
355
|
* from a failed lookup).
|
|
@@ -252,6 +376,13 @@ export function checkPreCommitHookWiring(projectPath) {
|
|
|
252
376
|
try { return !hasShebang(readFileSync(managedPath, 'utf-8')); } catch { return false; }
|
|
253
377
|
})();
|
|
254
378
|
|
|
379
|
+
// Independent of wiring, like missingShebang: the file can be wired and still ask
|
|
380
|
+
// npm for the bare name `uds` (see buildPreCommitBlock). Only the husky file is
|
|
381
|
+
// examined — that is the one UDS wrote the old one-liner into.
|
|
382
|
+
const legacyBareUds = hasHuskyHook && (() => {
|
|
383
|
+
try { return hasBareUdsRunner(readFileSync(huskyHookPath, 'utf-8')); } catch { return false; }
|
|
384
|
+
})();
|
|
385
|
+
|
|
255
386
|
const effectiveFile = join(effectiveHooksDir, 'pre-commit');
|
|
256
387
|
let wired = false;
|
|
257
388
|
if (existsSync(effectiveFile)) {
|
|
@@ -264,7 +395,7 @@ export function checkPreCommitHookWiring(projectPath) {
|
|
|
264
395
|
}
|
|
265
396
|
}
|
|
266
397
|
|
|
267
|
-
if (wired) return { relevant: true, wired: true, hookFile, missingShebang };
|
|
398
|
+
if (wired) return { relevant: true, wired: true, hookFile, missingShebang, legacyBareUds };
|
|
268
399
|
|
|
269
400
|
const legacyV8 = hasHuskyHook && (() => {
|
|
270
401
|
try { return hasLegacyHuskyShLine(readFileSync(huskyHookPath, 'utf-8')); } catch { return false; }
|
|
@@ -277,6 +408,7 @@ export function checkPreCommitHookWiring(projectPath) {
|
|
|
277
408
|
configuredHooksPath: getLocalHooksPathConfig(projectPath),
|
|
278
409
|
effectiveHooksDir,
|
|
279
410
|
legacyV8,
|
|
280
|
-
missingShebang
|
|
411
|
+
missingShebang,
|
|
412
|
+
legacyBareUds
|
|
281
413
|
};
|
|
282
414
|
}
|
package/src/utils/hasher.js
CHANGED
|
@@ -532,6 +532,42 @@ export function computeIntegrationBlockHash(filePath) {
|
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Hash everything in an integration file that is OUTSIDE the UDS marker block.
|
|
537
|
+
*
|
|
538
|
+
* `computeIntegrationBlockHash` answers "did anyone edit what UDS manages?".
|
|
539
|
+
* This answers the opposite question, which `uds uninstall` needs: "is anything
|
|
540
|
+
* outside the block something other than what UDS itself wrote?". A file UDS
|
|
541
|
+
* created from nothing (AGENTS.md's `# AGENTS.md` / `> Auto-generated by ...`
|
|
542
|
+
* header and its trailing sections, CLAUDE.md's leading guidance) has plenty of
|
|
543
|
+
* text outside the markers, and that text alone cannot tell "generated by UDS"
|
|
544
|
+
* from "typed by the adopter" — only a hash recorded when UDS created the file,
|
|
545
|
+
* compared with the same hash now, can.
|
|
546
|
+
*
|
|
547
|
+
* Line endings are normalized (same reason as every other hash here) and each
|
|
548
|
+
* side is trimmed, so an editor adding a trailing newline is not "a user edit".
|
|
549
|
+
*
|
|
550
|
+
* @param {string} filePath - Absolute file path
|
|
551
|
+
* @returns {{hash: string, size: number}|null} null when the file cannot be read,
|
|
552
|
+
* has no marker block, or has an ambiguous marker pair (no proof is available)
|
|
553
|
+
*/
|
|
554
|
+
export function computeOutsideBlockHash(filePath) {
|
|
555
|
+
try {
|
|
556
|
+
const content = normalizeLineEndings(readFileSync(filePath, 'utf-8'));
|
|
557
|
+
const { before, blockContent, after } = extractBlockContent(content, detectFormat(filePath));
|
|
558
|
+
if (!blockContent) return null;
|
|
559
|
+
const outside = `${before.trim()}\n\u0000\n${after.trim()}`;
|
|
560
|
+
return {
|
|
561
|
+
hash: `sha256:${createHash('sha256').update(outside).digest('hex')}`,
|
|
562
|
+
size: Buffer.byteLength(outside, 'utf-8')
|
|
563
|
+
};
|
|
564
|
+
} catch {
|
|
565
|
+
// Includes AmbiguousMarkerError: with two marker pairs there is no single
|
|
566
|
+
// "outside", so there is nothing to prove anything with.
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
535
571
|
/**
|
|
536
572
|
* Compare integration block hash with stored hash info
|
|
537
573
|
* @param {string} filePath - Absolute file path
|
|
@@ -3202,8 +3202,14 @@ export function writeIntegrationFile(tool, config, projectPath) {
|
|
|
3202
3202
|
try {
|
|
3203
3203
|
let content = generateIntegrationContent({ ...config, tool });
|
|
3204
3204
|
|
|
3205
|
+
// Whether this call CREATES the file (as opposed to merging into one that was
|
|
3206
|
+
// already there). Only a file UDS created from nothing can later be proven
|
|
3207
|
+
// to be wholly UDS's — see core/install-records.js. Taken from the existence
|
|
3208
|
+
// check that already gates the merge below, before anything is written.
|
|
3209
|
+
const created = !existsSync(filePath);
|
|
3210
|
+
|
|
3205
3211
|
// Handle merge if file exists
|
|
3206
|
-
if (
|
|
3212
|
+
if (!created) {
|
|
3207
3213
|
const existingContent = readFileSync(filePath, 'utf-8');
|
|
3208
3214
|
|
|
3209
3215
|
if (config.mergeStrategy) {
|
|
@@ -3242,6 +3248,7 @@ export function writeIntegrationFile(tool, config, projectPath) {
|
|
|
3242
3248
|
success: true,
|
|
3243
3249
|
path: fileName, // Return relative path for consistency
|
|
3244
3250
|
absolutePath: filePath,
|
|
3251
|
+
created,
|
|
3245
3252
|
blockHashInfo // Contains: blockHash, blockSize, fullHash, fullSize
|
|
3246
3253
|
};
|
|
3247
3254
|
} catch (error) {
|
|
@@ -3584,8 +3591,8 @@ export function parseStandardsIndexCount(content) {
|
|
|
3584
3591
|
export function wrapWithMarkers(content, format) {
|
|
3585
3592
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
3586
3593
|
const warning = format === 'plaintext'
|
|
3587
|
-
? '# WARNING: This block is managed by UDS
|
|
3588
|
-
: '<!-- WARNING: This block is managed by UDS
|
|
3594
|
+
? '# WARNING: This block is managed by UDS. DO NOT manually edit. Use \'npx universal-dev-standards init\' or \'npx universal-dev-standards update\' to modify.'
|
|
3595
|
+
: '<!-- WARNING: This block is managed by UDS. DO NOT manually edit. Use \'npx universal-dev-standards init\' or \'npx universal-dev-standards update\' to modify. -->';
|
|
3589
3596
|
// 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
|
|
3590
3597
|
// 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
|
|
3591
3598
|
// 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
|
|
@@ -3949,7 +3956,7 @@ export function generateAgentsMdSummary(config = {}) {
|
|
|
3949
3956
|
blockLines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
|
|
3950
3957
|
}
|
|
3951
3958
|
} else {
|
|
3952
|
-
blockLines.push('No standards installed yet. Run `npx
|
|
3959
|
+
blockLines.push('No standards installed yet. Run `npx universal-dev-standards init` to install.');
|
|
3953
3960
|
}
|
|
3954
3961
|
|
|
3955
3962
|
lines.push(wrapWithMarkers(blockLines.join('\n'), 'markdown'));
|
|
@@ -3976,12 +3983,14 @@ export function generateAgentsMdSummary(config = {}) {
|
|
|
3976
3983
|
*/
|
|
3977
3984
|
export function writeAgentsMdSummary(config, projectPath) {
|
|
3978
3985
|
const filePath = join(projectPath, 'AGENTS.md');
|
|
3979
|
-
|
|
3980
3986
|
try {
|
|
3981
3987
|
let content = generateAgentsMdSummary({ ...config, projectPath });
|
|
3982
3988
|
|
|
3989
|
+
// See writeIntegrationFile: true only when this call creates the file.
|
|
3990
|
+
const created = !existsSync(filePath);
|
|
3991
|
+
|
|
3983
3992
|
// Handle merge if file exists (preserve user content outside markers)
|
|
3984
|
-
if (
|
|
3993
|
+
if (!created) {
|
|
3985
3994
|
const existingContent = readFileSync(filePath, 'utf-8');
|
|
3986
3995
|
const newParts = extractMarkedContent(content, 'markdown');
|
|
3987
3996
|
if (newParts.content) {
|
|
@@ -3997,6 +4006,7 @@ export function writeAgentsMdSummary(config, projectPath) {
|
|
|
3997
4006
|
success: true,
|
|
3998
4007
|
path: 'AGENTS.md',
|
|
3999
4008
|
absolutePath: filePath,
|
|
4009
|
+
created,
|
|
4000
4010
|
blockHashInfo
|
|
4001
4011
|
};
|
|
4002
4012
|
} catch (error) {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrate the pre-commit hook line older UDS versions wrote
|
|
3
|
+
* (`npx uds check`) to the block current UDS writes.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: every adopter who ran `uds init` before this fix has
|
|
6
|
+
* `.husky/pre-commit` asking `npx` to run the bare name `uds`. `npx` looks in
|
|
7
|
+
* node_modules/.bin and PATH first and otherwise asks the npm registry — where
|
|
8
|
+
* `uds` is an unrelated package that is not ours (see buildPreCommitBlock in
|
|
9
|
+
* git-hooks.js for the measurements). Fixing what `uds init` writes today does
|
|
10
|
+
* nothing for the hooks already on disk, so `uds update` swaps them.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ THIS FILE RECOGNISES the old text; it must never EMIT it. The regression
|
|
13
|
+
* guard (tests/unit/no-bare-uds-npx.test.js) lists this file as the one place
|
|
14
|
+
* a bare-name npx string is allowed, and says why. Nothing here may write one.
|
|
15
|
+
*
|
|
16
|
+
* What counts as "UDS wrote this line" — exact equality, not a pattern:
|
|
17
|
+
* only the two shapes UDS has ever generated, and only directly under the
|
|
18
|
+
* marker comment UDS put above them.
|
|
19
|
+
* 2026-02-04 .. 2026-03-04 npx uds check --standard checkin-standards
|
|
20
|
+
* 2026-03-04 .. this fix npx uds check
|
|
21
|
+
* (from `git log -S` on the writer in cli/src/commands/init.js.) A line an
|
|
22
|
+
* adopter edited, or wrote themselves, is NOT swapped: it cannot be proven to be
|
|
23
|
+
* ours, and a hook that runs the adopter's own arguments is theirs to change.
|
|
24
|
+
* It is reported, with the exact line number and what to write instead.
|
|
25
|
+
*
|
|
26
|
+
* @module utils/legacy-hook-migration
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
30
|
+
import { join } from 'path';
|
|
31
|
+
import { buildPreCommitBlock, UDS_HOOK_MARKER, BARE_UDS_RUNNER_RE, hasBareUdsRunner } from './git-hooks.js';
|
|
32
|
+
import { getFileRecord, proveUnchanged, recordFile, newRecorder, RECORD_KINDS } from '../core/install-records.js';
|
|
33
|
+
|
|
34
|
+
/** The exact command lines UDS itself has written under the marker, and the extra arguments each carried. */
|
|
35
|
+
const UDS_WRITTEN_LEGACY_LINES = new Map([
|
|
36
|
+
['npx uds check', ''],
|
|
37
|
+
['npx uds check --standard checkin-standards', '--standard checkin-standards']
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Swap the legacy hook line UDS wrote in `.husky/pre-commit` for the current block.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectPath
|
|
44
|
+
* @param {{plan?: boolean, manifest?: object|null}} [opts]
|
|
45
|
+
* plan — report what would change, write nothing
|
|
46
|
+
* manifest — read only to see whether the file was provably UDS's before the
|
|
47
|
+
* swap (so the install record can follow it); may be null
|
|
48
|
+
* @returns {{
|
|
49
|
+
* state: 'none'|'migrated'|'would-migrate'|'kept'|'error',
|
|
50
|
+
* replaced: number, // legacy UDS-written lines swapped (or that would be)
|
|
51
|
+
* kept: Array<{line: number, text: string}>, // bare-name lines UDS cannot prove it wrote
|
|
52
|
+
* recorder: object|null, // fold into the manifest when non-null
|
|
53
|
+
* error?: string
|
|
54
|
+
* }}
|
|
55
|
+
*/
|
|
56
|
+
export function migrateLegacyHuskyHook(projectPath, { plan = false, manifest = null } = {}) {
|
|
57
|
+
const rel = '.husky/pre-commit';
|
|
58
|
+
const abs = join(projectPath, '.husky', 'pre-commit');
|
|
59
|
+
const none = { state: 'none', replaced: 0, kept: [], recorder: null };
|
|
60
|
+
if (!existsSync(abs)) return none;
|
|
61
|
+
|
|
62
|
+
let content;
|
|
63
|
+
try {
|
|
64
|
+
content = readFileSync(abs, 'utf-8');
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return { ...none, state: 'error', error: e.message };
|
|
67
|
+
}
|
|
68
|
+
if (!hasBareUdsRunner(content)) return none;
|
|
69
|
+
|
|
70
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
const out = [];
|
|
73
|
+
const kept = [];
|
|
74
|
+
let replaced = 0;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const text = lines[i].replace(/\r$/, '');
|
|
78
|
+
const next = i + 1 < lines.length ? lines[i + 1].replace(/\r$/, '').trim() : null;
|
|
79
|
+
// Marker line directly followed by a line UDS is known to have written.
|
|
80
|
+
if (text.trim() === UDS_HOOK_MARKER && next !== null && UDS_WRITTEN_LEGACY_LINES.has(next)) {
|
|
81
|
+
const block = buildPreCommitBlock({ args: UDS_WRITTEN_LEGACY_LINES.get(next) });
|
|
82
|
+
// block ends with '\n'; drop that so the surrounding line join supplies it.
|
|
83
|
+
out.push(...block.replace(/\n$/, '').split('\n').map((l) => l + (eol === '\r\n' ? '\r' : '')));
|
|
84
|
+
replaced += 1;
|
|
85
|
+
i += 1; // the legacy command line is consumed
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (BARE_UDS_RUNNER_RE.test(text)) kept.push({ line: i + 1, text: text.trim() });
|
|
89
|
+
out.push(lines[i]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (replaced === 0) return { state: 'kept', replaced: 0, kept, recorder: null };
|
|
93
|
+
|
|
94
|
+
if (plan) return { state: 'would-migrate', replaced, kept, recorder: null };
|
|
95
|
+
|
|
96
|
+
// Was the whole file provably UDS's until now? Then the swap must not cost it
|
|
97
|
+
// that proof: re-record the new content, or uninstall falls back from "delete
|
|
98
|
+
// the file UDS created" to "strip lines".
|
|
99
|
+
let recorder = null;
|
|
100
|
+
const wasProven = !!manifest && !!getFileRecord(manifest, rel) && proveUnchanged(manifest, projectPath, rel).state === 'proven';
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(abs, out.join('\n'), 'utf-8');
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { state: 'error', replaced: 0, kept, recorder: null, error: e.message };
|
|
106
|
+
}
|
|
107
|
+
if (wasProven) {
|
|
108
|
+
recorder = newRecorder();
|
|
109
|
+
recordFile(recorder, projectPath, rel, RECORD_KINDS.GIT_HOOK);
|
|
110
|
+
}
|
|
111
|
+
return { state: 'migrated', replaced, kept, recorder };
|
|
112
|
+
}
|
package/src/utils/locale.js
CHANGED
|
@@ -32,6 +32,24 @@ export function displayLanguageToLocale(displayLanguage) {
|
|
|
32
32
|
return LOCALE_MAP[displayLanguage.toLowerCase()] || 'en';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Normalise a requested display language and say whether it is supported.
|
|
37
|
+
*
|
|
38
|
+
* `uds init --locale zh-CN` used to install English and exit 0: the value was compared case-sensitively
|
|
39
|
+
* further down, so the upper-case spelling silently missed every Chinese branch. An unsupported value such
|
|
40
|
+
* as `fr` did the same — English, exit 0, nothing said. The caller still falls back to English for an
|
|
41
|
+
* unsupported value, but now it can say so.
|
|
42
|
+
* @param {string|undefined|null} requested - what the user, install.yaml or the environment asked for
|
|
43
|
+
* @returns {{ displayLanguage: string|undefined|null, supported: boolean }}
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeRequestedLocale(requested) {
|
|
46
|
+
if (typeof requested !== 'string' || requested.trim() === '') {
|
|
47
|
+
return { displayLanguage: requested, supported: true };
|
|
48
|
+
}
|
|
49
|
+
const displayLanguage = requested.trim().toLowerCase();
|
|
50
|
+
return { displayLanguage, supported: Object.prototype.hasOwnProperty.call(LOCALE_MAP, displayLanguage) };
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Check if a locale requires localized skill installation.
|
|
37
55
|
* English locale uses the default source, no localization needed.
|
|
@@ -67,6 +85,7 @@ export function detectLocaleFromStandards(projectPath) {
|
|
|
67
85
|
|
|
68
86
|
export default {
|
|
69
87
|
displayLanguageToLocale,
|
|
88
|
+
normalizeRequestedLocale,
|
|
70
89
|
isLocalizedLocale,
|
|
71
90
|
detectLocaleFromStandards,
|
|
72
91
|
LOCALE_MAP
|