@brawnen/agent-harness-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +224 -0
- package/README.zh-CN.md +232 -0
- package/bin/agent-harness.js +6 -0
- package/package.json +46 -0
- package/src/commands/audit.js +110 -0
- package/src/commands/delivery.js +497 -0
- package/src/commands/docs.js +251 -0
- package/src/commands/gate.js +236 -0
- package/src/commands/init.js +711 -0
- package/src/commands/report.js +272 -0
- package/src/commands/state.js +274 -0
- package/src/commands/status.js +493 -0
- package/src/commands/task.js +316 -0
- package/src/commands/verify.js +173 -0
- package/src/index.js +101 -0
- package/src/lib/audit-store.js +80 -0
- package/src/lib/delivery-policy.js +219 -0
- package/src/lib/output-policy.js +266 -0
- package/src/lib/project-config.js +235 -0
- package/src/lib/runtime-paths.js +46 -0
- package/src/lib/state-store.js +510 -0
- package/src/lib/task-core.js +490 -0
- package/src/lib/workflow-policy.js +307 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { evaluateTaskDeliveryReadiness, normalizeDeliveryPolicy } from "../lib/delivery-policy.js";
|
|
6
|
+
import { normalizeOutputPolicy } from "../lib/output-policy.js";
|
|
7
|
+
import { loadProjectConfig } from "../lib/project-config.js";
|
|
8
|
+
import { runtimeRelativeCandidates } from "../lib/runtime-paths.js";
|
|
9
|
+
import { requireTaskState, resolveTaskId } from "../lib/state-store.js";
|
|
10
|
+
import { buildWorkflowWarning, evaluateTaskWorkflowDecision, normalizeWorkflowPolicy } from "../lib/workflow-policy.js";
|
|
11
|
+
|
|
12
|
+
const VALID_ACTIONS = new Set(["commit", "push"]);
|
|
13
|
+
|
|
14
|
+
export function runDelivery(argv) {
|
|
15
|
+
const [subcommand, ...rest] = argv;
|
|
16
|
+
|
|
17
|
+
if (!subcommand) {
|
|
18
|
+
console.error("缺少 delivery 子命令。可用: ready, request, commit");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (subcommand === "ready") {
|
|
23
|
+
return runDeliveryReady(rest);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (subcommand === "request") {
|
|
27
|
+
return runDeliveryRequest(rest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (subcommand === "commit") {
|
|
31
|
+
return runDeliveryCommit(rest);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.error(`未知 delivery 子命令: ${subcommand}。可用: ready, request, commit`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runDeliveryReady(argv) {
|
|
39
|
+
const parsed = parseTaskIdArgs(argv);
|
|
40
|
+
if (!parsed.ok) {
|
|
41
|
+
console.error(parsed.error);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const taskId = resolveTaskId(cwd, parsed.options.taskId);
|
|
48
|
+
const taskState = requireTaskState(cwd, taskId);
|
|
49
|
+
const readiness = buildDeliveryReadiness(cwd, taskState);
|
|
50
|
+
const workflowDecision = buildWorkflowDecision(cwd, taskState);
|
|
51
|
+
|
|
52
|
+
printJson({
|
|
53
|
+
task_id: taskId,
|
|
54
|
+
delivery_readiness: readiness,
|
|
55
|
+
workflow_decision: workflowDecision,
|
|
56
|
+
workflow_warning: buildWorkflowWarning(workflowDecision)
|
|
57
|
+
});
|
|
58
|
+
return 0;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error.message);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runDeliveryRequest(argv) {
|
|
66
|
+
const parsed = parseDeliveryRequestArgs(argv);
|
|
67
|
+
if (!parsed.ok) {
|
|
68
|
+
console.error(parsed.error);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
const taskId = resolveTaskId(cwd, parsed.options.taskId);
|
|
75
|
+
const taskState = requireTaskState(cwd, taskId);
|
|
76
|
+
const readiness = buildDeliveryReadiness(cwd, taskState);
|
|
77
|
+
const actionReadiness = readiness[parsed.options.action];
|
|
78
|
+
const workflowDecision = buildWorkflowDecision(cwd, taskState);
|
|
79
|
+
|
|
80
|
+
const result = {
|
|
81
|
+
task_id: taskId,
|
|
82
|
+
action: parsed.options.action,
|
|
83
|
+
allowed: actionReadiness?.ready === true,
|
|
84
|
+
via: actionReadiness?.via ?? null,
|
|
85
|
+
delivery_readiness: readiness,
|
|
86
|
+
workflow_decision: workflowDecision,
|
|
87
|
+
workflow_warning: buildWorkflowWarning(workflowDecision),
|
|
88
|
+
requested_at: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
printJson(result);
|
|
92
|
+
return result.allowed ? 0 : 1;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(error.message);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function runDeliveryCommit(argv) {
|
|
100
|
+
const parsed = parseDeliveryCommitArgs(argv);
|
|
101
|
+
if (!parsed.ok) {
|
|
102
|
+
console.error(parsed.error);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
ensureGitRepository(cwd);
|
|
109
|
+
const taskId = resolveTaskId(cwd, parsed.options.taskId);
|
|
110
|
+
const taskState = requireTaskState(cwd, taskId);
|
|
111
|
+
const readiness = buildDeliveryReadiness(cwd, taskState);
|
|
112
|
+
const actionReadiness = readiness.commit;
|
|
113
|
+
const workflowDecision = buildWorkflowDecision(cwd, taskState);
|
|
114
|
+
|
|
115
|
+
if (actionReadiness?.ready !== true) {
|
|
116
|
+
printJson({
|
|
117
|
+
task_id: taskId,
|
|
118
|
+
action: "commit",
|
|
119
|
+
allowed: false,
|
|
120
|
+
via: actionReadiness?.via ?? null,
|
|
121
|
+
delivery_readiness: readiness,
|
|
122
|
+
workflow_decision: workflowDecision,
|
|
123
|
+
workflow_warning: buildWorkflowWarning(workflowDecision),
|
|
124
|
+
requested_at: new Date().toISOString()
|
|
125
|
+
});
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const report = loadTaskReport(cwd, taskId);
|
|
130
|
+
const commitPlan = buildCommitPlan(cwd, report, parsed.options);
|
|
131
|
+
if (commitPlan.paths.length === 0) {
|
|
132
|
+
throw new Error("未能从报告中推导出可提交文件,请先补充 report actual_scope 或 output_artifacts");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const commitMessage = parsed.options.message ?? buildCommitMessage(report);
|
|
136
|
+
if (commitPlan.wide_scope.length > 0 && !parsed.options.forceWideScope) {
|
|
137
|
+
const result = {
|
|
138
|
+
task_id: taskId,
|
|
139
|
+
action: "commit",
|
|
140
|
+
allowed: false,
|
|
141
|
+
via: actionReadiness.via ?? null,
|
|
142
|
+
dry_run: parsed.options.dryRun,
|
|
143
|
+
commit_message: commitMessage,
|
|
144
|
+
staged_paths: commitPlan.paths,
|
|
145
|
+
wide_scope: commitPlan.wide_scope,
|
|
146
|
+
reason: `检测到过宽 scope,需显式使用 --force-wide-scope: ${commitPlan.wide_scope.join(", ")}`,
|
|
147
|
+
delivery_readiness: readiness,
|
|
148
|
+
workflow_decision: workflowDecision,
|
|
149
|
+
workflow_warning: buildWorkflowWarning(workflowDecision),
|
|
150
|
+
requested_at: new Date().toISOString()
|
|
151
|
+
};
|
|
152
|
+
printJson(result);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parsed.options.dryRun) {
|
|
157
|
+
printJson({
|
|
158
|
+
task_id: taskId,
|
|
159
|
+
action: "commit",
|
|
160
|
+
allowed: true,
|
|
161
|
+
via: actionReadiness.via ?? null,
|
|
162
|
+
dry_run: true,
|
|
163
|
+
commit_message: commitMessage,
|
|
164
|
+
staged_paths: commitPlan.paths,
|
|
165
|
+
wide_scope: commitPlan.wide_scope,
|
|
166
|
+
delivery_readiness: readiness,
|
|
167
|
+
workflow_decision: workflowDecision,
|
|
168
|
+
workflow_warning: buildWorkflowWarning(workflowDecision),
|
|
169
|
+
requested_at: new Date().toISOString()
|
|
170
|
+
});
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
stageCommitPaths(cwd, commitPlan.paths);
|
|
175
|
+
runGit(cwd, ["commit", "-m", commitMessage]);
|
|
176
|
+
|
|
177
|
+
const result = {
|
|
178
|
+
task_id: taskId,
|
|
179
|
+
action: "commit",
|
|
180
|
+
allowed: true,
|
|
181
|
+
via: actionReadiness.via ?? null,
|
|
182
|
+
commit_message: commitMessage,
|
|
183
|
+
staged_paths: commitPlan.paths,
|
|
184
|
+
wide_scope: commitPlan.wide_scope,
|
|
185
|
+
commit_sha: getHeadSha(cwd),
|
|
186
|
+
delivery_readiness: readiness,
|
|
187
|
+
workflow_decision: workflowDecision,
|
|
188
|
+
workflow_warning: buildWorkflowWarning(workflowDecision),
|
|
189
|
+
requested_at: new Date().toISOString()
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
printJson(result);
|
|
193
|
+
return 0;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(error.message);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildDeliveryReadiness(cwd, taskState) {
|
|
201
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
202
|
+
return evaluateTaskDeliveryReadiness(cwd, taskState, {
|
|
203
|
+
deliveryPolicy: normalizeDeliveryPolicy(projectConfig?.delivery_policy),
|
|
204
|
+
reportPolicy: normalizeOutputPolicy(projectConfig?.output_policy).report
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildWorkflowDecision(cwd, taskState) {
|
|
209
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
210
|
+
const normalizedPolicy = normalizeWorkflowPolicy(projectConfig?.workflow_policy);
|
|
211
|
+
if (taskState?.workflow_decision && typeof taskState.workflow_decision === "object") {
|
|
212
|
+
if (taskState.workflow_decision.enforcement_mode === normalizedPolicy.enforcement.mode) {
|
|
213
|
+
return taskState.workflow_decision;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return evaluateTaskWorkflowDecision(taskState, {
|
|
218
|
+
workflowPolicy: normalizedPolicy,
|
|
219
|
+
outputPolicy: normalizeOutputPolicy(projectConfig?.output_policy),
|
|
220
|
+
previousDecision: taskState.workflow_decision
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseDeliveryCommitArgs(argv) {
|
|
225
|
+
const options = {
|
|
226
|
+
dryRun: false,
|
|
227
|
+
forceWideScope: false,
|
|
228
|
+
message: null,
|
|
229
|
+
taskId: null
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
233
|
+
const arg = argv[index];
|
|
234
|
+
if (arg === "--dry-run") {
|
|
235
|
+
options.dryRun = true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (arg === "--force-wide-scope") {
|
|
239
|
+
options.forceWideScope = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (arg === "--task-id") {
|
|
243
|
+
options.taskId = argv[index + 1] ?? null;
|
|
244
|
+
index += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg === "--message") {
|
|
248
|
+
options.message = argv[index + 1] ?? null;
|
|
249
|
+
index += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { ok: false, error: `未知参数: ${arg}` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { ok: true, options };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseTaskIdArgs(argv) {
|
|
260
|
+
const options = { taskId: null };
|
|
261
|
+
|
|
262
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
263
|
+
const arg = argv[index];
|
|
264
|
+
if (arg === "--task-id") {
|
|
265
|
+
options.taskId = argv[index + 1] ?? null;
|
|
266
|
+
index += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { ok: false, error: `未知参数: ${arg}` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { ok: true, options };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseDeliveryRequestArgs(argv) {
|
|
277
|
+
const options = {
|
|
278
|
+
action: null,
|
|
279
|
+
taskId: null
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
283
|
+
const arg = argv[index];
|
|
284
|
+
if (arg === "--task-id") {
|
|
285
|
+
options.taskId = argv[index + 1] ?? null;
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (arg === "--action") {
|
|
290
|
+
const value = argv[index + 1] ?? null;
|
|
291
|
+
if (!VALID_ACTIONS.has(value)) {
|
|
292
|
+
return { ok: false, error: "无效的 --action 参数。可选值: commit, push" };
|
|
293
|
+
}
|
|
294
|
+
options.action = value;
|
|
295
|
+
index += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { ok: false, error: `未知参数: ${arg}` };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!options.action) {
|
|
303
|
+
return { ok: false, error: "需要 --action 参数。可选值: commit, push" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { ok: true, options };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function printJson(value) {
|
|
310
|
+
console.log(`${JSON.stringify(value, null, 2)}\n`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ensureGitRepository(cwd) {
|
|
314
|
+
try {
|
|
315
|
+
runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
316
|
+
} catch {
|
|
317
|
+
throw new Error("当前目录不是 git repository,无法执行 delivery commit");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function loadTaskReport(cwd, taskId) {
|
|
322
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
323
|
+
const reportDirectory = normalizeOutputPolicy(projectConfig?.output_policy).report.directory;
|
|
324
|
+
const reportPath = path.join(cwd, reportDirectory, `${taskId}.json`);
|
|
325
|
+
if (!fs.existsSync(reportPath)) {
|
|
326
|
+
throw new Error(`缺少任务报告: ${reportPath}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error(`任务报告 JSON 解析失败: ${reportPath}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildCommitPlan(cwd, report, options) {
|
|
337
|
+
const reportScope = Array.isArray(report?.actual_scope) ? report.actual_scope : [];
|
|
338
|
+
const artifactPaths = Object.values(report?.output_artifacts ?? {})
|
|
339
|
+
.filter((artifact) => artifact?.satisfied === true && typeof artifact?.path === "string" && artifact.path.trim().length > 0)
|
|
340
|
+
.map((artifact) => artifact.path.trim());
|
|
341
|
+
|
|
342
|
+
const candidates = [...reportScope, ...artifactPaths]
|
|
343
|
+
.map((item) => normalizeRepoPath(item))
|
|
344
|
+
.filter(Boolean);
|
|
345
|
+
|
|
346
|
+
const resolvedPaths = resolveCommitPaths(cwd, candidates);
|
|
347
|
+
const wideScope = detectWideScope(cwd, candidates);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
message: options.message ?? null,
|
|
351
|
+
paths: resolvedPaths,
|
|
352
|
+
wide_scope: wideScope
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function normalizeRepoPath(value) {
|
|
357
|
+
const normalized = String(value ?? "").trim().replace(/\\/g, "/");
|
|
358
|
+
if (!normalized || normalized === ".") {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (normalized.startsWith("./")) {
|
|
363
|
+
return normalizeRepoPath(normalized.slice(2));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return normalized;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCommitMessage(report) {
|
|
370
|
+
const prefix = resolveCommitPrefix(report?.intent);
|
|
371
|
+
const summary = String(report?.conclusion ?? "")
|
|
372
|
+
.replace(/\s+/g, " ")
|
|
373
|
+
.trim()
|
|
374
|
+
.slice(0, 72);
|
|
375
|
+
|
|
376
|
+
if (!summary) {
|
|
377
|
+
throw new Error("无法从 report conclusion 生成 commit message,请使用 --message 显式指定");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return `${prefix}: ${summary}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function resolveCommitPrefix(intent) {
|
|
384
|
+
if (intent === "feature") {
|
|
385
|
+
return "feat";
|
|
386
|
+
}
|
|
387
|
+
if (intent === "bug") {
|
|
388
|
+
return "fix";
|
|
389
|
+
}
|
|
390
|
+
if (intent === "refactor") {
|
|
391
|
+
return "refactor";
|
|
392
|
+
}
|
|
393
|
+
return "chore";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function stageCommitPaths(cwd, paths) {
|
|
397
|
+
runGit(cwd, ["add", "-A", "--", ...paths]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function resolveCommitPaths(cwd, candidates) {
|
|
401
|
+
const changedFiles = listChangedFiles(cwd);
|
|
402
|
+
const changedSet = new Set(changedFiles);
|
|
403
|
+
const resolved = new Set();
|
|
404
|
+
const excludedPrefixes = runtimeRelativeCandidates("reports");
|
|
405
|
+
|
|
406
|
+
for (const candidate of candidates) {
|
|
407
|
+
if (!candidate || excludedPrefixes.some((prefix) => candidate.startsWith(`${prefix}/`) || candidate === prefix)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const fullPath = path.join(cwd, candidate);
|
|
412
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
|
413
|
+
const prefix = `${candidate.replace(/\/+$/, "")}/`;
|
|
414
|
+
for (const file of changedFiles) {
|
|
415
|
+
if (file === candidate || file.startsWith(prefix)) {
|
|
416
|
+
resolved.add(file);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (changedSet.has(candidate)) {
|
|
423
|
+
resolved.add(candidate);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
428
|
+
resolved.add(candidate);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return [...resolved];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function detectWideScope(cwd, candidates) {
|
|
436
|
+
const wide = [];
|
|
437
|
+
const excludedPrefixes = runtimeRelativeCandidates("reports");
|
|
438
|
+
|
|
439
|
+
for (const candidate of candidates) {
|
|
440
|
+
if (!candidate || excludedPrefixes.some((prefix) => candidate.startsWith(`${prefix}/`) || candidate === prefix)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const fullPath = path.join(cwd, candidate);
|
|
445
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const normalized = candidate.replace(/\/+$/, "");
|
|
450
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
451
|
+
if (segments.length <= 1) {
|
|
452
|
+
wide.push(candidate);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (segments.length === 2 && ["packages", "apps", "services"].includes(segments[0])) {
|
|
457
|
+
wide.push(candidate);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return [...new Set(wide)];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function listChangedFiles(cwd) {
|
|
465
|
+
const output = runGit(cwd, ["status", "--short", "--untracked-files=all"]);
|
|
466
|
+
if (!output) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return output
|
|
471
|
+
.split("\n")
|
|
472
|
+
.filter((line) => line.trim().length > 0)
|
|
473
|
+
.map((line) => extractStatusPath(line))
|
|
474
|
+
.map((file) => normalizeRepoPath(file))
|
|
475
|
+
.filter(Boolean);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function extractStatusPath(line) {
|
|
479
|
+
const rawPath = line.slice(3).trim();
|
|
480
|
+
const renameArrow = rawPath.lastIndexOf(" -> ");
|
|
481
|
+
if (renameArrow >= 0) {
|
|
482
|
+
return rawPath.slice(renameArrow + 4).trim();
|
|
483
|
+
}
|
|
484
|
+
return rawPath;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getHeadSha(cwd) {
|
|
488
|
+
return runGit(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function runGit(cwd, args) {
|
|
492
|
+
return execFileSync("git", args, {
|
|
493
|
+
cwd,
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
496
|
+
}).trimEnd();
|
|
497
|
+
}
|