kld-sdd 2.6.21 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kld-sdd-init.js +3 -9
- package/lib/init.js +20 -57
- package/package.json +2 -2
- package/skywalk-sdd/index.cjs +13 -0
- package/skywalk-sdd/lib/git-identity.cjs +270 -0
- package/skywalk-sdd/lib/usage-contract.cjs +10 -79
- package/skywalk-sdd/lib/usage-reporter.cjs +210 -104
- package/skywalk-sdd/lib/user-config.cjs +21 -46
- package/skywalk-sdd/ontology/artifact-parser.cjs +1 -2
- package/templates/git-hooks/commit-msg +58 -15
- package/templates/git-hooks/hooks.config +19 -0
- package/templates/git-hooks/pre-commit +45 -11
- package/templates/git-hooks/pre-push +53 -11
- package/templates/git-hooks/pre-push-consistency-check.cjs +112 -2
- package/lib/device-auth-cli.js +0 -130
- package/lib/device-auth.js +0 -345
- package/lib/init-account-binding.js +0 -108
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# KLD SDD Pre-Commit Hook
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
|
|
4
|
+
# 删除某行 = 禁用该插件
|
|
5
5
|
# marker: KLD SDD quality gate
|
|
6
6
|
|
|
7
7
|
# 从 hook 所在目录推算 git 根目录
|
|
@@ -38,20 +38,54 @@ if [ -z "$hooks_dir" ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
if [ -z "$hooks_dir" ]; then
|
|
41
|
-
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit"
|
|
42
42
|
exit 0
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
45
|
echo "[SDD] 执行提交前检查..."
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
# 从配置文件读取插件列表
|
|
48
|
+
config_file="$hooks_dir/hooks.config"
|
|
49
|
+
if [ ! -f "$config_file" ]; then
|
|
50
|
+
echo "[SDD] ️ 配置文件不存在:hooks.config,使用默认插件"
|
|
51
|
+
SCRIPTS="pre-commit-sdd-check.cjs pre-commit-consistency-check.cjs"
|
|
52
|
+
else
|
|
53
|
+
# 读取 [pre-commit] 段落的配置
|
|
54
|
+
in_section=0
|
|
55
|
+
SCRIPTS=""
|
|
56
|
+
while IFS= read -r line; do
|
|
57
|
+
line=$(printf '%s' "$line" | tr -d '\r')
|
|
58
|
+
case "$line" in
|
|
59
|
+
"[pre-commit]") in_section=1 ;;
|
|
60
|
+
"["*) in_section=0 ;;
|
|
61
|
+
"#"*|"") ;;
|
|
62
|
+
*)
|
|
63
|
+
if [ "$in_section" -eq 1 ]; then
|
|
64
|
+
SCRIPTS="$SCRIPTS $line"
|
|
65
|
+
fi
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
done < "$config_file"
|
|
50
69
|
fi
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
executed=0
|
|
72
|
+
failed=0
|
|
73
|
+
for script_name in $SCRIPTS; do
|
|
74
|
+
script="$hooks_dir/$script_name"
|
|
75
|
+
if [ ! -f "$script" ]; then
|
|
76
|
+
echo "[SDD] ⚠️ 插件不存在:$script_name"
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
executed=$((executed + 1))
|
|
80
|
+
echo "[SDD] 执行 pre-commit 插件:$script_name"
|
|
81
|
+
node "$script" --project="$git_root" "$@" || failed=$((failed + 1))
|
|
82
|
+
done
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
if [ "$executed" -eq 0 ]; then
|
|
85
|
+
echo "[SDD] 无 pre-commit 插件,跳过"
|
|
86
|
+
elif [ "$failed" -gt 0 ]; then
|
|
87
|
+
echo "[SDD] ❌ 共执行 $executed 个 pre-commit 插件,$failed 个失败"
|
|
88
|
+
exit 1
|
|
89
|
+
else
|
|
90
|
+
echo "[SDD] ✅ 共执行 $executed 个 pre-commit 插件,全部通过"
|
|
91
|
+
fi
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# KLD SDD Pre-Push Hook
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
|
|
4
|
+
# 删除某行 = 禁用该插件
|
|
5
5
|
# marker: KLD SDD quality gate
|
|
6
6
|
|
|
7
7
|
# 从 hook 所在目录推算 git 根目录
|
|
@@ -38,20 +38,62 @@ if [ -z "$hooks_dir" ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
if [ -z "$hooks_dir" ]; then
|
|
41
|
-
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-push
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-push"
|
|
42
42
|
exit 0
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
45
|
echo "[SDD] 执行推送前检查..."
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
# 保存 pre-push stdin(Git 标准输入:local_ref local_sha remote_ref remote_sha)
|
|
48
|
+
# 供下游 .cjs 读取本次 push 的 commit 范围
|
|
49
|
+
SDD_PRE_PUSH_REFS=""
|
|
50
|
+
while IFS= read -r line; do
|
|
51
|
+
SDD_PRE_PUSH_REFS="${SDD_PRE_PUSH_REFS}${line}\n"
|
|
52
|
+
done
|
|
53
|
+
export SDD_PRE_PUSH_REFS
|
|
51
54
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
# 从配置文件读取插件列表
|
|
56
|
+
config_file="$hooks_dir/hooks.config"
|
|
57
|
+
if [ ! -f "$config_file" ]; then
|
|
58
|
+
echo "[SDD] ⚠️ 配置文件不存在:hooks.config,使用默认插件"
|
|
59
|
+
SCRIPTS="pre-push-sdd-check.cjs pre-push-consistency-check.cjs"
|
|
60
|
+
else
|
|
61
|
+
# 读取 [pre-push] 段落的配置
|
|
62
|
+
in_section=0
|
|
63
|
+
SCRIPTS=""
|
|
64
|
+
while IFS= read -r line; do
|
|
65
|
+
line=$(printf '%s' "$line" | tr -d '\r')
|
|
66
|
+
case "$line" in
|
|
67
|
+
"[pre-push]") in_section=1 ;;
|
|
68
|
+
"["*) in_section=0 ;;
|
|
69
|
+
"#"*|"") ;;
|
|
70
|
+
*)
|
|
71
|
+
if [ "$in_section" -eq 1 ]; then
|
|
72
|
+
SCRIPTS="$SCRIPTS $line"
|
|
73
|
+
fi
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done < "$config_file"
|
|
55
77
|
fi
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
executed=0
|
|
80
|
+
failed=0
|
|
81
|
+
for script_name in $SCRIPTS; do
|
|
82
|
+
script="$hooks_dir/$script_name"
|
|
83
|
+
if [ ! -f "$script" ]; then
|
|
84
|
+
echo "[SDD] ⚠️ 插件不存在:$script_name"
|
|
85
|
+
continue
|
|
86
|
+
fi
|
|
87
|
+
executed=$((executed + 1))
|
|
88
|
+
echo "[SDD] 执行 pre-push 插件:$script_name"
|
|
89
|
+
node "$script" --project="$git_root" "$@" || failed=$((failed + 1))
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
if [ "$executed" -eq 0 ]; then
|
|
93
|
+
echo "[SDD] 无 pre-push 插件,跳过"
|
|
94
|
+
elif [ "$failed" -gt 0 ]; then
|
|
95
|
+
echo "[SDD] ❌ 共执行 $executed 个 pre-push 插件,$failed 个失败"
|
|
96
|
+
exit 1
|
|
97
|
+
else
|
|
98
|
+
echo "[SDD] ✅ 共执行 $executed 个 pre-push 插件,全部通过"
|
|
99
|
+
fi
|
|
@@ -39,6 +39,101 @@ function discoverActiveChanges(specRoot) {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// --- Push 范围收敛 ---
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 读取 pre-push 引用范围。
|
|
46
|
+
* 优先从 shell hook 通过环境变量 SDD_PRE_PUSH_REFS 传入(shell hook 已消费 stdin),
|
|
47
|
+
* 降级时尝试直接读 stdin。
|
|
48
|
+
*/
|
|
49
|
+
function readPushRefLines() {
|
|
50
|
+
const envRefs = process.env.SDD_PRE_PUSH_REFS;
|
|
51
|
+
if (envRefs) {
|
|
52
|
+
// shell hook 用 \n 字面量拼接,也可能含真实换行符
|
|
53
|
+
return envRefs.split(/\\n|\n/).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (process.stdin.isTTY) return [];
|
|
58
|
+
const buffer = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
59
|
+
return buffer.split(/\r?\n/).filter(Boolean);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 从 pre-push stdin 行解析 commit 范围,获取本次 push 实际变更的文件列表
|
|
67
|
+
*/
|
|
68
|
+
function getPushRangeFiles(projectRoot) {
|
|
69
|
+
const lines = readPushRefLines();
|
|
70
|
+
if (lines.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
const files = new Set();
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const parts = line.split(/\s+/);
|
|
75
|
+
if (parts.length < 4) continue;
|
|
76
|
+
const localSha = parts[1];
|
|
77
|
+
const remoteSha = parts[3];
|
|
78
|
+
|
|
79
|
+
// 删除分支:localSha 为全 0
|
|
80
|
+
if (localSha === '0000000000000000000000000000000000000000') continue;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
let diffFiles;
|
|
84
|
+
if (remoteSha === '0000000000000000000000000000000000000000') {
|
|
85
|
+
// 新建分支:用 git show 获取该 commit 引入的文件
|
|
86
|
+
diffFiles = execFileSync('git', ['show', '--name-only', '--pretty=format:', localSha], {
|
|
87
|
+
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
88
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
89
|
+
} else {
|
|
90
|
+
// 已有分支:用 git diff 获取范围变更
|
|
91
|
+
diffFiles = execFileSync('git', ['diff', `${remoteSha}..${localSha}`, '--name-only'], {
|
|
92
|
+
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
93
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
for (const f of diffFiles) files.add(f.replace(/\\/g, '/'));
|
|
96
|
+
} catch {
|
|
97
|
+
// 单个 ref 失败不阻断,继续处理其他 ref
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return files.size > 0 ? Array.from(files) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizePath(filePath) {
|
|
105
|
+
return filePath.replace(/\\/g, '/');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getChangeNameFromPath(filePath) {
|
|
109
|
+
const normalized = normalizePath(filePath);
|
|
110
|
+
const match = normalized.match(/^openspec\/changes\/([^/]+)\//);
|
|
111
|
+
return match ? match[1] : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 从 push 的变更文件列表中提取涉及的 change name
|
|
116
|
+
*/
|
|
117
|
+
function discoverChangesFromPushFiles(projectRoot, specRoot) {
|
|
118
|
+
const pushFiles = getPushRangeFiles(projectRoot);
|
|
119
|
+
if (!pushFiles) return null; // 降级信号
|
|
120
|
+
|
|
121
|
+
const changes = new Set();
|
|
122
|
+
for (const file of pushFiles) {
|
|
123
|
+
const changeName = getChangeNameFromPath(file);
|
|
124
|
+
if (changeName) changes.add(changeName);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (changes.size === 0) {
|
|
128
|
+
console.log('[SDD push-consistency-check] 本次 push 不涉及 openspec 变更,跳过');
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 过滤掉已归档的 change
|
|
133
|
+
const activeChanges = new Set(discoverActiveChanges(specRoot));
|
|
134
|
+
return Array.from(changes).filter(cn => activeChanges.has(cn));
|
|
135
|
+
}
|
|
136
|
+
|
|
42
137
|
// --- 报告查找与解析 ---
|
|
43
138
|
|
|
44
139
|
function findReport(specRoot, changeName, type) {
|
|
@@ -259,13 +354,28 @@ function main() {
|
|
|
259
354
|
|
|
260
355
|
const specRoot = resolveSpecRoot();
|
|
261
356
|
|
|
262
|
-
|
|
357
|
+
// 优先尝试按本次 push 范围收敛变更
|
|
358
|
+
let changes = discoverChangesFromPushFiles(projectRoot, specRoot);
|
|
359
|
+
let modifiedFiles = null;
|
|
360
|
+
|
|
361
|
+
if (changes === null) {
|
|
362
|
+
// 降级:stdin 读取失败,回退到现有行为
|
|
363
|
+
console.log('[SDD push-consistency-check] 无法读取 push 范围,降级为全量检查');
|
|
364
|
+
changes = discoverActiveChanges(specRoot);
|
|
365
|
+
modifiedFiles = getModifiedFiles(projectRoot);
|
|
366
|
+
} else if (changes.length === 0) {
|
|
367
|
+
console.log('[SDD push-consistency-check] 无相关活跃变更,跳过');
|
|
368
|
+
process.exit(0);
|
|
369
|
+
} else {
|
|
370
|
+
// 收敛成功:用 push 范围文件做过期判定
|
|
371
|
+
modifiedFiles = getPushRangeFiles(projectRoot) || [];
|
|
372
|
+
}
|
|
373
|
+
|
|
263
374
|
if (!changes.length) {
|
|
264
375
|
console.log('[SDD push-consistency-check] 无活跃变更,跳过');
|
|
265
376
|
process.exit(0);
|
|
266
377
|
}
|
|
267
378
|
|
|
268
|
-
const modifiedFiles = getModifiedFiles(projectRoot);
|
|
269
379
|
const missing = [];
|
|
270
380
|
const stale = [];
|
|
271
381
|
const needsConfirmation = [];
|
package/lib/device-auth-cli.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// kld-T03 — auth 子命令 CLI 薄层
|
|
2
|
-
// 仅做参数解析、交互提示与退出码;业务逻辑在 lib/device-auth.js + skywalk-sdd/lib/user-config.cjs
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const readline = require('node:readline');
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
resolveServer,
|
|
9
|
-
runDeviceFlow,
|
|
10
|
-
DEFAULT_SERVER_URL,
|
|
11
|
-
} = require('./device-auth.js');
|
|
12
|
-
const {
|
|
13
|
-
readUserConfig,
|
|
14
|
-
clearUserToken,
|
|
15
|
-
redactToken,
|
|
16
|
-
USER_CONFIG_VERSION,
|
|
17
|
-
} = require('../skywalk-sdd/lib/user-config.cjs');
|
|
18
|
-
const { normalizeServerUrl } = require('../skywalk-sdd/lib/usage-contract.cjs');
|
|
19
|
-
|
|
20
|
-
async function ask(rl, question) {
|
|
21
|
-
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function cmdStatus() {
|
|
25
|
-
const cfg = readUserConfig();
|
|
26
|
-
if (!cfg || !cfg.token) {
|
|
27
|
-
console.log('未绑定平台账号。');
|
|
28
|
-
console.log('使用 `kld-sdd auth login` 完成 Device Flow 绑定。');
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
console.log('已绑定平台账号:');
|
|
32
|
-
console.log(` server: ${cfg.server || '(未设置)'}`);
|
|
33
|
-
console.log(` userName: ${cfg.userName || '(未知)'}`);
|
|
34
|
-
console.log(` boundAt: ${cfg.boundAt || '(未知)'}`);
|
|
35
|
-
console.log(` token: ${redactToken(cfg.token)}`);
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function cmdLogout() {
|
|
40
|
-
const removed = clearUserToken();
|
|
41
|
-
if (removed) {
|
|
42
|
-
console.log('✅ 已清除本地绑定(其他配置保留)。');
|
|
43
|
-
} else {
|
|
44
|
-
console.log('未检测到已绑定 token。');
|
|
45
|
-
}
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function cmdLogin(args) {
|
|
50
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
51
|
-
try {
|
|
52
|
-
// 解析 server:已有绑定 → env → 交互输入
|
|
53
|
-
let server = resolveServer();
|
|
54
|
-
const forceArg = args.find((a) => a.startsWith('--server='));
|
|
55
|
-
if (forceArg) {
|
|
56
|
-
const normalized = normalizeServerUrl(forceArg.slice('--server='.length));
|
|
57
|
-
if (!normalized) {
|
|
58
|
-
console.error('❌ --server 必须是 http/https URL');
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
server = normalized;
|
|
62
|
-
}
|
|
63
|
-
if (!server) {
|
|
64
|
-
const answer = await ask(rl, `请输入平台地址(http/https,回车默认 ${DEFAULT_SERVER_URL}):`);
|
|
65
|
-
const normalized = normalizeServerUrl(answer || DEFAULT_SERVER_URL);
|
|
66
|
-
if (!normalized) {
|
|
67
|
-
console.error('❌ 非法地址,必须 http/https');
|
|
68
|
-
return 1;
|
|
69
|
-
}
|
|
70
|
-
server = normalized;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const existing = readUserConfig();
|
|
74
|
-
if (existing && existing.token && existing.server === server) {
|
|
75
|
-
const answer = await ask(rl, `已绑定 ${server}(userName=${existing.userName || '未知'}),是否更新绑定?[y/N] `);
|
|
76
|
-
if (!/^[yY]/.test(answer)) {
|
|
77
|
-
console.log('保留原有绑定。');
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const pkg = require('../package.json');
|
|
83
|
-
console.log(`正在通过 Device Flow 绑定到 ${server} ...`);
|
|
84
|
-
const ac = new AbortController();
|
|
85
|
-
const sigint = () => { ac.abort(); };
|
|
86
|
-
process.on('SIGINT', sigint);
|
|
87
|
-
try {
|
|
88
|
-
const result = await runDeviceFlow({
|
|
89
|
-
server,
|
|
90
|
-
clientName: 'kld-sdd',
|
|
91
|
-
clientVersion: pkg && pkg.version ? String(pkg.version) : 'unknown',
|
|
92
|
-
onPrint: (msg) => console.log(msg),
|
|
93
|
-
signal: ac.signal,
|
|
94
|
-
});
|
|
95
|
-
if (result.status === 'bound') {
|
|
96
|
-
console.log('✅ 绑定成功。');
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
if (result.status === 'aborted') {
|
|
100
|
-
console.log('已取消。');
|
|
101
|
-
return 130;
|
|
102
|
-
}
|
|
103
|
-
if (result.status === 'expired') {
|
|
104
|
-
console.log('⏱ 授权已过期,请重试。');
|
|
105
|
-
return 1;
|
|
106
|
-
}
|
|
107
|
-
if (result.status === 'denied') {
|
|
108
|
-
console.log('已拒绝授权。');
|
|
109
|
-
return 1;
|
|
110
|
-
}
|
|
111
|
-
console.error(`❌ 绑定失败:${result.reason || 'unknown'}`);
|
|
112
|
-
return 1;
|
|
113
|
-
} finally {
|
|
114
|
-
process.removeListener('SIGINT', sigint);
|
|
115
|
-
}
|
|
116
|
-
} finally {
|
|
117
|
-
rl.close();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function runAuthCommand(sub, args) {
|
|
122
|
-
if (sub === 'status') return cmdStatus(args);
|
|
123
|
-
if (sub === 'login') return cmdLogin(args);
|
|
124
|
-
if (sub === 'logout') return cmdLogout(args);
|
|
125
|
-
console.error(`未知 auth 子命令:${sub}`);
|
|
126
|
-
console.error('用法: kld-sdd auth [status|login|logout] [--server=<url>]');
|
|
127
|
-
return 2;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
module.exports = { runAuthCommand };
|