universal-dev-standards 6.13.0-beta.2 → 6.13.0-beta.5
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/turn-completion-integrity.ai.yaml +8 -2
- package/bundled/core/turn-completion-integrity.md +25 -4
- package/bundled/hooks/check-turn-completion.mjs +22 -6
- package/bundled/hooks/turn-completion/engine.mjs +11 -2
- package/bundled/locales/zh-CN/CHANGELOG.md +37 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/turn-completion-integrity.md +23 -8
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +33 -5
- package/bundled/locales/zh-TW/CHANGELOG.md +37 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/turn-completion-integrity.md +23 -8
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +33 -5
- package/package.json +1 -1
- package/src/commands/check.js +79 -0
- package/src/commands/init.js +152 -51
- package/src/i18n/messages.js +52 -0
- package/src/utils/git-hooks.js +282 -0
- package/standards-registry.json +7 -7
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git pre-commit hook wiring — shared by `uds init` (writer, see
|
|
3
|
+
* setupHuskyHook in ../commands/init.js) and `uds check` (read-only
|
|
4
|
+
* detector, see checkPreCommitHookWiring in ../commands/check.js).
|
|
5
|
+
*
|
|
6
|
+
* Origin: `uds init` writes `.husky/pre-commit`, but never made git actually
|
|
7
|
+
* run it. It relied on husky's own bootstrap (`npm install` triggering the
|
|
8
|
+
* `prepare` script, which husky uses to set `core.hooksPath`), which only
|
|
9
|
+
* fires the NEXT time `npm install` runs — never, if the adopter's
|
|
10
|
+
* `node_modules` already existed. Verified 2026-09-26 against three real
|
|
11
|
+
* adopters (asiaostrich-telemetry-server, asiaostrich-telemetry-client,
|
|
12
|
+
* machine-setup): all three have `.husky/pre-commit` calling `npx uds check`,
|
|
13
|
+
* none has `core.hooksPath` set, and the hook has never once run.
|
|
14
|
+
*
|
|
15
|
+
* Fix: set `core.hooksPath` directly ourselves. This is exactly what husky's
|
|
16
|
+
* own `index.js` does internally (verified against husky ^9.1.7's source:
|
|
17
|
+
* `git config core.hooksPath ${dir}/_`, then a shim under `_/` that execs the
|
|
18
|
+
* real script one directory up) — we do the equivalent for `.husky` directly
|
|
19
|
+
* so the hook is live immediately after `uds init`, independent of whether
|
|
20
|
+
* husky is installed or `npm install` ever runs again.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync } from 'fs';
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
import { join, dirname, basename, isAbsolute } from 'path';
|
|
25
|
+
|
|
26
|
+
/** Read `core.hooksPath` from LOCAL git config only (never global/system) — the
|
|
27
|
+
* scope we write to, and the only one that could conflict with what we set.
|
|
28
|
+
* @returns {string|null} the configured value, or null if unset
|
|
29
|
+
*/
|
|
30
|
+
export function getLocalHooksPathConfig(projectPath) {
|
|
31
|
+
try {
|
|
32
|
+
const out = execSync('git config --local --get core.hooksPath', {
|
|
33
|
+
cwd: projectPath,
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
36
|
+
}).trim();
|
|
37
|
+
return out || null;
|
|
38
|
+
} catch {
|
|
39
|
+
// `git config --get` exits 1 when the key is unset — that is not an error.
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The hooks directory git will ACTUALLY use right now, honoring `core.hooksPath`
|
|
46
|
+
* (local, global or system — whichever git resolves) and falling back to
|
|
47
|
+
* `.git/hooks` when unset. This is `git rev-parse --git-path hooks`, chosen
|
|
48
|
+
* over reading `core.hooksPath` ourselves because it matches what git itself
|
|
49
|
+
* resolves, including the worktree/`.git`-is-a-file case.
|
|
50
|
+
* @returns {string|null} absolute path, or null if it could not be determined
|
|
51
|
+
* (e.g. not a git repository, or git is not on PATH) — callers must treat
|
|
52
|
+
* that as "unknown", never as "unwired".
|
|
53
|
+
*/
|
|
54
|
+
export function getEffectiveHooksDir(projectPath) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execSync('git rev-parse --git-path hooks', {
|
|
57
|
+
cwd: projectPath,
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
60
|
+
}).trim();
|
|
61
|
+
if (!out) return null;
|
|
62
|
+
return isAbsolute(out) ? out : join(projectPath, out);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Point git's hook directory at `targetRelDir` (e.g. `.husky`) so a
|
|
70
|
+
* pre-commit hook UDS writes there actually runs — without depending on
|
|
71
|
+
* husky (or any npm install) ever executing.
|
|
72
|
+
*
|
|
73
|
+
* Never overrides:
|
|
74
|
+
* - an adopter's own `core.hooksPath` pointed anywhere else — we do not
|
|
75
|
+
* know what they use it for, and clobbering it could break hooks for
|
|
76
|
+
* every event, not just pre-commit.
|
|
77
|
+
* - an existing native `.git/hooks/pre-commit`, when `core.hooksPath` is
|
|
78
|
+
* unset (git's default is `.git/hooks`) — setting `core.hooksPath` in
|
|
79
|
+
* that case would silently stop git from ever running that file again.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} projectPath
|
|
82
|
+
* @param {string} targetRelDir - relative to projectPath, e.g. '.husky'
|
|
83
|
+
* @returns {{wired: boolean, reason?: string, hint?: string}}
|
|
84
|
+
*/
|
|
85
|
+
export function wireGitHooksPath(projectPath, targetRelDir) {
|
|
86
|
+
const targetAbs = join(projectPath, targetRelDir);
|
|
87
|
+
const configured = getLocalHooksPathConfig(projectPath);
|
|
88
|
+
|
|
89
|
+
if (configured) {
|
|
90
|
+
const configuredAbs = isAbsolute(configured) ? configured : join(projectPath, configured);
|
|
91
|
+
if (configuredAbs === targetAbs) {
|
|
92
|
+
return { wired: true };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
wired: false,
|
|
96
|
+
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 \`npx uds check\`, or switch it yourself: git config --local core.hooksPath ${targetRelDir}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Unset → git's default is `.git/hooks`. An existing hook there is the
|
|
102
|
+
// adopter's own (or from a tool that writes it directly, not via
|
|
103
|
+
// core.hooksPath); switching hooksPath now would silently stop git from
|
|
104
|
+
// ever running it again.
|
|
105
|
+
const nativeHookPath = join(projectPath, '.git', 'hooks', 'pre-commit');
|
|
106
|
+
if (existsSync(nativeHookPath)) {
|
|
107
|
+
return {
|
|
108
|
+
wired: false,
|
|
109
|
+
reason: '.git/hooks/pre-commit already exists',
|
|
110
|
+
hint: 'UDS will not overwrite or bypass an existing .git/hooks/pre-commit. Add "npx uds check" to it manually, or remove it and re-run `uds init`.'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
execSync(`git config --local core.hooksPath ${targetRelDir}`, {
|
|
116
|
+
cwd: projectPath,
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
118
|
+
});
|
|
119
|
+
return { wired: true };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return {
|
|
122
|
+
wired: false,
|
|
123
|
+
reason: `could not set git core.hooksPath (${e.message})`,
|
|
124
|
+
hint: `Run manually: git config --local core.hooksPath ${targetRelDir}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Does `filePath` contain the marker UDS writes into a hook it manages? */
|
|
130
|
+
function hasUdsMarker(filePath) {
|
|
131
|
+
try {
|
|
132
|
+
return readFileSync(filePath, 'utf-8').includes('uds check');
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Does `content` begin with a shebang line (`#!...`)?
|
|
140
|
+
*
|
|
141
|
+
* Why this matters on Windows and nowhere else: POSIX git, on ENOEXEC (a
|
|
142
|
+
* script with no shebang), silently retries the exec via `/bin/sh` — that
|
|
143
|
+
* fallback is why a husky v9 hook with no shebang has always worked on macOS
|
|
144
|
+
* and Linux. Git for Windows has no such fallback: without a shebang line it
|
|
145
|
+
* cannot resolve an interpreter at all, and fails EVERY commit with
|
|
146
|
+
* `error: cannot spawn <hookfile>: No such file or directory` — the exact
|
|
147
|
+
* message measured 2026-09-27 in CI (windows-latest) for both a husky hook
|
|
148
|
+
* (which has never carried a shebang) and a `.git/hooks/pre-commit` test
|
|
149
|
+
* fixture that also happened to lack one; a sibling fixture carrying
|
|
150
|
+
* `#!/bin/sh` executed correctly. The message names the hook file, not the
|
|
151
|
+
* missing interpreter, which is why this was first mistaken for a wiring
|
|
152
|
+
* problem rather than a content problem.
|
|
153
|
+
*/
|
|
154
|
+
export function hasShebang(content) {
|
|
155
|
+
return typeof content === 'string' && content.startsWith('#!');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Prepend `#!/bin/sh` when `content` has no shebang; a no-op otherwise. Never
|
|
160
|
+
* touches an existing shebang (an adopter may already declare bash or another
|
|
161
|
+
* interpreter) and never rewrites any other line — pairs with
|
|
162
|
+
* stripLegacyHuskyShLine below, which makes the same promise for the v8
|
|
163
|
+
* sourcing line.
|
|
164
|
+
* @returns {{content: string, added: boolean}}
|
|
165
|
+
*/
|
|
166
|
+
export function ensureShebang(content) {
|
|
167
|
+
if (hasShebang(content)) return { content, added: false };
|
|
168
|
+
return { content: `#!/bin/sh\n${content}`, added: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* husky v8's `_/husky.sh` sourcing line — the exact shape husky itself
|
|
173
|
+
* generated before v9 (`. "$(dirname -- "$0")/_/husky.sh"`, with minor
|
|
174
|
+
* `dirname` argument variations). The directory it sources (`.husky/_/`)
|
|
175
|
+
* only exists after husky's OWN bootstrap has actually run; once git
|
|
176
|
+
* executes the hook file directly — which `wireGitHooksPath` above makes it
|
|
177
|
+
* do, by pointing `core.hooksPath` straight at `.husky` instead of at
|
|
178
|
+
* husky's shim — a hook still carrying this line fails on EVERY commit with
|
|
179
|
+
* "No such file or directory", worse than the original defect. Verified
|
|
180
|
+
* against a real adopter's exact legacy template after following the
|
|
181
|
+
* hooksPath-only advice this module used to give (2026-09-27).
|
|
182
|
+
*
|
|
183
|
+
* Shared by the detector (checkPreCommitHookWiring, below) and the writer
|
|
184
|
+
* (setupHuskyHook's rewrite step, init.js) so they can never disagree on
|
|
185
|
+
* what counts as this line.
|
|
186
|
+
*/
|
|
187
|
+
export const LEGACY_HUSKY_SH_LINE = /^\s*(\.|source)\s+.*_\/husky\.sh["']?\s*$/;
|
|
188
|
+
|
|
189
|
+
/** Does `content` contain husky v8's `_/husky.sh` sourcing line? */
|
|
190
|
+
export function hasLegacyHuskyShLine(content) {
|
|
191
|
+
return content.split('\n').some((line) => LEGACY_HUSKY_SH_LINE.test(line));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Remove husky v8's `_/husky.sh` sourcing line from `content`, if present.
|
|
196
|
+
* Everything else — the adopter's own commands, an existing `uds check`
|
|
197
|
+
* line, a shebang — is left exactly as it was, in its original order.
|
|
198
|
+
* @returns {{content: string, removed: boolean}}
|
|
199
|
+
*/
|
|
200
|
+
export function stripLegacyHuskyShLine(content) {
|
|
201
|
+
const lines = content.split('\n');
|
|
202
|
+
const kept = lines.filter((line) => !LEGACY_HUSKY_SH_LINE.test(line));
|
|
203
|
+
return { content: kept.join('\n'), removed: kept.length !== lines.length };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Read-only detector for `uds check`: a UDS-managed pre-commit hook file
|
|
208
|
+
* exists, but is it actually on git's execution path?
|
|
209
|
+
*
|
|
210
|
+
* Handles three wiring shapes as "wired":
|
|
211
|
+
* 1. Direct — `core.hooksPath` points straight at the UDS-managed file's
|
|
212
|
+
* directory (what `wireGitHooksPath` above sets up).
|
|
213
|
+
* 2. husky's own bootstrap — `core.hooksPath` points at `<dir>/_` (a shim
|
|
214
|
+
* directory husky itself creates via `npx husky`/the `prepare` script);
|
|
215
|
+
* the shim at `<dir>/_/pre-commit` forwards to `<dir>/pre-commit`, the
|
|
216
|
+
* UDS-managed file, one directory up.
|
|
217
|
+
* 3. Non-Node native install — `.git/hooks/pre-commit` IS the UDS-managed
|
|
218
|
+
* file, and `core.hooksPath` is unset (git's default).
|
|
219
|
+
*
|
|
220
|
+
* Also reports `missingShebang`, independent of `wired`: a hook file that
|
|
221
|
+
* IS on git's execution path can still fail every commit on Windows if it
|
|
222
|
+
* has no `#!` line (see hasShebang above) — a portability defect, not a
|
|
223
|
+
* wiring defect, so it is surfaced even when `wired: true`.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} projectPath
|
|
226
|
+
* @returns {{relevant: boolean, wired?: boolean, hookFile?: string,
|
|
227
|
+
* configuredHooksPath?: string|null, effectiveHooksDir?: string|null,
|
|
228
|
+
* legacyV8?: boolean, missingShebang?: boolean}}
|
|
229
|
+
* `relevant: false` means there is nothing UDS-managed to report on (no
|
|
230
|
+
* hook file, or hooks-dir could not be determined — never guess "unwired"
|
|
231
|
+
* from a failed lookup).
|
|
232
|
+
*/
|
|
233
|
+
export function checkPreCommitHookWiring(projectPath) {
|
|
234
|
+
if (!existsSync(join(projectPath, '.git'))) return { relevant: false };
|
|
235
|
+
|
|
236
|
+
const huskyHookPath = join(projectPath, '.husky', 'pre-commit');
|
|
237
|
+
const nativeHookPath = join(projectPath, '.git', 'hooks', 'pre-commit');
|
|
238
|
+
const hasHuskyHook = existsSync(huskyHookPath) && hasUdsMarker(huskyHookPath);
|
|
239
|
+
const hasNativeHook = existsSync(nativeHookPath) && hasUdsMarker(nativeHookPath);
|
|
240
|
+
|
|
241
|
+
if (!hasHuskyHook && !hasNativeHook) return { relevant: false };
|
|
242
|
+
|
|
243
|
+
const effectiveHooksDir = getEffectiveHooksDir(projectPath);
|
|
244
|
+
if (!effectiveHooksDir) return { relevant: false }; // can't determine — do not guess
|
|
245
|
+
|
|
246
|
+
// The UDS-managed source file — not the resolved effectiveFile below, which
|
|
247
|
+
// for husky's shim shape (case 2) is a forwarding script we do not own —
|
|
248
|
+
// is the one whose first line actually decides whether Windows can spawn it.
|
|
249
|
+
const hookFile = hasHuskyHook ? '.husky/pre-commit' : '.git/hooks/pre-commit';
|
|
250
|
+
const managedPath = hasHuskyHook ? huskyHookPath : nativeHookPath;
|
|
251
|
+
const missingShebang = (() => {
|
|
252
|
+
try { return !hasShebang(readFileSync(managedPath, 'utf-8')); } catch { return false; }
|
|
253
|
+
})();
|
|
254
|
+
|
|
255
|
+
const effectiveFile = join(effectiveHooksDir, 'pre-commit');
|
|
256
|
+
let wired = false;
|
|
257
|
+
if (existsSync(effectiveFile)) {
|
|
258
|
+
if (hasUdsMarker(effectiveFile)) {
|
|
259
|
+
wired = true;
|
|
260
|
+
} else if (basename(effectiveHooksDir) === '_') {
|
|
261
|
+
// husky-style shim dir: the real script lives one directory up.
|
|
262
|
+
const delegated = join(dirname(effectiveHooksDir), 'pre-commit');
|
|
263
|
+
wired = existsSync(delegated) && hasUdsMarker(delegated);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (wired) return { relevant: true, wired: true, hookFile, missingShebang };
|
|
268
|
+
|
|
269
|
+
const legacyV8 = hasHuskyHook && (() => {
|
|
270
|
+
try { return hasLegacyHuskyShLine(readFileSync(huskyHookPath, 'utf-8')); } catch { return false; }
|
|
271
|
+
})();
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
relevant: true,
|
|
275
|
+
wired: false,
|
|
276
|
+
hookFile,
|
|
277
|
+
configuredHooksPath: getLocalHooksPathConfig(projectPath),
|
|
278
|
+
effectiveHooksDir,
|
|
279
|
+
legacyV8,
|
|
280
|
+
missingShebang
|
|
281
|
+
};
|
|
282
|
+
}
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.13.0-beta.
|
|
3
|
+
"version": "6.13.0-beta.5",
|
|
4
4
|
"lastUpdated": "2026-05-13",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"standards": {
|
|
59
59
|
"name": "universal-dev-standards",
|
|
60
60
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
61
|
-
"version": "6.13.0-beta.
|
|
61
|
+
"version": "6.13.0-beta.5"
|
|
62
62
|
},
|
|
63
63
|
"skills": {
|
|
64
64
|
"name": "universal-dev-standards",
|
|
65
65
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
66
66
|
"localPath": "skills",
|
|
67
67
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
|
|
68
|
-
"version": "6.13.0-beta.
|
|
68
|
+
"version": "6.13.0-beta.5",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2295,7 +2295,7 @@
|
|
|
2295
2295
|
"id": "license-compliance",
|
|
2296
2296
|
"name": "License Compliance Standards",
|
|
2297
2297
|
"nameZh": "授權合規標準",
|
|
2298
|
-
"version": "6.13.0-beta.
|
|
2298
|
+
"version": "6.13.0-beta.5",
|
|
2299
2299
|
"source": {
|
|
2300
2300
|
"human": "core/license-compliance.md",
|
|
2301
2301
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2307,7 +2307,7 @@
|
|
|
2307
2307
|
"id": "verification-oracle",
|
|
2308
2308
|
"name": "Verification Oracle Standards",
|
|
2309
2309
|
"nameZh": "驗證 Oracle 標準",
|
|
2310
|
-
"version": "6.13.0-beta.
|
|
2310
|
+
"version": "6.13.0-beta.5",
|
|
2311
2311
|
"source": {
|
|
2312
2312
|
"human": "core/verification-oracle.md",
|
|
2313
2313
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2319,7 +2319,7 @@
|
|
|
2319
2319
|
"id": "model-provenance",
|
|
2320
2320
|
"name": "Model Provenance Policy Standards",
|
|
2321
2321
|
"nameZh": "模型來源政策標準",
|
|
2322
|
-
"version": "6.13.0-beta.
|
|
2322
|
+
"version": "6.13.0-beta.5",
|
|
2323
2323
|
"source": {
|
|
2324
2324
|
"human": "core/model-provenance.md",
|
|
2325
2325
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2331,7 +2331,7 @@
|
|
|
2331
2331
|
"id": "resource-cost-boundary",
|
|
2332
2332
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2333
2333
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2334
|
-
"version": "6.13.0-beta.
|
|
2334
|
+
"version": "6.13.0-beta.5",
|
|
2335
2335
|
"source": {
|
|
2336
2336
|
"human": "core/resource-cost-boundary.md",
|
|
2337
2337
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|