universal-dev-standards 6.13.0-beta.1 → 6.13.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/turn-completion-integrity.ai.yaml +1 -1
- package/bundled/core/turn-completion-integrity.md +12 -4
- package/bundled/hooks/check-turn-completion-codex.mjs +42 -15
- package/bundled/hooks/turn-completion/engine.mjs +11 -2
- package/bundled/hooks/turn-completion/locales/en.mjs +21 -1
- package/bundled/hooks/turn-completion/locales/zh-TW.mjs +25 -2
- package/bundled/locales/zh-CN/CHANGELOG.md +34 -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 +12 -5
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +33 -5
- package/bundled/locales/zh-TW/CHANGELOG.md +34 -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 +12 -5
- 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 +146 -50
- package/src/commands/uninstall.js +1 -1
- package/src/i18n/messages.js +52 -0
- package/src/installers/hooks-installer.js +6 -2
- package/src/uninstallers/hook-uninstaller.js +190 -23
- package/src/utils/git-hooks.js +282 -0
- package/standards-registry.json +7 -7
package/src/commands/check.js
CHANGED
|
@@ -45,6 +45,7 @@ import { t, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
|
|
|
45
45
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
46
46
|
import { lintAll as lintI18nAll, partitionFindings as partitionI18nFindings } from '../lint/i18n.js';
|
|
47
47
|
import { resolveIntegrationFile } from '../core/constants.js';
|
|
48
|
+
import { checkPreCommitHookWiring } from '../utils/git-hooks.js';
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Display the summary of file integrity status
|
|
@@ -509,6 +510,9 @@ export async function checkCommand(options = {}) {
|
|
|
509
510
|
// 錯誤訊息單一出口閘門是否已安裝(只報告,不寫入)
|
|
510
511
|
checkErrorExitGate(projectPath);
|
|
511
512
|
|
|
513
|
+
// pre-commit 檢查檔存在,但 git 實際不會執行它(只報告,不寫入 —— 見 checkPreCommitWiring 下方註解)
|
|
514
|
+
checkPreCommitWiring(projectPath, msg);
|
|
515
|
+
|
|
512
516
|
// Workflow status
|
|
513
517
|
displayWorkflowStatus(projectPath);
|
|
514
518
|
|
|
@@ -1252,6 +1256,81 @@ function checkErrorExitGate(projectPath) {
|
|
|
1252
1256
|
console.log();
|
|
1253
1257
|
}
|
|
1254
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* pre-commit 檢查檔存在,但 git 實際不會執行它:偵測並報告,絕不寫入。
|
|
1261
|
+
*
|
|
1262
|
+
* 🔴 起因:`uds init` 會寫 `.husky/pre-commit`(或非 Node 專案的
|
|
1263
|
+
* `.git/hooks/pre-commit`),但過去從未確認 git 真的會執行它——它依賴 husky
|
|
1264
|
+
* 自己的 bootstrap(`npm install` 觸發 `prepare` script 設定
|
|
1265
|
+
* `core.hooksPath`),而那個時機點如果 `node_modules` 已存在就永遠不會再發生。
|
|
1266
|
+
* 2026-09-26 實測三個既有採用者(asiaostrich-telemetry-server、
|
|
1267
|
+
* asiaostrich-telemetry-client、machine-setup):三者都有呼叫
|
|
1268
|
+
* `npx uds check` 的 `.husky/pre-commit`,但 `core.hooksPath` 皆未設定,
|
|
1269
|
+
* 提交時檢查從未跑過,且沒有任何錯誤訊息——這正是這道檢查要補上的訊號。
|
|
1270
|
+
*
|
|
1271
|
+
* 只報告不寫入的理由與 checkErrorExitGate 相同:`uds check` 不是使用者同意我們
|
|
1272
|
+
* 動他 git 設定的時刻;真要修,`uds init`(新 clone/尚未跑過)或使用者自己
|
|
1273
|
+
* 手動下指令(既有 clone)才是合適的落筆點。
|
|
1274
|
+
*
|
|
1275
|
+
* 🔴 刻意不影響 `allGood` / `--ci` 的 exit code:這是一個既有裝置的健康度警告
|
|
1276
|
+
* (類似 checkErrorExitGate、checkFullCoverageCompliance 的既有慣例),不是
|
|
1277
|
+
* 標準本身的落差,翻動 exit code 會讓不相關的 CI pipeline 因為一個提交前檢查
|
|
1278
|
+
* 的本機設定而失敗,而那個設定本來就是 per-clone、CI 環境通常另有一套。
|
|
1279
|
+
*/
|
|
1280
|
+
export function checkPreCommitWiring(projectPath, msg) {
|
|
1281
|
+
const result = checkPreCommitHookWiring(projectPath);
|
|
1282
|
+
if (!result.relevant) return; // 沒有 UDS 管理的 hook
|
|
1283
|
+
|
|
1284
|
+
if (result.wired) {
|
|
1285
|
+
// 🔴 「已確認會執行」在 POSIX 上為真,在 Windows 上不一定——git for
|
|
1286
|
+
// Windows 沒有 POSIX 的 ENOEXEC → /bin/sh 後備機制,缺 shebang 的 hook
|
|
1287
|
+
// 每次 commit 都會失敗,訊息卻指向 hook 檔本身
|
|
1288
|
+
// (`cannot spawn <file>: No such file or directory`),2026-09-27 實測
|
|
1289
|
+
// 於 CI windows-latest。這與「wiring」是兩個獨立的缺陷面,有各自的旗標。
|
|
1290
|
+
if (result.missingShebang) {
|
|
1291
|
+
console.log(chalk.yellow((msg.hookMissingShebangTitle || '⚠ [pre-commit] {file} has no shebang line — git cannot run it on Windows.')
|
|
1292
|
+
.replace('{file}', result.hookFile)));
|
|
1293
|
+
console.log(chalk.gray((msg.hookMissingShebangFix || '').replace(/\{file\}/g, result.hookFile)));
|
|
1294
|
+
console.log();
|
|
1295
|
+
}
|
|
1296
|
+
return; // 已確認會執行——wiring 本身安靜通過
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
console.log(chalk.yellow((msg.hookNotWiredTitle || '⚠ [pre-commit] {file} was installed, but git will not run it.')
|
|
1300
|
+
.replace('{file}', result.hookFile)));
|
|
1301
|
+
|
|
1302
|
+
if (result.configuredHooksPath) {
|
|
1303
|
+
const key = result.hookFile === '.git/hooks/pre-commit' ? 'hookNotWiredFixNative' : 'hookNotWiredOverride';
|
|
1304
|
+
console.log(chalk.gray((msg[key] || '').replace('{path}', result.configuredHooksPath)));
|
|
1305
|
+
// 🔴 legacy 舊範本即使在 override 分支也要提醒——一旦使用者照上面那行改用
|
|
1306
|
+
// .husky,同一個 `_/husky.sh` 陷阱一樣會炸。絕不能只在「非 override」分支講。
|
|
1307
|
+
if (result.legacyV8) {
|
|
1308
|
+
console.log(chalk.gray((msg.hookNotWiredLegacyV8Fix || '').replace(/\{file\}/g, result.hookFile)));
|
|
1309
|
+
}
|
|
1310
|
+
} else if (result.legacyV8) {
|
|
1311
|
+
// 🔴 這是 2026-09-27 的實測教訓:舊 husky v8 範本(含 `_/husky.sh` source
|
|
1312
|
+
// 那一行)若只給「設定 core.hooksPath」這個修法,使用者照做後 git 會直接
|
|
1313
|
+
// 執行這個檔案、卡死在那一行、每次 commit 都失敗——比原本的缺陷更糟。
|
|
1314
|
+
// 這裡絕不能退化成只印 hookNotWiredFix;一定要用把「先刪行、再設定」
|
|
1315
|
+
// 兩步講完整的版本。`uds init` 對已初始化專案會直接拒絕執行,修不了這個,
|
|
1316
|
+
// 所以這裡的文字必須自己講完整,不能叫使用者去跑別的指令。
|
|
1317
|
+
console.log(chalk.gray((msg.hookNotWiredFixLegacy || '').replace(/\{file\}/g, result.hookFile)));
|
|
1318
|
+
} else {
|
|
1319
|
+
console.log(chalk.gray((msg.hookNotWiredUnwired || '').replace('{file}', result.hookFile)));
|
|
1320
|
+
console.log(chalk.gray(msg.hookNotWiredFix || ''));
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Independent of wiring — a hook can be fully wired for commit and still
|
|
1324
|
+
// fail every commit on Windows if it has no shebang (see the `wired`
|
|
1325
|
+
// branch above for why). Say so here too so fixing wiring alone does not
|
|
1326
|
+
// look like a complete fix.
|
|
1327
|
+
if (result.missingShebang) {
|
|
1328
|
+
console.log(chalk.gray((msg.hookMissingShebangFix || '').replace(/\{file\}/g, result.hookFile)));
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
console.log();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1255
1334
|
/**
|
|
1256
1335
|
* XSPEC-178: Check full-coverage-testing standard presence and STUB markers
|
|
1257
1336
|
*/
|
package/src/commands/init.js
CHANGED
|
@@ -28,6 +28,7 @@ import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
|
28
28
|
import { readInstallYaml } from '../utils/config-manager.js';
|
|
29
29
|
import { resolveIntegrationTargetFile } from '../utils/integration-generator.js';
|
|
30
30
|
import { withFileTransaction } from '../utils/transaction.js';
|
|
31
|
+
import { wireGitHooksPath, getLocalHooksPathConfig, stripLegacyHuskyShLine, ensureShebang } from '../utils/git-hooks.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Init command - initialize standards in current project
|
|
@@ -339,32 +340,42 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
|
|
|
339
340
|
const isNodeProject = existsSync(join(projectPath, 'package.json'));
|
|
340
341
|
|
|
341
342
|
if (isNodeProject) {
|
|
342
|
-
console.log(chalk.cyan('Configuring Pre-commit Hook
|
|
343
|
+
console.log(chalk.cyan('Configuring Pre-commit Hook...'));
|
|
343
344
|
|
|
344
345
|
// Every edit we make to the adopter's package.json, reported at the end.
|
|
345
346
|
// `uds init` writes ~70 files; a one-line change to package.json is invisible
|
|
346
347
|
// in that diff unless we say it out loud (XSPEC-341 R1).
|
|
347
348
|
const pkgChanges = [];
|
|
349
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
348
350
|
|
|
349
|
-
// 1.
|
|
351
|
+
// 1. Do NOT install husky, and do NOT touch package.json's dependencies.
|
|
352
|
+
//
|
|
353
|
+
// This used to run `npm install --save-dev husky` here. Installing a
|
|
354
|
+
// package for the adopter is not this command's call to make, and — the
|
|
355
|
+
// actual bug this fix addresses — the wiring that install produced only
|
|
356
|
+
// took effect the NEXT time `npm install` ran husky's own `prepare`
|
|
357
|
+
// script. If node_modules already existed, that install never ran again,
|
|
358
|
+
// and the hook this function writes below was never executed by git.
|
|
359
|
+
// Verified against three real adopters (asiaostrich-telemetry-server,
|
|
360
|
+
// asiaostrich-telemetry-client, machine-setup, 2026-09-26): all three
|
|
361
|
+
// have `.husky/pre-commit` calling `npx uds check`, none has
|
|
362
|
+
// `core.hooksPath` set, and the check has never once run on commit.
|
|
363
|
+
//
|
|
364
|
+
// Whether husky is installed is the adopter's own decision. Step 5 below
|
|
365
|
+
// wires git directly and works with or without it.
|
|
366
|
+
let hasHusky = false;
|
|
350
367
|
try {
|
|
351
|
-
const pkgPath = join(projectPath, 'package.json');
|
|
352
368
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (!hasHusky) {
|
|
356
|
-
console.log(chalk.gray(' Installing husky...'));
|
|
357
|
-
// stdio: 'pipe' rather than 'ignore' — the error text belongs in the
|
|
358
|
-
// message below, not in /dev/null.
|
|
359
|
-
execSync('npm install --save-dev husky', { stdio: 'pipe', cwd: projectPath });
|
|
360
|
-
pkgChanges.push('devDependencies.husky — added');
|
|
361
|
-
}
|
|
369
|
+
hasHusky = Boolean(pkg.devDependencies?.husky || pkg.dependencies?.husky);
|
|
362
370
|
} catch (e) {
|
|
363
|
-
console.log(chalk.yellow(` ⚠ Failed to
|
|
364
|
-
return;
|
|
371
|
+
console.log(chalk.yellow(` ⚠ Failed to read package.json: ${e.message}`));
|
|
365
372
|
}
|
|
366
373
|
|
|
367
|
-
// 2.
|
|
374
|
+
// 2. If the adopter already depends on husky, also chain its `prepare`
|
|
375
|
+
// script so a future `npm install` keeps husky's own wiring in sync too.
|
|
376
|
+
// This is belt-and-suspenders — step 5 wires git directly regardless of
|
|
377
|
+
// whether this succeeds — kept only because an adopter who already uses
|
|
378
|
+
// husky should not have `npm install` silently stop maintaining it.
|
|
368
379
|
//
|
|
369
380
|
// We deliberately do NOT run `npx husky init` (XSPEC-341 R1). That command is a
|
|
370
381
|
// one-time bootstrap for a NEW project, not an idempotent operation: it sets
|
|
@@ -375,30 +386,31 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
|
|
|
375
386
|
// (It also seeds .husky/pre-commit with `npm test`, a gate the adopter never asked
|
|
376
387
|
// for.) Adopting a standards library must never rewrite the adopter's build.
|
|
377
388
|
const huskyDir = join(projectPath, '.husky');
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
if (hasHusky) {
|
|
390
|
+
try {
|
|
391
|
+
const raw = readFileSync(pkgPath, 'utf-8');
|
|
392
|
+
const pkg = JSON.parse(raw);
|
|
393
|
+
pkg.scripts = pkg.scripts || {};
|
|
394
|
+
const existing = pkg.scripts.prepare;
|
|
395
|
+
|
|
396
|
+
if (!existing) {
|
|
397
|
+
pkg.scripts.prepare = 'husky';
|
|
398
|
+
pkgChanges.push('scripts.prepare — added: "husky"');
|
|
399
|
+
} else if (!/\bhusky\b/.test(existing)) {
|
|
400
|
+
// Chain, never clobber. The adopter's command runs first and keeps its
|
|
401
|
+
// exit code meaningful.
|
|
402
|
+
pkg.scripts.prepare = `${existing} && husky`;
|
|
403
|
+
pkgChanges.push(`scripts.prepare — "${existing}" → "${pkg.scripts.prepare}"`);
|
|
404
|
+
}
|
|
394
405
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
406
|
+
if (pkg.scripts.prepare !== existing) {
|
|
407
|
+
// Preserve the file's trailing newline convention.
|
|
408
|
+
const indent = raw.match(/^\{\n(\s+)"/)?.[1]?.length ?? 2;
|
|
409
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + (raw.endsWith('\n') ? '\n' : ''), 'utf-8');
|
|
410
|
+
}
|
|
411
|
+
} catch (e) {
|
|
412
|
+
console.log(chalk.yellow(` ⚠ Failed to configure the prepare script: ${e.message}`));
|
|
399
413
|
}
|
|
400
|
-
} catch (e) {
|
|
401
|
-
console.log(chalk.yellow(` ⚠ Failed to configure the prepare script: ${e.message}`));
|
|
402
414
|
}
|
|
403
415
|
|
|
404
416
|
// 3. Ensure .husky directory exists
|
|
@@ -411,34 +423,97 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
|
|
|
411
423
|
}
|
|
412
424
|
}
|
|
413
425
|
|
|
414
|
-
// 4. Add pre-commit hook
|
|
426
|
+
// 4. Add pre-commit hook content
|
|
415
427
|
const preCommitPath = join(huskyDir, 'pre-commit');
|
|
416
428
|
const udsCmd = 'npx uds check';
|
|
417
429
|
|
|
418
430
|
try {
|
|
419
|
-
// husky v9
|
|
420
|
-
// (that is v8 syntax, deprecated in v9 and removed in v10).
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
431
|
+
// husky v9's own templates carry no shebang, no `_/husky.sh` sourcing
|
|
432
|
+
// (that is v8 syntax, deprecated in v9 and removed in v10). Verified
|
|
433
|
+
// empirically (2026-09-26, macOS): git executes a hook file with no
|
|
434
|
+
// shebang directly and correctly propagates its exit code, once it is
|
|
435
|
+
// on git's hooks path — see step 5.
|
|
436
|
+
//
|
|
437
|
+
// 🔴 That verification did not cover Windows, and Windows behaves
|
|
438
|
+
// differently: POSIX git falls back to `/bin/sh` on ENOEXEC (a script
|
|
439
|
+
// with no shebang) — that fallback is why the shebang-less shape above
|
|
440
|
+
// has always worked on macOS and Linux. Git for Windows has no such
|
|
441
|
+
// fallback and fails EVERY commit with `error: cannot spawn
|
|
442
|
+
// <hookfile>: No such file or directory` (measured 2026-09-27 in CI,
|
|
443
|
+
// windows-latest). So a shebang is added below regardless of platform
|
|
444
|
+
// — cheap, harmless on POSIX, and required on Windows. Existing files
|
|
445
|
+
// are appended to, never rewritten — their contents are the adopter's,
|
|
446
|
+
// not ours; ensureShebang only ever prepends a missing first line.
|
|
447
|
+
let content = existsSync(preCommitPath) ? readFileSync(preCommitPath, 'utf-8') : '';
|
|
448
|
+
|
|
449
|
+
// A pre-existing file may still carry husky v8's `_/husky.sh` sourcing
|
|
450
|
+
// line (adopted before v9, or before this fix). That line references a
|
|
451
|
+
// directory (`.husky/_/`) that only exists after husky's OWN bootstrap
|
|
452
|
+
// has run — but step 5 below wires `core.hooksPath` straight at
|
|
453
|
+
// `.husky`, so git executes THIS FILE directly. Left in place, that
|
|
454
|
+
// line would make every commit fail with "No such file or directory" —
|
|
455
|
+
// strictly worse than the original defect (verified against a real
|
|
456
|
+
// adopter's exact legacy template, 2026-09-27). Rewrite it away rather
|
|
457
|
+
// than appending alongside it; everything else — the adopter's own
|
|
458
|
+
// commands, an existing `uds check` line — is preserved as-is.
|
|
459
|
+
const { content: destripped, removed: hadLegacyLine } = stripLegacyHuskyShLine(content);
|
|
460
|
+
content = destripped;
|
|
461
|
+
|
|
462
|
+
const needsAppend = !content.includes('uds check');
|
|
463
|
+
if (needsAppend) {
|
|
426
464
|
const sep = content && !content.endsWith('\n') ? '\n' : '';
|
|
427
|
-
|
|
465
|
+
content = `${content}${sep}\n# UDS Standard Check\n${udsCmd}\n`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const { content: shebanged, added: addedShebang } = ensureShebang(content);
|
|
469
|
+
content = shebanged;
|
|
470
|
+
|
|
471
|
+
if (hadLegacyLine || needsAppend || addedShebang) {
|
|
472
|
+
writeFileSync(preCommitPath, content, 'utf-8');
|
|
428
473
|
try {
|
|
429
474
|
execSync(`chmod +x ${preCommitPath}`);
|
|
430
475
|
} catch {
|
|
431
476
|
// Ignore chmod failures on systems that don't support it
|
|
432
477
|
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (hadLegacyLine) {
|
|
481
|
+
console.log(chalk.yellow(' ⚠ Rewrote legacy husky v8 template: removed `_/husky.sh` sourcing (that directory only exists after husky\'s own bootstrap runs; git now executes this file directly, so that line would have failed every commit).'));
|
|
482
|
+
}
|
|
483
|
+
if (addedShebang) {
|
|
484
|
+
console.log(chalk.yellow(' ⚠ Added a missing #!/bin/sh shebang: without it, git cannot execute this hook on Windows.'));
|
|
485
|
+
}
|
|
486
|
+
if (needsAppend) {
|
|
433
487
|
console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
|
|
434
|
-
} else {
|
|
488
|
+
} else if (!hadLegacyLine && !addedShebang) {
|
|
435
489
|
console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
|
|
436
490
|
}
|
|
437
491
|
} catch (e) {
|
|
438
492
|
console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
|
|
439
493
|
}
|
|
440
494
|
|
|
441
|
-
// 5.
|
|
495
|
+
// 5. Wire git so the hook ACTUALLY runs — this is the fix. Setting
|
|
496
|
+
// `core.hooksPath` ourselves is what husky's own bootstrap does internally
|
|
497
|
+
// (verified against husky ^9.1.7's source: `git config core.hooksPath
|
|
498
|
+
// <dir>/_`, plus a shim under `_/` forwarding to the real script); doing
|
|
499
|
+
// it directly means the check is live immediately after `uds init`,
|
|
500
|
+
// whether or not husky is installed or `npm install` ever runs again.
|
|
501
|
+
// Never overrides an adopter's own `core.hooksPath`, or an existing
|
|
502
|
+
// native `.git/hooks/pre-commit` (see wireGitHooksPath).
|
|
503
|
+
const wireResult = wireGitHooksPath(projectPath, '.husky');
|
|
504
|
+
if (wireResult.wired) {
|
|
505
|
+
console.log(chalk.green(' ✓ Pre-commit check is active for this clone (git core.hooksPath → .husky)'));
|
|
506
|
+
} else {
|
|
507
|
+
console.log(chalk.yellow(` ⚠ Pre-commit check NOT enabled: ${wireResult.reason}`));
|
|
508
|
+
console.log(chalk.gray(` ${wireResult.hint}`));
|
|
509
|
+
}
|
|
510
|
+
// `core.hooksPath` is LOCAL git config — it is never committed. This wiring
|
|
511
|
+
// only applies to this clone; `uds check` reports the gap for anyone who
|
|
512
|
+
// clones the repo without re-running this step (see
|
|
513
|
+
// checkPreCommitHookWiring in git-hooks.js / check.js).
|
|
514
|
+
console.log(chalk.gray(' Note: this is per-clone (git config is not committed) — teammates need to run `uds init` (or the fix above) in their own clone too.'));
|
|
515
|
+
|
|
516
|
+
// 6. Say what we changed in their package.json.
|
|
442
517
|
if (pkgChanges.length > 0) {
|
|
443
518
|
console.log(chalk.cyan(' package.json modified:'));
|
|
444
519
|
for (const change of pkgChanges) {
|
|
@@ -452,13 +527,34 @@ export async function setupHuskyHook(projectPath, { allowInTest = false } = {})
|
|
|
452
527
|
const hookDir = join(projectPath, '.git', 'hooks');
|
|
453
528
|
const hookPath = join(hookDir, 'pre-commit');
|
|
454
529
|
|
|
530
|
+
// git's default hooks directory is `.git/hooks` — but only when
|
|
531
|
+
// `core.hooksPath` is unset. If the adopter (or another tool) already
|
|
532
|
+
// points it elsewhere, writing here would produce a file git never looks
|
|
533
|
+
// at, and `uds init` would report success for a check that never runs.
|
|
534
|
+
const configuredHooksPath = getLocalHooksPathConfig(projectPath);
|
|
535
|
+
if (configuredHooksPath) {
|
|
536
|
+
console.log(chalk.yellow(` ⚠ Pre-commit check NOT enabled: git core.hooksPath is already set to "${configuredHooksPath}"`));
|
|
537
|
+
console.log(chalk.gray(' UDS writes to .git/hooks/pre-commit, but git will not look there while core.hooksPath points elsewhere.'));
|
|
538
|
+
console.log(chalk.gray(` Add "uds check" to a pre-commit hook under "${configuredHooksPath}" yourself, or: git config --local --unset core.hooksPath`));
|
|
539
|
+
console.log();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
455
543
|
try {
|
|
456
544
|
if (!existsSync(hookDir)) {
|
|
457
545
|
mkdirSync(hookDir, { recursive: true });
|
|
458
546
|
}
|
|
459
547
|
|
|
460
|
-
if (existsSync(hookPath)
|
|
461
|
-
|
|
548
|
+
if (existsSync(hookPath)) {
|
|
549
|
+
const existingContent = readFileSync(hookPath, 'utf-8');
|
|
550
|
+
if (existingContent.includes('uds check')) {
|
|
551
|
+
console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
|
|
552
|
+
} else {
|
|
553
|
+
// Never clobber an adopter's own hook (this fix — it used to be
|
|
554
|
+
// overwritten unconditionally here).
|
|
555
|
+
console.log(chalk.yellow(' ⚠ Pre-commit check NOT enabled: .git/hooks/pre-commit already exists'));
|
|
556
|
+
console.log(chalk.gray(' UDS will not overwrite it. Add "uds check" to it yourself, or remove it and re-run `uds init`.'));
|
|
557
|
+
}
|
|
462
558
|
} else {
|
|
463
559
|
const hookContent = `#!/bin/sh
|
|
464
560
|
# UDS pre-commit hook
|
|
@@ -56,7 +56,7 @@ export async function uninstallCommand(options) {
|
|
|
56
56
|
const categories = await checkbox({
|
|
57
57
|
message: msg.selectCategories,
|
|
58
58
|
choices: [
|
|
59
|
-
{ name: `${msg.categoryHooks} (.husky/pre-commit)`, value: 'hooks', checked: true },
|
|
59
|
+
{ name: `${msg.categoryHooks} (.husky/pre-commit, .claude/settings.json, .codex/hooks.json, .gemini/settings.json)`, value: 'hooks', checked: true },
|
|
60
60
|
{ name: `${msg.categorySkills} (skills, commands)`, value: 'skills', checked: true },
|
|
61
61
|
{ name: `${msg.categoryIntegrations} (CLAUDE.md, .cursorrules, ...)`, value: 'integrations', checked: true },
|
|
62
62
|
{ name: `${msg.categoryStandards} (.standards/)`, value: 'standards', checked: true }
|
package/src/i18n/messages.js
CHANGED
|
@@ -872,6 +872,26 @@ export const messages = {
|
|
|
872
872
|
commandsInstalledSuccess: 'Installed commands for {count} AI tools',
|
|
873
873
|
// Read-only hint
|
|
874
874
|
missingSkillsHint: 'Tip: Run `uds update` to install missing Skills/Commands',
|
|
875
|
+
// Pre-commit hook wiring (XSPEC: uds init writes a hook but never
|
|
876
|
+
// confirmed git would run it — this reports the gap, read-only)
|
|
877
|
+
hookNotWiredTitle: '⚠ [pre-commit] {file} was installed, but git will not run it.',
|
|
878
|
+
hookNotWiredOverride: ' git core.hooksPath is set to "{path}" — UDS will not override it. Confirm the hook there also runs `npx uds check`, or switch it yourself: git config --local core.hooksPath .husky',
|
|
879
|
+
hookNotWiredUnwired: ' git core.hooksPath is not set, and {file} is not on git\'s default hook path.',
|
|
880
|
+
hookNotWiredFix: ' Fix (per-clone — not committed, teammates must repeat it): git config --local core.hooksPath .husky',
|
|
881
|
+
hookNotWiredFixNative: ' Fix: point core.hooksPath back to the default (git config --local --unset core.hooksPath), or add `uds check` under "{path}" instead.',
|
|
882
|
+
// Order matters here: setting hooksPath BEFORE removing the `_/husky.sh`
|
|
883
|
+
// line makes git execute {file} directly and fail on that line — verified
|
|
884
|
+
// against a real adopter's exact legacy template, 2026-09-27. `uds init`
|
|
885
|
+
// will not fix this for an already-initialized project (it refuses to run
|
|
886
|
+
// at all once `.standards/` exists), so the fix must be self-contained here.
|
|
887
|
+
hookNotWiredFixLegacy: ' Fix, in this exact order — this file still sources `_/husky.sh` (husky v8 syntax), a directory that only exists after husky\'s own bootstrap has run: (1) delete the line `. "$(dirname -- "$0")/_/husky.sh"` from {file}; (2) then run: git config --local core.hooksPath .husky (per-clone — not committed, teammates must repeat it). Doing (2) alone makes every commit fail with an error that `_/husky.sh` cannot be found ("No such file or directory" on macOS, ".: cannot open" on Linux).',
|
|
888
|
+
hookNotWiredLegacyV8Fix: ' Also: {file} still sources `_/husky.sh` (husky v8 syntax) — delete that line too, or the fix above will make every commit fail with an error that `_/husky.sh` cannot be found ("No such file or directory" on macOS, ".: cannot open" on Linux).',
|
|
889
|
+
// Missing shebang: independent of wiring — POSIX git falls back to
|
|
890
|
+
// /bin/sh on ENOEXEC (a hook with no shebang), git for Windows does
|
|
891
|
+
// not, and fails every commit with "cannot spawn {file}: No such
|
|
892
|
+
// file or directory". Measured 2026-09-27 in CI (windows-latest).
|
|
893
|
+
hookMissingShebangTitle: '⚠ [pre-commit] {file} has no shebang line — git cannot run it on Windows.',
|
|
894
|
+
hookMissingShebangFix: ' Fix: add a `#!/bin/sh` shebang as the very first line of {file} (a husky-managed hook gets this automatically the next time `uds init` touches it), or re-run `uds init`.',
|
|
875
895
|
// Summary mode (--summary)
|
|
876
896
|
summary_mode: {
|
|
877
897
|
title: 'UDS Status Summary',
|
|
@@ -2112,6 +2132,24 @@ export const messages = {
|
|
|
2112
2132
|
commandsInstalledSuccess: '已為 {count} 個 AI 工具安裝斜線命令',
|
|
2113
2133
|
// Read-only hint
|
|
2114
2134
|
missingSkillsHint: '提示:執行 `uds update` 安裝缺少的 Skills/斜線命令',
|
|
2135
|
+
// 提交前檢查是否真的會被 git 執行(uds init 寫了檢查檔,卻沒確認 git 會跑它——這裡只回報,不寫入)
|
|
2136
|
+
hookNotWiredTitle: '⚠ [pre-commit] {file} 已安裝,但 git 實際不會執行它。',
|
|
2137
|
+
hookNotWiredOverride: ' git core.hooksPath 已設定為「{path}」——UDS 不會覆蓋它。請確認該路徑下的檔案也會執行 `npx uds check`,或自行改用:git config --local core.hooksPath .husky',
|
|
2138
|
+
hookNotWiredUnwired: ' git core.hooksPath 未設定,而 {file} 也不在 git 預設會讀取的路徑上。',
|
|
2139
|
+
hookNotWiredFix: ' 修復方式(僅對此 clone 生效,不會進版控,其他人 clone 後要自己再跑一次):git config --local core.hooksPath .husky',
|
|
2140
|
+
hookNotWiredFixNative: ' 修復方式:把 core.hooksPath 改回預設(git config --local --unset core.hooksPath),或改在「{path}」底下也加上 `uds check`。',
|
|
2141
|
+
// 順序不可顛倒:先設 hooksPath 再刪那一行,會讓 git 直接執行 {file} 並卡在
|
|
2142
|
+
// `_/husky.sh` 那一行(2026-09-26 已在真實採用者的舊範本上實測到)。
|
|
2143
|
+
// `uds init` 對已初始化的專案會直接拒絕執行,修不了這個,所以這裡要給
|
|
2144
|
+
// 一次就講完整、不需要再跑任何指令的修法。
|
|
2145
|
+
hookNotWiredFixLegacy: ' 修復方式,順序不可顛倒——這個檔案還留著 `_/husky.sh`(husky v8 舊語法),它 source 的目錄只有在 husky 自己的 bootstrap 跑過後才存在:(1) 先刪掉 {file} 裡的這一行:`. "$(dirname -- "$0")/_/husky.sh"`;(2) 再執行:git config --local core.hooksPath .husky(僅對此 clone 生效,不會進版控,其他人要自己再做一次)。只做 (2) 不做 (1) 會讓每一次提交都失敗,印出找不到 `_/husky.sh` 的錯誤(macOS 為「No such file or directory」、Linux 為「.: cannot open」)。',
|
|
2146
|
+
hookNotWiredLegacyV8Fix: ' 另外:{file} 還留著 `_/husky.sh`(husky v8 舊語法)——這一行也要刪掉,不然上面的修復方式會讓每一次提交都失敗,印出找不到 `_/husky.sh` 的錯誤(macOS 為「No such file or directory」、Linux 為「.: cannot open」)。',
|
|
2147
|
+
// 缺少 shebang:與 wiring 無關——POSIX git 在 ENOEXEC(hook 沒有 shebang)
|
|
2148
|
+
// 時會退回用 /bin/sh 執行,git for Windows 沒有這個後備機制,每次提交
|
|
2149
|
+
// 都會失敗,訊息是「cannot spawn {file}: No such file or directory」。
|
|
2150
|
+
// 2026-09-27 於 CI(windows-latest)實測。
|
|
2151
|
+
hookMissingShebangTitle: '⚠ [pre-commit] {file} 沒有 shebang 行——在 Windows 上 git 無法執行它。',
|
|
2152
|
+
hookMissingShebangFix: ' 修復方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 動到它時會自動處理),或重新執行 `uds init`。',
|
|
2115
2153
|
// Summary mode (--summary)
|
|
2116
2154
|
summary_mode: {
|
|
2117
2155
|
title: 'UDS 狀態摘要',
|
|
@@ -3364,6 +3402,20 @@ export const messages = {
|
|
|
3364
3402
|
commandsInstalledSuccess: '已为 {count} 个 AI 工具安装斜线命令',
|
|
3365
3403
|
// Read-only hint
|
|
3366
3404
|
missingSkillsHint: '提示:执行 `uds update` 安装缺少的 Skills/斜线命令',
|
|
3405
|
+
// 提交前检查是否真的会被 git 执行(uds init 写了检查文件,却没确认 git 会跑它——这里只回报,不写入)
|
|
3406
|
+
hookNotWiredTitle: '⚠ [pre-commit] {file} 已安装,但 git 实际不会执行它。',
|
|
3407
|
+
hookNotWiredOverride: ' git core.hooksPath 已设定为“{path}”——UDS 不会覆盖它。请确认该路径下的文件也会执行 `npx uds check`,或自行改用:git config --local core.hooksPath .husky',
|
|
3408
|
+
hookNotWiredUnwired: ' git core.hooksPath 未设定,而 {file} 也不在 git 默认会读取的路径上。',
|
|
3409
|
+
hookNotWiredFix: ' 修复方式(仅对此 clone 生效,不会进版控,其他人 clone 后要自己再跑一次):git config --local core.hooksPath .husky',
|
|
3410
|
+
hookNotWiredFixNative: ' 修复方式:把 core.hooksPath 改回默认(git config --local --unset core.hooksPath),或改在“{path}”下也加上 `uds check`。',
|
|
3411
|
+
hookNotWiredFixLegacy: ' 修复方式,顺序不可颠倒——这个文件还留着 `_/husky.sh`(husky v8 旧语法),它 source 的目录只有在 husky 自己的 bootstrap 跑过后才存在:(1) 先删掉 {file} 里的这一行:`. "$(dirname -- "$0")/_/husky.sh"`;(2) 再执行:git config --local core.hooksPath .husky(仅对此 clone 生效,不会进版控,其他人要自己再做一次)。只做 (2) 不做 (1) 会让每一次提交都失败,打印找不到 `_/husky.sh` 的错误(macOS 为“No such file or directory”、Linux 为“.: cannot open”)。',
|
|
3412
|
+
hookNotWiredLegacyV8Fix: ' 另外:{file} 还留着 `_/husky.sh`(husky v8 旧语法)——这一行也要删掉,不然上面的修复方式会让每一次提交都失败,打印找不到 `_/husky.sh` 的错误(macOS 为“No such file or directory”、Linux 为“.: cannot open”)。',
|
|
3413
|
+
// 缺少 shebang:与 wiring 无关——POSIX git 在 ENOEXEC(hook 没有 shebang)
|
|
3414
|
+
// 时会回退用 /bin/sh 执行,git for Windows 没有这个后备机制,每次提交
|
|
3415
|
+
// 都会失败,消息是“cannot spawn {file}: No such file or directory”。
|
|
3416
|
+
// 2026-09-27 于 CI(windows-latest)实测。
|
|
3417
|
+
hookMissingShebangTitle: '⚠ [pre-commit] {file} 没有 shebang 行——在 Windows 上 git 无法执行它。',
|
|
3418
|
+
hookMissingShebangFix: ' 修复方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 动到它时会自动处理),或重新运行 `uds init`。',
|
|
3367
3419
|
// Summary mode (--summary)
|
|
3368
3420
|
summary_mode: {
|
|
3369
3421
|
title: 'UDS 状态摘要',
|
|
@@ -231,8 +231,12 @@ export function installHooks(projectPath) {
|
|
|
231
231
|
*
|
|
232
232
|
* @see core/turn-completion-integrity.md
|
|
233
233
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
// Exported so the uninstaller (../uninstallers/hook-uninstaller.js) can
|
|
235
|
+
// recognize exactly these two script names as UDS's own, rather than
|
|
236
|
+
// guessing from the shared `scripts/hooks/` directory path alone — a path
|
|
237
|
+
// an adopter's own hook could just as easily live under.
|
|
238
|
+
export const CODEX_HOOK_SCRIPT = 'check-turn-completion-codex.mjs';
|
|
239
|
+
export const GEMINI_HOOK_SCRIPT = 'check-turn-completion-gemini.mjs';
|
|
236
240
|
|
|
237
241
|
/** Copy the shared hook scripts into the project, same as installHooks() does. */
|
|
238
242
|
function copyHookScripts(hookDir, hooksDir) {
|