@samaksh/git-worker 0.2.2 → 0.3.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/dist/cli/index.js +1 -1
- package/dist/config/repo-presets.d.ts +16 -0
- package/dist/config/repo-presets.js +271 -0
- package/dist/core/types.d.ts +6 -0
- package/dist/services/workspace/manager.js +1 -1
- package/dist/stages/apply.js +92 -71
- package/dist/stages/edit-template.js +167 -111
- package/dist/stages/eligibility.js +45 -16
- package/dist/stages/load-template.js +109 -94
- package/dist/stages/plan.js +231 -187
- package/dist/stages/report.js +23 -18
- package/dist/stages/review.js +129 -111
- package/dist/stages/select.js +115 -15
- package/package.json +3 -5
package/dist/stages/plan.js
CHANGED
|
@@ -8,50 +8,174 @@ import { computeDiff } from "../services/review/index.js";
|
|
|
8
8
|
import { clack, sectionHeader, statusLine, sym } from "../utils/ui.js";
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
export const planChangesStage = async (context, deps) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
!context.target ||
|
|
14
|
-
!context.project) {
|
|
11
|
+
const templates = context.templates || (context.activeTemplate ? [context.activeTemplate] : []);
|
|
12
|
+
if (templates.length === 0 || !context.eligible || !context.project) {
|
|
15
13
|
return err(workspaceError("Prerequisites missing for planning"));
|
|
16
14
|
}
|
|
17
|
-
sectionHeader(
|
|
15
|
+
sectionHeader(`Planning Changes across ${context.eligible.length} branch(es) & ${templates.length} template(s)`);
|
|
18
16
|
const plans = [];
|
|
19
17
|
const isCi = context.input.ci ?? false;
|
|
20
|
-
const targetPath = context.target.path;
|
|
21
|
-
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
22
|
-
const engine = context.activeTemplate?.engine ?? (isEnv ? "env-reconcile" : "source-of-truth");
|
|
23
|
-
const templateRaw = context.activeTemplate?.raw ?? context.template?.raw ?? "";
|
|
24
18
|
for (const branch of context.eligible) {
|
|
25
19
|
const s = clack.spinner();
|
|
26
20
|
s.start(`Planning ${branch.name}…`);
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
// Read branch's existing .env once to populate envMap and extract dynamic variables
|
|
22
|
+
const envRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, ".env");
|
|
23
|
+
const envMap = new Map();
|
|
24
|
+
if (envRead.ok) {
|
|
25
|
+
const parsedEnv = parseEnvFile(envRead.value);
|
|
26
|
+
if (parsedEnv.ok) {
|
|
27
|
+
for (const [key, entry] of parsedEnv.value.map) {
|
|
28
|
+
envMap.set(key, entry.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Resolve branch-level servicename (support lowercase, uppercase, snake_case)
|
|
33
|
+
let branchServiceName = envMap.get("servicename") ||
|
|
34
|
+
envMap.get("SERVICE_NAME") ||
|
|
35
|
+
envMap.get("service_name") ||
|
|
36
|
+
envMap.get("SERVICENAME");
|
|
37
|
+
// Fallback: check if an env template in context statically defines servicename
|
|
38
|
+
if (!branchServiceName) {
|
|
39
|
+
const envTmpl = templates.find((t) => t.engine === "env-reconcile");
|
|
40
|
+
if (envTmpl?.parsedEnv) {
|
|
41
|
+
const assign = envTmpl.parsedEnv.assignments.find((a) => a.key === "servicename" ||
|
|
42
|
+
a.key === "SERVICE_NAME" ||
|
|
43
|
+
a.key.toLowerCase() === "servicename");
|
|
44
|
+
if (assign && assign.isStatic && assign.value) {
|
|
45
|
+
branchServiceName = assign.value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let branchChangedCount = 0;
|
|
50
|
+
let branchErrorCount = 0;
|
|
51
|
+
for (const template of templates) {
|
|
52
|
+
let targetPath = template.path;
|
|
53
|
+
// Handle dynamic path substitution (e.g. {servicename}/requirements.txt)
|
|
54
|
+
if (template.isDynamic || targetPath.includes("{servicename}")) {
|
|
55
|
+
const varName = template.dynamicVar || "servicename";
|
|
56
|
+
const val = branchServiceName ||
|
|
57
|
+
envMap.get(varName) ||
|
|
58
|
+
envMap.get(varName.toLowerCase()) ||
|
|
59
|
+
envMap.get(varName.toUpperCase());
|
|
60
|
+
if (!val) {
|
|
61
|
+
branchErrorCount++;
|
|
62
|
+
const errorReason = `Missing '${varName}' in branch .env required for dynamic path ${targetPath}`;
|
|
63
|
+
plans.push({
|
|
64
|
+
branch,
|
|
65
|
+
baseCommitSha: branch.sha,
|
|
66
|
+
path: targetPath,
|
|
67
|
+
beforeContent: "",
|
|
68
|
+
beforeContentHash: sha256(""),
|
|
69
|
+
afterContent: "",
|
|
70
|
+
afterContentHash: sha256(""),
|
|
71
|
+
diff: "",
|
|
72
|
+
planHash: computePlanHash(branch.name, branch.sha, targetPath, sha256(""), sha256("")),
|
|
73
|
+
status: "error",
|
|
74
|
+
reason: errorReason,
|
|
75
|
+
addedKeys: [],
|
|
76
|
+
updatedKeys: [],
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
targetPath = targetPath.replace(`{${varName}}`, val);
|
|
81
|
+
}
|
|
82
|
+
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
83
|
+
const engine = template.engine ?? (isEnv ? "env-reconcile" : "source-of-truth");
|
|
84
|
+
const templateRaw = template.raw;
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────
|
|
86
|
+
// 1. SOURCE-OF-TRUTH ENGINE (Dockerfile, scripts, JSON, requirements)
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────
|
|
88
|
+
if (engine === "source-of-truth") {
|
|
89
|
+
const targetRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, targetPath);
|
|
90
|
+
const isNewFile = !targetRead.ok;
|
|
91
|
+
const beforeContent = targetRead.ok ? targetRead.value : "";
|
|
92
|
+
const beforeContentHash = sha256(beforeContent);
|
|
93
|
+
// Add dynamic servicename to envMap if available
|
|
94
|
+
if (branchServiceName && !envMap.has("servicename")) {
|
|
95
|
+
envMap.set("servicename", branchServiceName);
|
|
96
|
+
envMap.set("SERVICE_NAME", branchServiceName);
|
|
40
97
|
}
|
|
98
|
+
const interpolated = interpolateTemplate(templateRaw, envMap, {
|
|
99
|
+
branchName: branch.name,
|
|
100
|
+
repoName: context.project.id,
|
|
101
|
+
commitSha: branch.sha,
|
|
102
|
+
});
|
|
103
|
+
if (!interpolated.ok) {
|
|
104
|
+
branchErrorCount++;
|
|
105
|
+
plans.push({
|
|
106
|
+
branch,
|
|
107
|
+
baseCommitSha: branch.sha,
|
|
108
|
+
path: targetPath,
|
|
109
|
+
beforeContent,
|
|
110
|
+
beforeContentHash,
|
|
111
|
+
afterContent: beforeContent,
|
|
112
|
+
afterContentHash: beforeContentHash,
|
|
113
|
+
diff: "",
|
|
114
|
+
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
115
|
+
status: "error",
|
|
116
|
+
reason: interpolated.error.message,
|
|
117
|
+
addedKeys: [],
|
|
118
|
+
updatedKeys: [],
|
|
119
|
+
isNewFile,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const { content: afterContent, missingVars } = interpolated.value;
|
|
124
|
+
if (missingVars.length > 0) {
|
|
125
|
+
branchErrorCount++;
|
|
126
|
+
const errorReason = `Missing required variable(s) from .env: ${missingVars.join(", ")}`;
|
|
127
|
+
plans.push({
|
|
128
|
+
branch,
|
|
129
|
+
baseCommitSha: branch.sha,
|
|
130
|
+
path: targetPath,
|
|
131
|
+
beforeContent,
|
|
132
|
+
beforeContentHash,
|
|
133
|
+
afterContent: beforeContent,
|
|
134
|
+
afterContentHash: beforeContentHash,
|
|
135
|
+
diff: "",
|
|
136
|
+
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
137
|
+
status: "error",
|
|
138
|
+
reason: errorReason,
|
|
139
|
+
addedKeys: [],
|
|
140
|
+
updatedKeys: [],
|
|
141
|
+
missingVars,
|
|
142
|
+
isNewFile,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const afterContentHash = sha256(afterContent);
|
|
147
|
+
const changed = afterContent !== beforeContent;
|
|
148
|
+
const diff = computeDiff(beforeContent, afterContent, targetPath);
|
|
149
|
+
const hash = computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, afterContentHash);
|
|
150
|
+
if (changed) {
|
|
151
|
+
branchChangedCount++;
|
|
152
|
+
}
|
|
153
|
+
plans.push({
|
|
154
|
+
branch,
|
|
155
|
+
baseCommitSha: branch.sha,
|
|
156
|
+
path: targetPath,
|
|
157
|
+
beforeContent,
|
|
158
|
+
beforeContentHash,
|
|
159
|
+
afterContent,
|
|
160
|
+
afterContentHash,
|
|
161
|
+
diff,
|
|
162
|
+
planHash: hash,
|
|
163
|
+
status: changed ? "changed" : "unchanged",
|
|
164
|
+
addedKeys: isNewFile ? [targetPath] : [],
|
|
165
|
+
updatedKeys: !isNewFile && changed ? [targetPath] : [],
|
|
166
|
+
isNewFile,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
41
169
|
}
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────
|
|
171
|
+
// 2. ENV RECONCILE ENGINE (.env)
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────
|
|
173
|
+
const readResult = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, targetPath);
|
|
174
|
+
const beforeContent = readResult.ok ? readResult.value : "";
|
|
46
175
|
const beforeContentHash = sha256(beforeContent);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
repoName: context.project.id,
|
|
51
|
-
commitSha: branch.sha,
|
|
52
|
-
});
|
|
53
|
-
if (!interpolated.ok) {
|
|
54
|
-
s.stop(chalk.red(`${branch.name} — interpolation error`));
|
|
176
|
+
const existingParse = parseEnvFile(beforeContent);
|
|
177
|
+
if (!existingParse.ok) {
|
|
178
|
+
branchErrorCount++;
|
|
55
179
|
plans.push({
|
|
56
180
|
branch,
|
|
57
181
|
baseCommitSha: branch.sha,
|
|
@@ -63,18 +187,55 @@ export const planChangesStage = async (context, deps) => {
|
|
|
63
187
|
diff: "",
|
|
64
188
|
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
65
189
|
status: "error",
|
|
66
|
-
reason:
|
|
190
|
+
reason: existingParse.error.message,
|
|
67
191
|
addedKeys: [],
|
|
68
192
|
updatedKeys: [],
|
|
69
|
-
isNewFile,
|
|
70
193
|
});
|
|
71
194
|
continue;
|
|
72
195
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
196
|
+
const existingMap = existingParse.value.map;
|
|
197
|
+
const branchValues = new Map();
|
|
198
|
+
let hadError = false;
|
|
199
|
+
let errorReason = "";
|
|
200
|
+
const envTemplate = template.parsedEnv ?? context.template;
|
|
201
|
+
if (envTemplate) {
|
|
202
|
+
for (const assignment of envTemplate.assignments) {
|
|
203
|
+
if (!assignment.isPlaceholder || !assignment.placeholderName)
|
|
204
|
+
continue;
|
|
205
|
+
if (existingMap.has(assignment.key)) {
|
|
206
|
+
branchValues.set(assignment.key, existingMap.get(assignment.key).value);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
if (isCi) {
|
|
210
|
+
hadError = true;
|
|
211
|
+
errorReason = `Missing value for '${assignment.key}' in CI mode`;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
s.stop(`${branch.name} — needs input for ${targetPath}`);
|
|
215
|
+
console.log("");
|
|
216
|
+
console.log(chalk.bold(` ${sym.branch} Branch: ${chalk.cyan(branch.name)}`));
|
|
217
|
+
console.log(chalk.yellow(` Missing key in ${targetPath}: ${chalk.bold(assignment.key)}`));
|
|
218
|
+
const value = await clack.text({
|
|
219
|
+
message: `Value for ${chalk.bold(assignment.key)} in ${chalk.cyan(branch.name)}:`,
|
|
220
|
+
placeholder: `enter value for ${assignment.key}`,
|
|
221
|
+
validate: (v) => {
|
|
222
|
+
if (!v || v.trim() === "")
|
|
223
|
+
return `Value cannot be empty`;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
if (clack.isCancel(value)) {
|
|
227
|
+
hadError = true;
|
|
228
|
+
errorReason = "User cancelled value input";
|
|
229
|
+
s.stop(chalk.yellow(`${branch.name} — skipped`));
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
branchValues.set(assignment.key, value);
|
|
233
|
+
s.start(`Planning ${branch.name}…`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (hadError) {
|
|
238
|
+
branchErrorCount++;
|
|
78
239
|
plans.push({
|
|
79
240
|
branch,
|
|
80
241
|
baseCommitSha: branch.sha,
|
|
@@ -89,21 +250,35 @@ export const planChangesStage = async (context, deps) => {
|
|
|
89
250
|
reason: errorReason,
|
|
90
251
|
addedKeys: [],
|
|
91
252
|
updatedKeys: [],
|
|
92
|
-
missingVars,
|
|
93
|
-
isNewFile,
|
|
94
253
|
});
|
|
95
254
|
continue;
|
|
96
255
|
}
|
|
97
|
-
|
|
256
|
+
const transformResult = transformEnv(beforeContent, envTemplate, branchValues);
|
|
257
|
+
if (!transformResult.ok) {
|
|
258
|
+
branchErrorCount++;
|
|
259
|
+
plans.push({
|
|
260
|
+
branch,
|
|
261
|
+
baseCommitSha: branch.sha,
|
|
262
|
+
path: targetPath,
|
|
263
|
+
beforeContent,
|
|
264
|
+
beforeContentHash,
|
|
265
|
+
afterContent: beforeContent,
|
|
266
|
+
afterContentHash: beforeContentHash,
|
|
267
|
+
diff: "",
|
|
268
|
+
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
269
|
+
status: "error",
|
|
270
|
+
reason: transformResult.error.message,
|
|
271
|
+
addedKeys: [],
|
|
272
|
+
updatedKeys: [],
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const { content: afterContent, changed, addedKeys, updatedKeys } = transformResult.value;
|
|
98
277
|
const afterContentHash = sha256(afterContent);
|
|
99
|
-
const changed = afterContent !== beforeContent;
|
|
100
278
|
const diff = computeDiff(beforeContent, afterContent, targetPath);
|
|
101
279
|
const hash = computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, afterContentHash);
|
|
102
280
|
if (changed) {
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
s.stop(chalk.gray(`${branch.name} — no changes`));
|
|
281
|
+
branchChangedCount++;
|
|
107
282
|
}
|
|
108
283
|
plans.push({
|
|
109
284
|
branch,
|
|
@@ -116,155 +291,24 @@ export const planChangesStage = async (context, deps) => {
|
|
|
116
291
|
diff,
|
|
117
292
|
planHash: hash,
|
|
118
293
|
status: changed ? "changed" : "unchanged",
|
|
119
|
-
addedKeys
|
|
120
|
-
updatedKeys
|
|
121
|
-
isNewFile,
|
|
294
|
+
addedKeys,
|
|
295
|
+
updatedKeys,
|
|
122
296
|
});
|
|
123
|
-
continue;
|
|
124
297
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// ─────────────────────────────────────────────────────────────────
|
|
128
|
-
const readResult = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, targetPath);
|
|
129
|
-
const beforeContent = readResult.ok ? readResult.value : "";
|
|
130
|
-
const beforeContentHash = sha256(beforeContent);
|
|
131
|
-
// Parse existing file to find which keys exist
|
|
132
|
-
const existingParse = parseEnvFile(beforeContent);
|
|
133
|
-
if (!existingParse.ok) {
|
|
134
|
-
s.stop(chalk.red(`${branch.name} — file parse error`));
|
|
135
|
-
plans.push({
|
|
136
|
-
branch,
|
|
137
|
-
baseCommitSha: branch.sha,
|
|
138
|
-
path: targetPath,
|
|
139
|
-
beforeContent,
|
|
140
|
-
beforeContentHash,
|
|
141
|
-
afterContent: beforeContent,
|
|
142
|
-
afterContentHash: beforeContentHash,
|
|
143
|
-
diff: "",
|
|
144
|
-
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
145
|
-
status: "error",
|
|
146
|
-
reason: existingParse.error.message,
|
|
147
|
-
addedKeys: [],
|
|
148
|
-
updatedKeys: [],
|
|
149
|
-
});
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const existingMap = existingParse.value.map;
|
|
153
|
-
const branchValues = new Map();
|
|
154
|
-
let hadError = false;
|
|
155
|
-
let errorReason = "";
|
|
156
|
-
const envTemplate = context.activeTemplate?.parsedEnv ?? context.template;
|
|
157
|
-
if (envTemplate) {
|
|
158
|
-
for (const assignment of envTemplate.assignments) {
|
|
159
|
-
if (!assignment.isPlaceholder || !assignment.placeholderName)
|
|
160
|
-
continue;
|
|
161
|
-
if (existingMap.has(assignment.key)) {
|
|
162
|
-
branchValues.set(assignment.key, existingMap.get(assignment.key).value);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
if (isCi) {
|
|
166
|
-
hadError = true;
|
|
167
|
-
errorReason = `Missing value for '${assignment.key}' in CI mode`;
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
s.stop(`${branch.name} — needs input`);
|
|
171
|
-
console.log("");
|
|
172
|
-
console.log(chalk.bold(` ${sym.branch} Branch: ${chalk.cyan(branch.name)}`));
|
|
173
|
-
console.log(chalk.yellow(` Missing key: ${chalk.bold(assignment.key)}`));
|
|
174
|
-
const value = await clack.text({
|
|
175
|
-
message: `Value for ${chalk.bold(assignment.key)} in ${chalk.cyan(branch.name)}:`,
|
|
176
|
-
placeholder: `enter value for ${assignment.key}`,
|
|
177
|
-
validate: (v) => {
|
|
178
|
-
if (!v || v.trim() === "")
|
|
179
|
-
return `Value cannot be empty`;
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
if (clack.isCancel(value)) {
|
|
183
|
-
hadError = true;
|
|
184
|
-
errorReason = "User cancelled value input";
|
|
185
|
-
s.stop(chalk.yellow(`${branch.name} — skipped`));
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
branchValues.set(assignment.key, value);
|
|
189
|
-
s.start(`Planning ${branch.name}…`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (hadError) {
|
|
194
|
-
s.stop(chalk.red(`${branch.name} — ${errorReason}`));
|
|
195
|
-
plans.push({
|
|
196
|
-
branch,
|
|
197
|
-
baseCommitSha: branch.sha,
|
|
198
|
-
path: targetPath,
|
|
199
|
-
beforeContent,
|
|
200
|
-
beforeContentHash,
|
|
201
|
-
afterContent: beforeContent,
|
|
202
|
-
afterContentHash: beforeContentHash,
|
|
203
|
-
diff: "",
|
|
204
|
-
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
205
|
-
status: "error",
|
|
206
|
-
reason: errorReason,
|
|
207
|
-
addedKeys: [],
|
|
208
|
-
updatedKeys: [],
|
|
209
|
-
});
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
// Transform
|
|
213
|
-
const transformResult = transformEnv(beforeContent, envTemplate, branchValues);
|
|
214
|
-
if (!transformResult.ok) {
|
|
215
|
-
s.stop(chalk.red(`${branch.name} — transform error`));
|
|
216
|
-
plans.push({
|
|
217
|
-
branch,
|
|
218
|
-
baseCommitSha: branch.sha,
|
|
219
|
-
path: targetPath,
|
|
220
|
-
beforeContent,
|
|
221
|
-
beforeContentHash,
|
|
222
|
-
afterContent: beforeContent,
|
|
223
|
-
afterContentHash: beforeContentHash,
|
|
224
|
-
diff: "",
|
|
225
|
-
planHash: computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, beforeContentHash),
|
|
226
|
-
status: "error",
|
|
227
|
-
reason: transformResult.error.message,
|
|
228
|
-
addedKeys: [],
|
|
229
|
-
updatedKeys: [],
|
|
230
|
-
});
|
|
231
|
-
continue;
|
|
298
|
+
if (branchErrorCount > 0) {
|
|
299
|
+
s.stop(chalk.red(`${branch.name} — ${branchErrorCount} error(s)`));
|
|
232
300
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const diff = computeDiff(beforeContent, afterContent, targetPath);
|
|
236
|
-
const hash = computePlanHash(branch.name, branch.sha, targetPath, beforeContentHash, afterContentHash);
|
|
237
|
-
if (changed) {
|
|
238
|
-
const summary = [
|
|
239
|
-
updatedKeys.length > 0 ? `${updatedKeys.length} updated` : "",
|
|
240
|
-
addedKeys.length > 0 ? `${addedKeys.length} added` : "",
|
|
241
|
-
]
|
|
242
|
-
.filter(Boolean)
|
|
243
|
-
.join(", ");
|
|
244
|
-
s.stop(chalk.green(`${branch.name} — ${summary || "changed"}`));
|
|
301
|
+
else if (branchChangedCount > 0) {
|
|
302
|
+
s.stop(chalk.green(`${branch.name} — ${branchChangedCount} file(s) changed`));
|
|
245
303
|
}
|
|
246
304
|
else {
|
|
247
305
|
s.stop(chalk.gray(`${branch.name} — no changes`));
|
|
248
306
|
}
|
|
249
|
-
plans.push({
|
|
250
|
-
branch,
|
|
251
|
-
baseCommitSha: branch.sha,
|
|
252
|
-
path: targetPath,
|
|
253
|
-
beforeContent,
|
|
254
|
-
beforeContentHash,
|
|
255
|
-
afterContent,
|
|
256
|
-
afterContentHash,
|
|
257
|
-
diff,
|
|
258
|
-
planHash: hash,
|
|
259
|
-
status: changed ? "changed" : "unchanged",
|
|
260
|
-
addedKeys,
|
|
261
|
-
updatedKeys,
|
|
262
|
-
});
|
|
263
307
|
}
|
|
264
308
|
const changed = plans.filter((p) => p.status === "changed").length;
|
|
265
309
|
const unchanged = plans.filter((p) => p.status === "unchanged").length;
|
|
266
310
|
const errors = plans.filter((p) => p.status === "error").length;
|
|
267
311
|
console.log("");
|
|
268
|
-
statusLine("ok", `Planning complete — ${changed}
|
|
312
|
+
statusLine("ok", `Planning complete — ${changed} file change(s) across branches (${unchanged} unchanged, ${errors} error(s))`);
|
|
269
313
|
return ok({ ...context, plans });
|
|
270
314
|
};
|
package/dist/stages/report.js
CHANGED
|
@@ -47,34 +47,39 @@ export const reportResultsStage = async (context, deps) => {
|
|
|
47
47
|
skipped > 0 ? chalk.gray(`${skipped} skipped`) : "",
|
|
48
48
|
].filter(Boolean).join(" "));
|
|
49
49
|
console.log("");
|
|
50
|
-
// Save template state
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
50
|
+
// Save template state for all edited templates
|
|
51
|
+
const templates = context.templates || (context.activeTemplate ? [context.activeTemplate] : []);
|
|
52
|
+
if (context.project && templates.length > 0) {
|
|
53
53
|
const summary = {
|
|
54
54
|
committed,
|
|
55
55
|
failed,
|
|
56
56
|
skipped: skipped + rejected,
|
|
57
57
|
total: results.length,
|
|
58
58
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
if (committed > 0) {
|
|
60
|
+
for (const t of templates) {
|
|
61
|
+
if (t.raw) {
|
|
62
|
+
const saveResult = await deps.services.state.saveTemplate(context.project.stateDir, t.path, t.raw, summary);
|
|
63
|
+
if (saveResult.ok) {
|
|
64
|
+
statusLine("ok", `Template state saved for '${t.path}' (ready for future runs)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const { clack } = await import("../utils/ui.js");
|
|
70
|
-
if (!context.input.ci) {
|
|
69
|
+
else if (!context.input.ci) {
|
|
70
|
+
// No commits — offer to save reviewed templates
|
|
71
|
+
const { clack } = await import("../utils/ui.js");
|
|
71
72
|
const save = await clack.confirm({
|
|
72
|
-
message:
|
|
73
|
+
message: `No changes committed. Save ${templates.length} reviewed template(s) for next run?`,
|
|
73
74
|
initialValue: true,
|
|
74
75
|
});
|
|
75
76
|
if (!clack.isCancel(save) && save) {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
for (const t of templates) {
|
|
78
|
+
if (t.raw) {
|
|
79
|
+
await deps.services.state.saveTemplate(context.project.stateDir, t.path, t.raw);
|
|
80
|
+
statusLine("ok", `Template saved for '${t.path}'`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
}
|
|
@@ -82,7 +87,7 @@ export const reportResultsStage = async (context, deps) => {
|
|
|
82
87
|
if (context.project && (deps.config.report.writeJson || deps.config.report.writeMarkdown)) {
|
|
83
88
|
const report = {
|
|
84
89
|
runId: context.runId,
|
|
85
|
-
targetPath: context.target?.path ?? "",
|
|
90
|
+
targetPath: context.target?.path ?? templates.map((t) => t.path).join(", "),
|
|
86
91
|
startedAt: new Date().toISOString(),
|
|
87
92
|
completedAt: new Date().toISOString(),
|
|
88
93
|
plans: context.plans ?? [],
|