universal-dev-standards 6.13.0-beta.2 → 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/hooks/turn-completion/engine.mjs +11 -2
- package/bundled/locales/zh-CN/CHANGELOG.md +24 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +33 -5
- package/bundled/locales/zh-TW/CHANGELOG.md +24 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- 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/i18n/messages.js +52 -0
- package/src/utils/git-hooks.js +282 -0
- package/standards-registry.json +7 -7
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
id: turn-completion-integrity
|
|
5
5
|
meta:
|
|
6
6
|
version: "1.4.0"
|
|
7
|
-
updated: "2026-09-
|
|
7
|
+
updated: "2026-09-26"
|
|
8
8
|
source: core/turn-completion-integrity.md
|
|
9
9
|
description: An agent must not end a turn having stated a next action it did not take; enforced at turn end, not by instruction
|
|
10
10
|
related:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { join, dirname } from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
18
|
import { detectCommitment, userAskedToStop } from './detect.mjs';
|
|
19
19
|
|
|
20
20
|
export const VERSION = '1.2.0';
|
|
@@ -53,7 +53,16 @@ export async function loadPacks() {
|
|
|
53
53
|
const failed = [];
|
|
54
54
|
for (const id of SHIPPED_LOCALES) {
|
|
55
55
|
try {
|
|
56
|
-
|
|
56
|
+
// A filesystem path (`C:\...` on Windows) is not a valid ESM import
|
|
57
|
+
// specifier — dynamic `import()` needs a `file://` URL there. On
|
|
58
|
+
// POSIX both happen to look like absolute paths that Node accepts, so
|
|
59
|
+
// this went unnoticed until measured 2026-09-27 in CI
|
|
60
|
+
// (windows-latest): every pack failed to load, `failed` was silently
|
|
61
|
+
// non-empty, and every adapter test that expects a block/deny decision
|
|
62
|
+
// got `undefined` instead — the hook ran, found no packs, and let
|
|
63
|
+
// every turn end uninspected. pathToFileURL(...).href is the one
|
|
64
|
+
// form valid on every platform.
|
|
65
|
+
packs.push(await import(pathToFileURL(join(HERE, 'locales', `${id}.mjs`)).href));
|
|
57
66
|
} catch (e) {
|
|
58
67
|
failed.push({ id, why: String((e && e.message) || e) });
|
|
59
68
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.13.0-beta.
|
|
4
|
-
translation_version: 6.13.0-beta.
|
|
5
|
-
last_synced: 2026-09-
|
|
3
|
+
source_version: 6.13.0-beta.4
|
|
4
|
+
translation_version: 6.13.0-beta.4
|
|
5
|
+
last_synced: 2026-09-27
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,6 +17,27 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.13.0-beta.4] - 2026-09-28
|
|
21
|
+
|
|
22
|
+
> **测试版** — 以 `npm install -g universal-dev-standards@beta` 安装。要测什么、已知限制、如何退回正式版:见 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。注意:6.13.0-beta.3 从未上架 npm,其内容随本版发布。
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- **`turn-completion-integrity` 的 Stop hook 在 Windows 上一路到 6.13.0-beta.3 都静默失效——它照跑、找不到任何语言包、然后放行每一轮对话,且什么都不打印。** `engine.mjs` 的 `loadPacks()` 用 `join(HERE, 'locales', ...)` 拼出每个语言包的路径,直接把这个文件系统路径交给动态 `import()`;在 Windows 上那是 `C:\...` 这种路径,不是合法的 ESM import 指定字符串(POSIX 上的绝对路径恰好也能被解析成合法指定字符串,这正是为何在 macOS/Linux 上从未被发现)。2026-09-27 于 CI(windows-latest)实测:每一个出货的语言包都加载失败,而每一个原本该回 `block`/`deny` 决策的适配层(Claude Code、Codex、Gemini CLI)全部返回 `undefined`。修复方式改用 `pathToFileURL(...).href`,与 `cli/src/utils/standard-fixer.js`/`standard-validator.js` 既有的正确写法一致。四支 `scripts/check-*.ts` 开发工具脚本有同样的写法(Windows CI job 不会跑到它们,因为它们只在 `ubuntu-latest` 上执行,但那里同样是坏的),一并以同样方式修正。新增一支全 repo 走查测试(`cli/tests/unit/scripts/no-fs-path-dynamic-import.test.js`)扫描 `scripts/`、`cli/src/`、`cli/scripts/` 找这个写法,未来新增的一处不需要有人记得这次事故也会被挡下。
|
|
27
|
+
- **由 `uds init` 写入的 husky 管理 `.husky/pre-commit`,即使 `core.hooksPath` 已正确接好,在 Windows 上一路到 6.13.0-beta.3 都会让每一次提交失败,错误是 `error: cannot spawn .husky/pre-commit: No such file or directory`。** husky v9 自己的模板没有 shebang 行,这在 macOS/Linux 上一直能用,因为 POSIX git 在脚本没有 shebang 时(`ENOEXEC`)会回退用 `/bin/sh` 执行;git for Windows 没有这个后备机制,完全无法对没有 shebang 的文件 spawn,而且错误信息指向 hook 文件本身而非缺失的解释器——很容易被误判成 wiring 问题而非内容问题。`uds init` 现在会在缺少 shebang 时,于 husky 管理的 hook 最前面补上 `#!/bin/sh`,不分平台一律如此,无论是写新 hook 还是动到既有的采用者文件(只会插入,绝不重写采用者自己的 shebang 或任何其他行)。`uds check` 的 `[pre-commit]` 警告新增 `missingShebang` 信号,独立于 wiring 报告(一个 hook 可以完全接好但在 Windows 上仍因此失败),且维持既有设计,只读不写。
|
|
28
|
+
|
|
29
|
+
## [6.13.0-beta.3] - 2026-09-27
|
|
30
|
+
|
|
31
|
+
> **测试版** — 以 `npm install -g universal-dev-standards@beta` 安装。要测什么、已知限制、如何退回正式版:见 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`uds init` 会写入提交前检查(`.husky/pre-commit`,非 Node 项目则为 `.git/hooks/pre-commit`),却从未确认 git 真的会执行它。** 它依赖 husky 自己的 bootstrap 机制——`npm install` 触发 husky 的 `prepare` script 去设定 `core.hooksPath`——而这个时机只在**下一次** `npm install` 执行时才会发生;如果 `node_modules` 早就存在,这件事就永远不会发生。实测三个既有采用者(asiaostrich-telemetry-server、asiaostrich-telemetry-client、machine-setup,2026-09-26):三者都有调用 `npx uds check` 的 `.husky/pre-commit`,但 `core.hooksPath` 均未设定,提交时检查从未跑过——而且完全没有任何错误信息。`uds init` 现在不再替用户安装 husky 或改动 package.json 的依赖;改为直接执行 `git config --local core.hooksPath .husky`(与 husky 自己 bootstrap 内部所做的事完全相同),让检查在 `uds init` 执行完就立刻生效,无论 husky 有没有安装。绝不覆盖用户既有的 `core.hooksPath`,或既有的 `.git/hooks/pre-commit`——两者都会被保留原状,并打印信息说明检查**未启用**及原因。非 Node 项目的原生 hook 路径也不再无条件覆写既有的 `.git/hooks/pre-commit`(过去会)。新增 `uds check` 警告 `[pre-commit]`:检测 UDS 写入的检查文件存在,但实际不在 git 真正会执行的路径上(涵盖直接设定、husky 自己的 `<dir>/_` shim 转发、以及原生默认路径三种形状),并附上修复方式——这就是像上述三个既有采用者这样“已经中招”的项目能发现问题的渠道。此警告仅提示、不影响 `uds check --ci` 的退出码,因为这是每个 clone 各自的本机设定落差,不是标准本身不合规。`core.hooksPath` 不会进版控,所以这道启用只对执行 `uds init` 的那个 clone 生效——信息与新警告都会说明这一点。
|
|
36
|
+
|
|
37
|
+
- **后续修正(2026-09-27):上面那个修法,若既有采用者手上的 `.husky/pre-commit` 还是 husky v8 旧模板(含 `_/husky.sh` 那一行),照做反而会让每一次提交都失败。** 实测其中一个既有采用者的一次性 clone:照 `uds check` 原本建议的修法(`git config --local core.hooksPath .husky`)执行,结果打印 `.husky/pre-commit: line 2: .husky/_/husky.sh: No such file or directory`,`git commit` 以 exit 1 失败——比原本“静默不跑”的缺陷更糟,因为 `.husky/_/` 这个目录只有在 husky 自己的 bootstrap 真的跑过后才存在,而直接把 `core.hooksPath` 设成 `.husky` 会让 git 原封不动地执行这个文件。`uds init` 的 `setupHuskyHook` 现在会检测并移除这一行后再改写 `.husky/pre-commit`(其余内容——用户自己加的命令、既有的 `uds check` 那一行——全部保留);`uds check` 的 `[pre-commit]` 警告检测到这种旧模板时,不再只单独建议设定 hooksPath,改为给出“先删那一行、再设定 hooksPath”的两步修法——因为 `uds init` 对已初始化的项目会直接拒绝执行,修不了这三个既有采用者的问题。`git-hooks.js` 新增共用函数 `hasLegacyHuskyShLine`/`stripLegacyHuskyShLine`。
|
|
38
|
+
|
|
39
|
+
- **`bump-version.mjs` 在“预发布→预发布”的版本升版时,把 `SECURITY.md`“最新正式版”那一行标错——实测发生于 6.13.0-beta.2 发版当下(2026-09-26),当时以手动更正。** 它的 `SECURITY.md` 修补逻辑找“裸版号(无后缀)那一行”来认定是正式版行;新的预发布版号(如 `6.13.0-beta.2`)永远带着连字符、永远不会符合“裸版号”,于是修补逻辑退而求其次改到唯一真正裸版号的那一行——也就是不相关的正式版行——把它的版号换成新的预发布版号,而原本该更新、已经过期的预发布行(仍是 `6.13.0-beta.1`)反而原封不动。已将产生表格的逻辑(`generate-docs.mjs` 原本就写对、但 `bump-version.mjs` 未使用)抽成共用的 `scripts/lib/security-versions.mjs`,两支脚本现在都改成用 `(version, stableVersion)` 整段重新产生 2 或 3 行的表格,而不是找一行去 patch——已针对全部四种版本类型转换(正式→正式、正式→预发布、预发布→预发布、预发布→正式)、三种语言,通过对隔离副本执行一次真正的端到端 `bump-version.mjs` 验证正确。`check-version-sync.sh` 原本的 SECURITY.md 检查只比对第一行数据的版号是否等于 `package.json`(一种位置代理,恰好抓到了这次事故);现在还会逐行比对“标签”与“该行版号的形状”是否吻合(“最新正式版/Latest stable”行若版号带连字符、或“预发布版本/Pre-release”行若版号不带连字符,即使位置检查会通过,仍会被标记为错误)。
|
|
40
|
+
|
|
20
41
|
## [6.13.0-beta.2] - 2026-09-26
|
|
21
42
|
|
|
22
43
|
> **测试版** — 以 `npm install -g universal-dev-standards@beta` 安装。要测什么、已知限制、如何退回正式版:见 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
|
|
17
17
|
|
|
18
|
-
**版本**: 6.13.0-beta.
|
|
18
|
+
**版本**: 6.13.0-beta.4 (Pre-release) | **发布日期**: 2026-09-28 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
|
|
21
21
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../../docs/CLI-INIT-OPTIONS.md
|
|
3
|
-
source_version: 3.
|
|
4
|
-
translation_version: 3.
|
|
5
|
-
last_synced: 2026-09-
|
|
3
|
+
source_version: 3.7.0
|
|
4
|
+
translation_version: 3.7.0
|
|
5
|
+
last_synced: 2026-09-26
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,8 +10,8 @@ status: current
|
|
|
10
10
|
|
|
11
11
|
> **语言**: [English](../../../docs/CLI-INIT-OPTIONS.md) | [简体中文](../../zh-TW/docs/CLI-INIT-OPTIONS.md) | 简体中文
|
|
12
12
|
>
|
|
13
|
-
> **版本**: 3.
|
|
14
|
-
> **最后更新**: 2026-09-
|
|
13
|
+
> **版本**: 3.7.0
|
|
14
|
+
> **最后更新**: 2026-09-26
|
|
15
15
|
|
|
16
16
|
本文档详细说明 `uds init` 命令的每一个选项,包含使用情境、影响范围和建议选择。
|
|
17
17
|
|
|
@@ -838,6 +838,34 @@ uds init --experimental
|
|
|
838
838
|
| Claude Code 目标文件 | `--claude-target` | Claude Code 集成内容要写到哪里:`project`(`CLAUDE.md`,默认)或 `local`(`CLAUDE.local.md`) |
|
|
839
839
|
| 模式(已弃用) | `-m, --mode` | 安装模式(skills, full)- 请改用 `--skills-location` |
|
|
840
840
|
|
|
841
|
+
### 提交前标准检查(git hook 接线)
|
|
842
|
+
|
|
843
|
+
`uds init` 一律会设定“`git commit` 时跑 `uds check`”——这不是标志,只要项目
|
|
844
|
+
是 git 仓库就会执行。Node.js 项目(检测到 `package.json`)写入
|
|
845
|
+
`.husky/pre-commit`,否则写入 `.git/hooks/pre-commit`,接着会确保 git 真的
|
|
846
|
+
会执行它:设定 `git config --local core.hooksPath .husky`(非 Node 项目则
|
|
847
|
+
保留原生 `.git/hooks` 默认不动)——这与 husky 自己的 `npx husky` bootstrap
|
|
848
|
+
内部所做的事完全相同,只是直接做,让检查立刻生效,不论最后有没有装 husky。
|
|
849
|
+
|
|
850
|
+
以下两件事是刻意不做的:
|
|
851
|
+
|
|
852
|
+
- **替你安装 husky,或改动 `package.json` 的依赖。** husky 要不要作为依赖
|
|
853
|
+
由你决定;上面的接线不论有没有 husky 都能运作。若项目已经把 husky 列为
|
|
854
|
+
依赖,`uds init` 也会顺手串接它的 `prepare` script(`"prepare": "既有内容
|
|
855
|
+
&& husky"`),让未来的 `npm install` 也保持 husky 自己的 bootstrap 同步
|
|
856
|
+
——这是锦上添花,不是这道接线能否生效的关键。
|
|
857
|
+
- **覆盖你自己的 git hook 设定。** 若 `core.hooksPath` 已经指向别处,或
|
|
858
|
+
`.git/hooks/pre-commit` 已经存在,`uds init` 会两者都不动,并打印检查
|
|
859
|
+
**未启用**及原因。
|
|
860
|
+
|
|
861
|
+
`git config core.hooksPath` 是**本机、per-clone 的设定——不会进版控。**
|
|
862
|
+
跑 `uds init` 只会让“跑过这个命令的那个 clone”生效;其他人 clone 这个
|
|
863
|
+
仓库后要自己再跑一次 `uds init`(或 `uds check` 打印的那一行修复命令)。
|
|
864
|
+
`uds check` 会检测“`.husky/pre-commit`/`.git/hooks/pre-commit` 存在,但
|
|
865
|
+
实际不在 git 真正会执行的路径上”的情况——包含在这次修复之前就已采用
|
|
866
|
+
UDS 的项目——并在 `[pre-commit]` 下回报同样的修复方式;此警告不影响
|
|
867
|
+
`uds check --ci` 的退出码。
|
|
868
|
+
|
|
841
869
|
### Claude Code 以外的强制执行 Hooks
|
|
842
870
|
|
|
843
871
|
`--with-hooks` 一定会安装进 `.claude/settings.json`。四个有 hook 支持的标准
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../CHANGELOG.md
|
|
3
|
-
source_version: 6.13.0-beta.
|
|
4
|
-
translation_version: 6.13.0-beta.
|
|
5
|
-
last_synced: 2026-09-
|
|
3
|
+
source_version: 6.13.0-beta.4
|
|
4
|
+
translation_version: 6.13.0-beta.4
|
|
5
|
+
last_synced: 2026-09-27
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,6 +17,27 @@ status: current
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [6.13.0-beta.4] - 2026-09-28
|
|
21
|
+
|
|
22
|
+
> **測試版** — 以 `npm install -g universal-dev-standards@beta` 安裝。要測什麼、已知限制、如何退回正式版:見 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。注意:6.13.0-beta.3 從未上架 npm,其內容隨本版出貨。
|
|
23
|
+
|
|
24
|
+
### 修復
|
|
25
|
+
|
|
26
|
+
- **`turn-completion-integrity` 的 Stop hook 在 Windows 上一路到 6.13.0-beta.3 都靜默失效——它照跑、找不到任何語言包、然後放行每一輪對話,且什麼都不印。** `engine.mjs` 的 `loadPacks()` 用 `join(HERE, 'locales', ...)` 組出每個語言包的路徑,直接把這個檔案系統路徑交給動態 `import()`;在 Windows 上那是 `C:\...` 這種路徑,不是合法的 ESM import 指定字串(POSIX 上的絕對路徑恰好也能被解析成合法指定字串,這正是為何在 macOS/Linux 上從未被發現)。2026-09-27 於 CI(windows-latest)實測:每一個出貨的語言包都載入失敗,而每一個原本該回 `block`/`deny` 決策的介接層(Claude Code、Codex、Gemini CLI)全部回傳 `undefined`。修法改用 `pathToFileURL(...).href`,與 `cli/src/utils/standard-fixer.js`/`standard-validator.js` 既有的正確寫法一致。四支 `scripts/check-*.ts` 開發工具腳本有同樣的寫法(Windows CI job 不會跑到它們,因為它們只在 `ubuntu-latest` 上執行,但那裡同樣是壞的),一併以同樣方式修正。新增一支全 repo 走訪測試(`cli/tests/unit/scripts/no-fs-path-dynamic-import.test.js`)掃描 `scripts/`、`cli/src/`、`cli/scripts/` 找這個寫法,未來新增的一處不需要有人記得這次事故也會被擋下。
|
|
27
|
+
- **由 `uds init` 寫入的 husky 管理 `.husky/pre-commit`,即使 `core.hooksPath` 已正確接好,在 Windows 上一路到 6.13.0-beta.3 都會讓每一次提交失敗,錯誤是 `error: cannot spawn .husky/pre-commit: No such file or directory`。** husky v9 自己的範本沒有 shebang 行,這在 macOS/Linux 上一直能動,因為 POSIX git 在腳本沒有 shebang 時(`ENOEXEC`)會退回用 `/bin/sh` 執行;git for Windows 沒有這個後備機制,完全無法對沒有 shebang 的檔案 spawn,而且錯誤訊息指向 hook 檔本身而非缺少的直譯器——很容易被誤判成 wiring 問題而非內容問題。`uds init` 現在會在缺少 shebang 時,於 husky 管理的 hook 最前面補上 `#!/bin/sh`,不分平台一律如此,無論是寫新 hook 還是動到既有的採用者檔案(只會插入,絕不重寫採用者自己的 shebang 或任何其他行)。`uds check` 的 `[pre-commit]` 警告新增 `missingShebang` 訊號,獨立於 wiring 回報(一個 hook 可以完全接好但在 Windows 上仍因此失敗),且維持既有設計,只讀不寫。
|
|
28
|
+
|
|
29
|
+
## [6.13.0-beta.3] - 2026-09-27
|
|
30
|
+
|
|
31
|
+
> **測試版** — 以 `npm install -g universal-dev-standards@beta` 安裝。要測什麼、已知限制、如何退回正式版:見 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`uds init` 會寫入提交前檢查(`.husky/pre-commit`,非 Node 專案則為 `.git/hooks/pre-commit`),卻從未確認 git 真的會執行它。** 它依賴 husky 自己的 bootstrap 機制——`npm install` 觸發 husky 的 `prepare` script 去設定 `core.hooksPath`——而這個時機只在**下一次** `npm install` 執行時才會發生;如果 `node_modules` 早就存在,這件事就永遠不會發生。實測三個既有採用者(asiaostrich-telemetry-server、asiaostrich-telemetry-client、machine-setup,2026-09-26):三者都有呼叫 `npx uds check` 的 `.husky/pre-commit`,但 `core.hooksPath` 皆未設定,提交時檢查從未跑過——而且完全沒有任何錯誤訊息。`uds init` 現在不再替使用者安裝 husky 或改動 package.json 的依賴;改成直接執行 `git config --local core.hooksPath .husky`(與 husky 自己 bootstrap 內部做的事完全相同),讓檢查在 `uds init` 執行完就立刻生效,無論 husky 有沒有裝。絕不覆蓋使用者既有的 `core.hooksPath`,或既有的 `.git/hooks/pre-commit`——兩者都會被保留原狀,並印出訊息說明檢查**未啟用**及原因。非 Node 專案的原生 hook 路徑也不再無條件覆寫既有的 `.git/hooks/pre-commit`(過去會)。新增 `uds check` 警告 `[pre-commit]`:偵測 UDS 寫入的檢查檔存在,但實際不在 git 真正會執行的路徑上(涵蓋直接設定、husky 自己的 `<dir>/_` shim 轉呼叫、以及原生預設路徑三種形狀),並附上修復方式——這就是像上述三個既有採用者這樣「已經中招」的專案能發現問題的管道。此警告只提示不影響 `uds check --ci` 的結束碼,因為這是每個 clone 各自的本機設定落差,不是標準本身不合規。`core.hooksPath` 不會進版控,所以這道啟用只對執行 `uds init` 的那個 clone 生效——訊息與新警告都會說明這一點。
|
|
36
|
+
|
|
37
|
+
- **後續修正(2026-09-27):上面那個修法,若既有採用者手上的 `.husky/pre-commit` 還是 husky v8 舊範本(含 `_/husky.sh` 那一行),照做反而會讓每一次提交都失敗。** 實測其中一個既有採用者的拋棄式 clone:照 `uds check` 原本建議的修法(`git config --local core.hooksPath .husky`)執行,結果印出 `.husky/pre-commit: line 2: .husky/_/husky.sh: No such file or directory`,`git commit` 以 exit 1 失敗——比原本「靜默不跑」的缺陷更糟,因為 `.husky/_/` 這個目錄只有在 husky 自己的 bootstrap 真的跑過後才存在,而直接把 `core.hooksPath` 設成 `.husky` 會讓 git 原封不動地執行這個檔案。`uds init` 的 `setupHuskyHook` 現在會偵測並移除這一行後再改寫 `.husky/pre-commit`(其餘內容——使用者自己加的指令、既有的 `uds check` 那一行——全部保留);`uds check` 的 `[pre-commit]` 警告偵測到這種舊範本時,不再只單獨建議設定 hooksPath,改成給「先刪那一行、再設定 hooksPath」的兩步修法——因為 `uds init` 對已初始化的專案會直接拒絕執行,修不了這三個既有採用者的問題。`git-hooks.js` 新增共用函式 `hasLegacyHuskyShLine`/`stripLegacyHuskyShLine`。
|
|
38
|
+
|
|
39
|
+
- **`bump-version.mjs` 在「預發布→預發布」的版本升版時,把 `SECURITY.md`「最新正式版」那一列標錯——實測發生於 6.13.0-beta.2 發版當下(2026-09-26),當時以手動更正。** 它的 `SECURITY.md` 修補邏輯找「裸版號(無尾碼)那一列」來認定是正式版列;新的預發布版號(如 `6.13.0-beta.2`)永遠帶著連字號、永遠不會符合「裸版號」,於是修補邏輯退而求其次改到唯一真正裸版號的那一列——也就是不相關的正式版列——把它的版號換成新的預發布版號,而原本該更新、已經過期的預發布列(仍是 `6.13.0-beta.1`)反而原封不動。已將產生表格的邏輯(`generate-docs.mjs` 原本就寫對、但 `bump-version.mjs` 未使用)抽成共用的 `scripts/lib/security-versions.mjs`,兩支腳本現在都改成用 `(version, stableVersion)` 整段重新產生 2 或 3 列的表格,而不是找一列去 patch——已針對全部四種版本型態轉換(正式→正式、正式→預發布、預發布→預發布、預發布→正式)、三種語言,透過對隔離複本執行一次真正的端到端 `bump-version.mjs` 驗證正確。`check-version-sync.sh` 原本的 SECURITY.md 檢查只比對第一列資料的版號是否等於 `package.json`(一種位置代理,恰好抓到了這次事故);現在還會逐列比對「標籤」與「該列版號的形狀」是否吻合(「最新正式版/Latest stable」列若版號帶連字號、或「預發布版本/Pre-release」列若版號不帶連字號,即使位置檢查會通過,仍會被標記為錯誤)。
|
|
40
|
+
|
|
20
41
|
## [6.13.0-beta.2] - 2026-09-26
|
|
21
42
|
|
|
22
43
|
> **測試版** — 以 `npm install -g universal-dev-standards@beta` 安裝。要測什麼、已知限制、如何退回正式版:見 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。
|
|
@@ -15,7 +15,7 @@ status: current
|
|
|
15
15
|
|
|
16
16
|
> **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
|
|
17
17
|
|
|
18
|
-
**版本**: 6.13.0-beta.
|
|
18
|
+
**版本**: 6.13.0-beta.4 (Pre-release) | **發布日期**: 2026-09-28 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
|
|
19
19
|
|
|
20
20
|
語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
|
|
21
21
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: ../../../docs/CLI-INIT-OPTIONS.md
|
|
3
|
-
source_version: 3.
|
|
4
|
-
translation_version: 3.
|
|
5
|
-
last_synced: 2026-09-
|
|
3
|
+
source_version: 3.7.0
|
|
4
|
+
translation_version: 3.7.0
|
|
5
|
+
last_synced: 2026-09-26
|
|
6
6
|
status: current
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,8 +10,8 @@ status: current
|
|
|
10
10
|
|
|
11
11
|
> **語言**: [English](../../../docs/CLI-INIT-OPTIONS.md) | 繁體中文 | [简体中文](../../zh-CN/docs/CLI-INIT-OPTIONS.md)
|
|
12
12
|
>
|
|
13
|
-
> **版本**: 3.
|
|
14
|
-
> **最後更新**: 2026-09-
|
|
13
|
+
> **版本**: 3.7.0
|
|
14
|
+
> **最後更新**: 2026-09-26
|
|
15
15
|
|
|
16
16
|
本文件詳細說明 `uds init` 命令的每一個選項,包含使用情境、影響範圍和建議選擇。
|
|
17
17
|
|
|
@@ -838,6 +838,34 @@ uds init --experimental
|
|
|
838
838
|
| Claude Code 目標檔 | `--claude-target` | Claude Code 整合內容要寫到哪裡:`project`(`CLAUDE.md`,預設)或 `local`(`CLAUDE.local.md`) |
|
|
839
839
|
| 模式(已棄用) | `-m, --mode` | 安裝模式(skills, full)- 請改用 `--skills-location` |
|
|
840
840
|
|
|
841
|
+
### 提交前標準檢查(git hook 接線)
|
|
842
|
+
|
|
843
|
+
`uds init`一律會設定「`git commit` 時跑 `uds check`」——這不是旗標,只要專案是
|
|
844
|
+
git repo 就會執行。Node.js 專案(偵測到 `package.json`)寫入
|
|
845
|
+
`.husky/pre-commit`,否則寫入 `.git/hooks/pre-commit`,接著會確保 git 真的會
|
|
846
|
+
執行它:設定 `git config --local core.hooksPath .husky`(非 Node 專案則保留
|
|
847
|
+
原生 `.git/hooks` 預設不動)——這與 husky 自己的 `npx husky` bootstrap 內部
|
|
848
|
+
做的事完全相同,只是直接做,讓檢查立刻生效,不論最後有沒有裝 husky。
|
|
849
|
+
|
|
850
|
+
以下兩件事是刻意不做的:
|
|
851
|
+
|
|
852
|
+
- **替你安裝 husky,或動 `package.json` 的依賴。** husky 要不要是依賴由你
|
|
853
|
+
決定;上面的接線不論有沒有 husky 都能運作。若專案已經把 husky 列為依賴,
|
|
854
|
+
`uds init` 也會順手串接它的 `prepare` script(`"prepare": "既有內容 &&
|
|
855
|
+
husky"`),讓未來的 `npm install` 也保持 husky 自己的 bootstrap 同步——
|
|
856
|
+
這是錦上添花,不是這道接線能不能生效的關鍵。
|
|
857
|
+
- **覆蓋你自己的 git hook 設定。** 若 `core.hooksPath` 已經指向別處,或
|
|
858
|
+
`.git/hooks/pre-commit` 已經存在,`uds init` 會兩者都不動,並印出檢查
|
|
859
|
+
**未啟用**及原因。
|
|
860
|
+
|
|
861
|
+
`git config core.hooksPath` 是**本機、per-clone 的設定——不會進版控。**
|
|
862
|
+
跑 `uds init` 只會讓「跑過這個指令的那個 clone」生效;其他人 clone 這個
|
|
863
|
+
repo 後要自己再跑一次 `uds init`(或 `uds check` 印出的那一行修復指令)。
|
|
864
|
+
`uds check` 會偵測「`.husky/pre-commit`/`.git/hooks/pre-commit` 存在,
|
|
865
|
+
但實際不在 git 真正會執行的路徑上」的情況——包含在這個修正之前就已採用
|
|
866
|
+
UDS 的專案——並在 `[pre-commit]` 底下回報同樣的修復方式;此警告不影響
|
|
867
|
+
`uds check --ci` 的結束碼。
|
|
868
|
+
|
|
841
869
|
### Claude Code 以外的強制執行 Hooks
|
|
842
870
|
|
|
843
871
|
`--with-hooks` 一定會安裝進 `.claude/settings.json`。四個有 hook 支援的標準
|
package/package.json
CHANGED
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
|
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 状态摘要',
|
|
@@ -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.4",
|
|
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.4"
|
|
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.4",
|
|
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.4",
|
|
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.4",
|
|
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.4",
|
|
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.4",
|
|
2335
2335
|
"source": {
|
|
2336
2336
|
"human": "core/resource-cost-boundary.md",
|
|
2337
2337
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|