@samaksh/git-worker 0.1.0 → 0.2.1
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/dist/cli/index.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/services/git/reader.js +105 -34
- package/dist/services/workspace/manager.js +1 -1
- package/dist/stages/eligibility.js +38 -14
- package/dist/stages/select.js +46 -37
- package/package.json +11 -1
- package/GIT_WORKER_PROJECT_PLAN.md +0 -499
- package/demo/README.md +0 -58
- package/demo/index.ts +0 -526
- package/src/app.ts +0 -69
- package/src/cli/commands.ts +0 -142
- package/src/cli/index.ts +0 -123
- package/src/config/defaults.ts +0 -46
- package/src/config/load.ts +0 -70
- package/src/config/schema.ts +0 -48
- package/src/core/errors.ts +0 -45
- package/src/core/pipeline.ts +0 -17
- package/src/core/types.ts +0 -188
- package/src/services/editor/index.ts +0 -85
- package/src/services/git/ops.ts +0 -170
- package/src/services/git/reader.ts +0 -148
- package/src/services/report/index.ts +0 -104
- package/src/services/review/index.ts +0 -80
- package/src/services/state/index.ts +0 -77
- package/src/services/template/env.ts +0 -169
- package/src/services/template/interpolator.ts +0 -109
- package/src/services/template/transform.ts +0 -97
- package/src/services/template/validator.ts +0 -75
- package/src/services/workspace/cache.ts +0 -144
- package/src/services/workspace/manager.ts +0 -370
- package/src/stages/apply.ts +0 -194
- package/src/stages/edit-template.ts +0 -227
- package/src/stages/eligibility.ts +0 -67
- package/src/stages/load-template.ts +0 -188
- package/src/stages/plan.ts +0 -362
- package/src/stages/push.ts +0 -120
- package/src/stages/report.ts +0 -126
- package/src/stages/resolve-project.ts +0 -341
- package/src/stages/review.ts +0 -228
- package/src/stages/select.ts +0 -302
- package/src/stages/sync.ts +0 -63
- package/src/utils/hash.ts +0 -15
- package/src/utils/ui.ts +0 -137
- package/src/utils/which.ts +0 -18
- package/tsconfig.json +0 -18
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { Stage } from "../core/pipeline.js";
|
|
2
|
-
import { RunContext, AppDeps, ok, err } from "../core/types.js";
|
|
3
|
-
import { clack, banner, statusLine, sectionHeader, sym } from "../utils/ui.js";
|
|
4
|
-
import { inputCancelledError } from "../core/errors.js";
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import { resolve } from "path";
|
|
7
|
-
import chalk from "chalk";
|
|
8
|
-
import { listCachedProjects, CachedProjectInfo } from "../services/workspace/cache.js";
|
|
9
|
-
import { getWorkspaceRoot } from "../services/workspace/manager.js";
|
|
10
|
-
|
|
11
|
-
async function promptCachedRepo(
|
|
12
|
-
cachedProjects: CachedProjectInfo[],
|
|
13
|
-
root: string
|
|
14
|
-
): Promise<string | "__BACK__" | null> {
|
|
15
|
-
if (cachedProjects.length === 0) {
|
|
16
|
-
clack.log.info(`No cached repositories found in ${root}/projects yet.`);
|
|
17
|
-
return "__BACK__";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
console.log("");
|
|
21
|
-
console.log(
|
|
22
|
-
chalk.bold(
|
|
23
|
-
` ${"Name".padEnd(20)} ${"Remote URL / Path".padEnd(46)} Cached Date`
|
|
24
|
-
)
|
|
25
|
-
);
|
|
26
|
-
console.log(chalk.gray(" " + "─".repeat(80)));
|
|
27
|
-
for (const p of cachedProjects) {
|
|
28
|
-
const name = chalk.cyan(p.name.slice(0, 18).padEnd(20));
|
|
29
|
-
const target = (p.remoteUrl || p.sourceRepoPath || "(unknown)").slice(0, 44).padEnd(46);
|
|
30
|
-
const date = p.createdAt ? p.createdAt.slice(0, 10) : "unknown";
|
|
31
|
-
console.log(` ${name} ${chalk.white(target)} ${chalk.gray(date)}`);
|
|
32
|
-
}
|
|
33
|
-
console.log(chalk.gray(" " + "─".repeat(80)));
|
|
34
|
-
console.log("");
|
|
35
|
-
|
|
36
|
-
const selected = await clack.select<string>({
|
|
37
|
-
message: "Select a cached repository to target:",
|
|
38
|
-
options: [
|
|
39
|
-
...cachedProjects.map((p) => ({
|
|
40
|
-
value: p.remoteUrl || p.sourceRepoPath || p.id,
|
|
41
|
-
label: `${sym.branch} ${chalk.bold(p.name)}`,
|
|
42
|
-
hint: `${p.remoteUrl || p.sourceRepoPath}`,
|
|
43
|
-
})),
|
|
44
|
-
{
|
|
45
|
-
value: "__BACK__",
|
|
46
|
-
label: "← Back to repository menu",
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (clack.isCancel(selected)) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return selected;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const resolveProjectStage: Stage<RunContext, AppDeps> = async (context, deps) => {
|
|
59
|
-
banner();
|
|
60
|
-
|
|
61
|
-
let targetRepo = context.input.repo;
|
|
62
|
-
|
|
63
|
-
// If repo was not supplied via CLI argument (-r / --repo)
|
|
64
|
-
if (!targetRepo) {
|
|
65
|
-
const isCi = context.input.ci ?? false;
|
|
66
|
-
const cwd = process.cwd();
|
|
67
|
-
|
|
68
|
-
// Check if the current working directory is a git repository
|
|
69
|
-
const localCheck = await deps.services.workspace.checkLocalRepo(
|
|
70
|
-
cwd,
|
|
71
|
-
context.input.remote || deps.config.git.remote
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (isCi) {
|
|
75
|
-
// In CI mode, cannot prompt: use cwd if git repo, otherwise fail
|
|
76
|
-
if (localCheck.isRepo) {
|
|
77
|
-
targetRepo = cwd;
|
|
78
|
-
} else {
|
|
79
|
-
clack.log.error(
|
|
80
|
-
`Current directory '${cwd}' is not a Git repository. Pass --repo <url-or-path> in CI mode.`
|
|
81
|
-
);
|
|
82
|
-
return err(inputCancelledError("Not a git repository in CI mode without --repo flag"));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
// Interactive TUI mode
|
|
86
|
-
sectionHeader("Target Repository");
|
|
87
|
-
|
|
88
|
-
const root = getWorkspaceRoot(deps.config);
|
|
89
|
-
|
|
90
|
-
while (!targetRepo) {
|
|
91
|
-
const cachedProjects = await listCachedProjects(deps.config);
|
|
92
|
-
|
|
93
|
-
// If user specifically requested --cached via CLI
|
|
94
|
-
if (context.input.cached) {
|
|
95
|
-
if (cachedProjects.length === 0) {
|
|
96
|
-
clack.log.warn(`No cached repositories found in ${root}/projects yet.`);
|
|
97
|
-
context.input.cached = false;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const selected = await promptCachedRepo(cachedProjects, root);
|
|
101
|
-
if (selected === null) {
|
|
102
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
103
|
-
}
|
|
104
|
-
if (selected === "__BACK__") {
|
|
105
|
-
context.input.cached = false;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
targetRepo = selected;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (localCheck.isRepo) {
|
|
113
|
-
// Current directory IS a git repo
|
|
114
|
-
const remoteInfo = localCheck.remoteUrl
|
|
115
|
-
? ` (remote: ${localCheck.remoteUrl})`
|
|
116
|
-
: " (local-only)";
|
|
117
|
-
statusLine(
|
|
118
|
-
"ok",
|
|
119
|
-
"Detected Git repository in current directory",
|
|
120
|
-
`${cwd}${remoteInfo}`
|
|
121
|
-
);
|
|
122
|
-
console.log("");
|
|
123
|
-
|
|
124
|
-
const choice = await clack.select<string>({
|
|
125
|
-
message: "What repository would you like to target?",
|
|
126
|
-
options: [
|
|
127
|
-
{
|
|
128
|
-
value: "current",
|
|
129
|
-
label: "Use current repository",
|
|
130
|
-
hint: localCheck.remoteUrl || cwd,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
value: "cached",
|
|
134
|
-
label: `📦 Select from cached repositories (${cachedProjects.length} available)`,
|
|
135
|
-
hint: "list already cloned repositories (name, remote URL)",
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
value: "remote",
|
|
139
|
-
label: "Clone a remote repository URL",
|
|
140
|
-
hint: "https://... or git@...",
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
value: "local-path",
|
|
144
|
-
label: "Specify a different local repository path",
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
if (clack.isCancel(choice)) {
|
|
150
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (choice === "current") {
|
|
154
|
-
targetRepo = cwd;
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (choice === "cached") {
|
|
159
|
-
const selected = await promptCachedRepo(cachedProjects, root);
|
|
160
|
-
if (selected === null) {
|
|
161
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
162
|
-
}
|
|
163
|
-
if (selected === "__BACK__") {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
targetRepo = selected;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (choice === "remote") {
|
|
171
|
-
const remoteUrlInput = await clack.text({
|
|
172
|
-
message: "Remote repository URL:",
|
|
173
|
-
placeholder: "https://github.com/org/repo.git or git@github.com:org/repo.git",
|
|
174
|
-
validate: (v) => {
|
|
175
|
-
if (!v || v.trim() === "") return "Repository URL cannot be empty";
|
|
176
|
-
const trimmed = v.trim();
|
|
177
|
-
const isUrl =
|
|
178
|
-
trimmed.startsWith("http://") ||
|
|
179
|
-
trimmed.startsWith("https://") ||
|
|
180
|
-
trimmed.startsWith("git@") ||
|
|
181
|
-
trimmed.startsWith("ssh://") ||
|
|
182
|
-
trimmed.startsWith("git://");
|
|
183
|
-
if (!isUrl) {
|
|
184
|
-
return "Please enter a valid remote URL (e.g. https://github.com/... or git@github.com:...)";
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
if (clack.isCancel(remoteUrlInput)) {
|
|
189
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
190
|
-
}
|
|
191
|
-
targetRepo = (remoteUrlInput as string).trim();
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (choice === "local-path") {
|
|
196
|
-
const pathInput = await clack.text({
|
|
197
|
-
message: "Path to local repository:",
|
|
198
|
-
placeholder: "/path/to/repo or ../other-repo",
|
|
199
|
-
validate: (v) => {
|
|
200
|
-
if (!v || v.trim() === "") return "Path cannot be empty";
|
|
201
|
-
if (!existsSync(resolve(v.trim()))) return `Path does not exist: ${v.trim()}`;
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
if (clack.isCancel(pathInput)) {
|
|
205
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
206
|
-
}
|
|
207
|
-
targetRepo = (pathInput as string).trim();
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
// Current directory IS NOT a git repo
|
|
212
|
-
statusLine("info", "Current directory is not a Git repository", cwd);
|
|
213
|
-
console.log("");
|
|
214
|
-
|
|
215
|
-
const choice = await clack.select<string>({
|
|
216
|
-
message: "How would you like to target a repository?",
|
|
217
|
-
options: [
|
|
218
|
-
...(cachedProjects.length > 0
|
|
219
|
-
? [
|
|
220
|
-
{
|
|
221
|
-
value: "cached",
|
|
222
|
-
label: `📦 Select from cached repositories (${cachedProjects.length} available)`,
|
|
223
|
-
hint: "list already cloned repositories (name, remote URL)",
|
|
224
|
-
},
|
|
225
|
-
]
|
|
226
|
-
: [
|
|
227
|
-
{
|
|
228
|
-
value: "cached",
|
|
229
|
-
label: "📦 Cached repositories (none yet)",
|
|
230
|
-
hint: "view already cloned repositories in ~/.git-worker",
|
|
231
|
-
},
|
|
232
|
-
]),
|
|
233
|
-
{
|
|
234
|
-
value: "remote",
|
|
235
|
-
label: "Clone a remote repository URL",
|
|
236
|
-
hint: "recommended — https://... or git@...",
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
value: "local-path",
|
|
240
|
-
label: "Specify path to a local Git repository",
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (clack.isCancel(choice)) {
|
|
246
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (choice === "cached") {
|
|
250
|
-
const selected = await promptCachedRepo(cachedProjects, root);
|
|
251
|
-
if (selected === null) {
|
|
252
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
253
|
-
}
|
|
254
|
-
if (selected === "__BACK__") {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
targetRepo = selected;
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (choice === "remote") {
|
|
262
|
-
const remoteUrlInput = await clack.text({
|
|
263
|
-
message: "Remote repository URL:",
|
|
264
|
-
placeholder: "https://github.com/org/repo.git or git@github.com:org/repo.git",
|
|
265
|
-
validate: (v) => {
|
|
266
|
-
if (!v || v.trim() === "") return "Repository URL cannot be empty";
|
|
267
|
-
const trimmed = v.trim();
|
|
268
|
-
const isUrl =
|
|
269
|
-
trimmed.startsWith("http://") ||
|
|
270
|
-
trimmed.startsWith("https://") ||
|
|
271
|
-
trimmed.startsWith("git@") ||
|
|
272
|
-
trimmed.startsWith("ssh://") ||
|
|
273
|
-
trimmed.startsWith("git://");
|
|
274
|
-
if (!isUrl) {
|
|
275
|
-
return "Please enter a valid remote URL (e.g. https://github.com/... or git@github.com:...)";
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
if (clack.isCancel(remoteUrlInput)) {
|
|
280
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
281
|
-
}
|
|
282
|
-
targetRepo = (remoteUrlInput as string).trim();
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (choice === "local-path") {
|
|
287
|
-
const pathInput = await clack.text({
|
|
288
|
-
message: "Path to local repository:",
|
|
289
|
-
placeholder: "/path/to/repo or ../other-repo",
|
|
290
|
-
validate: (v) => {
|
|
291
|
-
if (!v || v.trim() === "") return "Path cannot be empty";
|
|
292
|
-
if (!existsSync(resolve(v.trim()))) return `Path does not exist: ${v.trim()}`;
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
if (clack.isCancel(pathInput)) {
|
|
296
|
-
return err(inputCancelledError("Repository selection cancelled"));
|
|
297
|
-
}
|
|
298
|
-
targetRepo = (pathInput as string).trim();
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Now resolve the project workspace
|
|
307
|
-
const s = clack.spinner();
|
|
308
|
-
s.start("Resolving project workspace…");
|
|
309
|
-
|
|
310
|
-
const result = await deps.services.workspace.resolveProject(
|
|
311
|
-
targetRepo!,
|
|
312
|
-
context.input.remote,
|
|
313
|
-
deps.config
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
if (!result.ok) {
|
|
317
|
-
s.stop(chalk.red("Project resolution failed"));
|
|
318
|
-
clack.log.error(result.error.message);
|
|
319
|
-
return result;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
s.stop(chalk.green("Project workspace ready"));
|
|
323
|
-
if (result.value.isCached) {
|
|
324
|
-
statusLine(
|
|
325
|
-
"info",
|
|
326
|
-
"Using cached repository clone (already downloaded)",
|
|
327
|
-
result.value.remoteUrl || result.value.managedRepoPath
|
|
328
|
-
);
|
|
329
|
-
} else {
|
|
330
|
-
statusLine("info", `Managed workspace: ${result.value.managedRepoPath}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return ok({
|
|
334
|
-
...context,
|
|
335
|
-
input: {
|
|
336
|
-
...context.input,
|
|
337
|
-
repo: targetRepo,
|
|
338
|
-
},
|
|
339
|
-
project: result.value,
|
|
340
|
-
});
|
|
341
|
-
};
|
package/src/stages/review.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { Stage } from "../core/pipeline.js";
|
|
2
|
-
import { RunContext, AppDeps, Approval, ok, err } from "../core/types.js";
|
|
3
|
-
import { workspaceError, inputCancelledError } from "../core/errors.js";
|
|
4
|
-
import { writeSnapshots, parseDiffLines } from "../services/review/index.js";
|
|
5
|
-
import { clack, sectionHeader, statusLine, divider, renderDiff, renderBranchTable, sym } from "../utils/ui.js";
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
|
|
8
|
-
export const reviewChangesStage: Stage<RunContext, AppDeps> = async (context, deps) => {
|
|
9
|
-
if (!context.plans || !context.project) {
|
|
10
|
-
return err(workspaceError("No plans to review"));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const isCi = context.input.ci ?? false;
|
|
14
|
-
const autoApprove = context.input.autoApprove || deps.config.review.autoApprove;
|
|
15
|
-
|
|
16
|
-
sectionHeader("Review Changes");
|
|
17
|
-
|
|
18
|
-
// Show summary table first
|
|
19
|
-
renderBranchTable(
|
|
20
|
-
context.plans.map((p) => ({
|
|
21
|
-
name: p.branch.name,
|
|
22
|
-
status: p.status,
|
|
23
|
-
detail:
|
|
24
|
-
p.status === "changed"
|
|
25
|
-
? `${p.updatedKeys.length} updated, ${p.addedKeys.length} added`
|
|
26
|
-
: p.status === "error"
|
|
27
|
-
? p.reason ?? "error"
|
|
28
|
-
: "no changes",
|
|
29
|
-
}))
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const changedPlans = context.plans.filter((p) => p.status === "changed");
|
|
33
|
-
const approvals: Approval[] = [];
|
|
34
|
-
|
|
35
|
-
// Auto-approve unchanged and errors
|
|
36
|
-
for (const plan of context.plans) {
|
|
37
|
-
if (plan.status !== "changed") {
|
|
38
|
-
approvals.push({
|
|
39
|
-
branchName: plan.branch.name,
|
|
40
|
-
approved: false,
|
|
41
|
-
planHash: plan.planHash,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (changedPlans.length === 0) {
|
|
47
|
-
statusLine("info", "No changed branches to review");
|
|
48
|
-
return ok({ ...context, approvals });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Auto-approve all
|
|
52
|
-
if (autoApprove) {
|
|
53
|
-
for (const plan of changedPlans) {
|
|
54
|
-
approvals.push({
|
|
55
|
-
branchName: plan.branch.name,
|
|
56
|
-
approved: true,
|
|
57
|
-
planHash: plan.planHash,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
statusLine("ok", `Auto-approved ${changedPlans.length} branch(es)`);
|
|
61
|
-
return ok({ ...context, approvals });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (isCi) {
|
|
65
|
-
// CI without auto-approve: reject all
|
|
66
|
-
for (const plan of changedPlans) {
|
|
67
|
-
approvals.push({
|
|
68
|
-
branchName: plan.branch.name,
|
|
69
|
-
approved: false,
|
|
70
|
-
planHash: plan.planHash,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
statusLine("warn", "CI mode — all branches rejected (use --auto-approve to commit in CI)");
|
|
74
|
-
return ok({ ...context, approvals });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Interactive review
|
|
78
|
-
console.log("");
|
|
79
|
-
|
|
80
|
-
// Ask if user wants batch action or per-branch review
|
|
81
|
-
const batchChoice = await clack.select<string>({
|
|
82
|
-
message: `Review ${changedPlans.length} changed branch(es):`,
|
|
83
|
-
options: [
|
|
84
|
-
{ value: "per-branch", label: "Review each branch individually", hint: "recommended" },
|
|
85
|
-
{ value: "approve-all", label: `Approve all ${changedPlans.length} branches` },
|
|
86
|
-
{ value: "reject-all", label: "Reject all" },
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (clack.isCancel(batchChoice)) {
|
|
91
|
-
return err(inputCancelledError("Review cancelled"));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (batchChoice === "approve-all") {
|
|
95
|
-
for (const plan of changedPlans) {
|
|
96
|
-
approvals.push({ branchName: plan.branch.name, approved: true, planHash: plan.planHash });
|
|
97
|
-
}
|
|
98
|
-
statusLine("ok", `Approved all ${changedPlans.length} branch(es)`);
|
|
99
|
-
return ok({ ...context, approvals });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (batchChoice === "reject-all") {
|
|
103
|
-
for (const plan of changedPlans) {
|
|
104
|
-
approvals.push({ branchName: plan.branch.name, approved: false, planHash: plan.planHash });
|
|
105
|
-
}
|
|
106
|
-
statusLine("warn", "All branches rejected");
|
|
107
|
-
return ok({ ...context, approvals });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Per-branch review
|
|
111
|
-
for (const plan of changedPlans) {
|
|
112
|
-
console.log("");
|
|
113
|
-
divider();
|
|
114
|
-
console.log(` ${sym.branch} ${chalk.bold.cyan(plan.branch.name)} ${chalk.gray(plan.baseCommitSha.slice(0, 8))}`);
|
|
115
|
-
console.log(` ${chalk.gray(plan.path)}`);
|
|
116
|
-
console.log("");
|
|
117
|
-
|
|
118
|
-
// Show inline diff
|
|
119
|
-
if (deps.config.report.warnOnSecrets) {
|
|
120
|
-
console.log(chalk.yellow(" ⚠ Diff may contain secret values — treat with care"));
|
|
121
|
-
console.log("");
|
|
122
|
-
}
|
|
123
|
-
renderDiff(plan.diff, false);
|
|
124
|
-
|
|
125
|
-
// Write snapshots for external diff tool
|
|
126
|
-
const snapResult = await writeSnapshots(
|
|
127
|
-
context.project.stagingDir,
|
|
128
|
-
context.runId,
|
|
129
|
-
plan
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
let openDiff = false;
|
|
133
|
-
if (snapResult.ok) {
|
|
134
|
-
const choice = await clack.select<string>({
|
|
135
|
-
message: "Action:",
|
|
136
|
-
options: [
|
|
137
|
-
{ value: "approve", label: `${chalk.green("✓")} Approve`, hint: "commit this branch" },
|
|
138
|
-
{ value: "reject", label: `${chalk.red("✗")} Reject`, hint: "skip this branch" },
|
|
139
|
-
{ value: "diff", label: `${chalk.cyan("⊞")} Open in diff viewer`, hint: deps.config.editor.name },
|
|
140
|
-
],
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (clack.isCancel(choice)) {
|
|
144
|
-
return err(inputCancelledError("Review cancelled"));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (choice === "diff") {
|
|
148
|
-
const { beforePath, afterPath } = snapResult.value;
|
|
149
|
-
const diffResult = await deps.services.editor.openForDiff(beforePath, afterPath, deps.config);
|
|
150
|
-
if (!diffResult.ok) {
|
|
151
|
-
clack.log.warn(diffResult.error.message);
|
|
152
|
-
}
|
|
153
|
-
// Ask again after opening diff
|
|
154
|
-
const afterDiff = await clack.select<string>({
|
|
155
|
-
message: "After reviewing the diff:",
|
|
156
|
-
options: [
|
|
157
|
-
{ value: "approve", label: `${chalk.green("✓")} Approve` },
|
|
158
|
-
{ value: "reject", label: `${chalk.red("✗")} Reject` },
|
|
159
|
-
],
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (clack.isCancel(afterDiff)) {
|
|
163
|
-
return err(inputCancelledError("Review cancelled"));
|
|
164
|
-
}
|
|
165
|
-
approvals.push({
|
|
166
|
-
branchName: plan.branch.name,
|
|
167
|
-
approved: afterDiff === "approve",
|
|
168
|
-
planHash: plan.planHash,
|
|
169
|
-
});
|
|
170
|
-
} else {
|
|
171
|
-
approvals.push({
|
|
172
|
-
branchName: plan.branch.name,
|
|
173
|
-
approved: choice === "approve",
|
|
174
|
-
planHash: plan.planHash,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
// No snapshots — just approve/reject
|
|
179
|
-
const choice = await clack.select<string>({
|
|
180
|
-
message: "Action:",
|
|
181
|
-
options: [
|
|
182
|
-
{ value: "approve", label: `${chalk.green("✓")} Approve` },
|
|
183
|
-
{ value: "reject", label: `${chalk.red("✗")} Reject` },
|
|
184
|
-
],
|
|
185
|
-
});
|
|
186
|
-
if (clack.isCancel(choice)) {
|
|
187
|
-
return err(inputCancelledError("Review cancelled"));
|
|
188
|
-
}
|
|
189
|
-
approvals.push({
|
|
190
|
-
branchName: plan.branch.name,
|
|
191
|
-
approved: choice === "approve",
|
|
192
|
-
planHash: plan.planHash,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
divider();
|
|
198
|
-
|
|
199
|
-
const approved = approvals.filter((a) => a.approved).length;
|
|
200
|
-
const rejected = approvals.filter((a) => !a.approved && context.plans!.find((p) => p.branch.name === a.branchName)?.status === "changed").length;
|
|
201
|
-
statusLine("ok", `Review complete — ${approved} approved, ${rejected} rejected`);
|
|
202
|
-
|
|
203
|
-
// Confirm before destructive apply
|
|
204
|
-
if (approved > 0) {
|
|
205
|
-
console.log("");
|
|
206
|
-
console.log(chalk.bold(" Summary of approved changes:"));
|
|
207
|
-
for (const plan of changedPlans) {
|
|
208
|
-
const a = approvals.find((ap) => ap.branchName === plan.branch.name);
|
|
209
|
-
if (a?.approved) {
|
|
210
|
-
const detail = [
|
|
211
|
-
plan.updatedKeys.length > 0 ? `update: ${plan.updatedKeys.join(", ")}` : "",
|
|
212
|
-
plan.addedKeys.length > 0 ? `add: ${plan.addedKeys.join(", ")}` : "",
|
|
213
|
-
].filter(Boolean).join(" | ");
|
|
214
|
-
statusLine("ok", plan.branch.name, detail);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
console.log("");
|
|
218
|
-
const confirm = await clack.confirm({
|
|
219
|
-
message: `Proceed to commit ${approved} branch(es)?`,
|
|
220
|
-
initialValue: true,
|
|
221
|
-
});
|
|
222
|
-
if (clack.isCancel(confirm) || !confirm) {
|
|
223
|
-
return err(inputCancelledError("Commit aborted by user"));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return ok({ ...context, approvals });
|
|
228
|
-
};
|