@samaksh/git-worker 0.2.3 → 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/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/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 +1 -1
|
@@ -3,7 +3,7 @@ import { workspaceError, inputCancelledError } from "../core/errors.js";
|
|
|
3
3
|
import { parseEnvTemplate } from "../services/template/env.js";
|
|
4
4
|
import { extractTemplateVariables } from "../services/template/interpolator.js";
|
|
5
5
|
import { validateTemplateSyntax } from "../services/template/validator.js";
|
|
6
|
-
import { clack, sectionHeader, statusLine } from "../utils/ui.js";
|
|
6
|
+
import { clack, sectionHeader, statusLine, divider } from "../utils/ui.js";
|
|
7
7
|
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
9
|
import { join } from "path";
|
|
@@ -54,143 +54,199 @@ function stripInstructionHeader(content) {
|
|
|
54
54
|
}
|
|
55
55
|
return lines.slice(headerEnd).join("\n");
|
|
56
56
|
}
|
|
57
|
+
async function editSingleTemplate(template, index, total, tmpDir, deps, context) {
|
|
58
|
+
const targetPath = template.path;
|
|
59
|
+
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
60
|
+
let currentRaw = template.raw;
|
|
61
|
+
const safeFileName = targetPath.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
62
|
+
const templateFile = join(tmpDir, `${index + 1}_${safeFileName}`);
|
|
63
|
+
while (true) {
|
|
64
|
+
const header = buildInstructionHeader(targetPath);
|
|
65
|
+
const content = header ? `${header}${currentRaw}` : currentRaw;
|
|
66
|
+
await writeFile(templateFile, content, "utf-8");
|
|
67
|
+
const editorName = context.input.editor || deps.config.editor.name;
|
|
68
|
+
statusLine("info", `[${index + 1}/${total}] Opening ${chalk.bold.cyan(targetPath)} in ${editorName}`);
|
|
69
|
+
console.log(chalk.gray(` Path: ${templateFile}`));
|
|
70
|
+
if (template.description) {
|
|
71
|
+
console.log(chalk.gray(` Purpose: ${template.description}`));
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
const editResult = await deps.services.editor.openForEdit(templateFile, deps.config);
|
|
75
|
+
if (!editResult.ok) {
|
|
76
|
+
clack.log.warn(editResult.error.message);
|
|
77
|
+
const proceed = await clack.confirm({
|
|
78
|
+
message: `Editor unavailable for ${targetPath}. Continue with current content?`,
|
|
79
|
+
initialValue: true,
|
|
80
|
+
});
|
|
81
|
+
if (clack.isCancel(proceed) || !proceed) {
|
|
82
|
+
return { ok: false, error: inputCancelledError(`Template edit cancelled for ${targetPath}`) };
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// Read edited content
|
|
87
|
+
const edited = await readFile(templateFile, "utf-8");
|
|
88
|
+
const stripped = stripInstructionHeader(edited).trimEnd() + "\n";
|
|
89
|
+
// 1. Validate syntax immediately
|
|
90
|
+
const validation = validateTemplateSyntax(targetPath, stripped);
|
|
91
|
+
if (!validation.ok) {
|
|
92
|
+
console.log("");
|
|
93
|
+
clack.log.error(`Syntax Validation Failed for ${targetPath}:\n ${chalk.red(validation.error.message)}`);
|
|
94
|
+
console.log("");
|
|
95
|
+
const fixChoice = await clack.select({
|
|
96
|
+
message: "How would you like to handle this syntax error?",
|
|
97
|
+
options: [
|
|
98
|
+
{ value: "re-edit", label: "✏️ Re-edit template to fix the error" },
|
|
99
|
+
{ value: "discard", label: "❌ Discard changes and cancel" },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
if (clack.isCancel(fixChoice) || fixChoice === "discard") {
|
|
103
|
+
return { ok: false, error: inputCancelledError(`Template edit cancelled for ${targetPath}`) };
|
|
104
|
+
}
|
|
105
|
+
currentRaw = stripped;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// 2. Syntax is valid
|
|
109
|
+
currentRaw = stripped;
|
|
110
|
+
statusLine("ok", `Syntax validated for ${chalk.bold.cyan(targetPath)}`);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
// Build updated TargetTemplate
|
|
114
|
+
if (isEnv) {
|
|
115
|
+
const parseResult = parseEnvTemplate(currentRaw);
|
|
116
|
+
if (!parseResult.ok) {
|
|
117
|
+
return { ok: false, error: parseResult.error };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
template: {
|
|
122
|
+
...template,
|
|
123
|
+
raw: currentRaw,
|
|
124
|
+
parsedEnv: parseResult.value,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const variables = extractTemplateVariables(currentRaw);
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
template: {
|
|
133
|
+
...template,
|
|
134
|
+
raw: currentRaw,
|
|
135
|
+
variables,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
57
140
|
export const editTemplateStage = async (context, deps) => {
|
|
58
|
-
|
|
59
|
-
|
|
141
|
+
const templates = context.templates || (context.activeTemplate ? [context.activeTemplate] : []);
|
|
142
|
+
if (templates.length === 0) {
|
|
143
|
+
return err(workspaceError("No templates loaded for editing"));
|
|
60
144
|
}
|
|
61
|
-
|
|
62
|
-
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
63
|
-
let currentRaw = context.activeTemplate?.raw ?? context.template?.raw ?? "";
|
|
64
|
-
// In CI mode, validate syntax and proceed without interactive editor
|
|
145
|
+
// In CI mode, validate all templates and proceed
|
|
65
146
|
if (context.input.ci) {
|
|
66
|
-
statusLine("info",
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
147
|
+
statusLine("info", `CI mode — validating syntax for ${templates.length} template(s)`);
|
|
148
|
+
for (const t of templates) {
|
|
149
|
+
const validation = validateTemplateSyntax(t.path, t.raw);
|
|
150
|
+
if (!validation.ok) {
|
|
151
|
+
clack.log.error(`Template syntax error in ${t.path}: ${validation.error.message}`);
|
|
152
|
+
return validation;
|
|
153
|
+
}
|
|
71
154
|
}
|
|
72
155
|
return ok(context);
|
|
73
156
|
}
|
|
74
|
-
sectionHeader(
|
|
157
|
+
sectionHeader(`Edit Template(s) (${templates.length} file(s) selected)`);
|
|
75
158
|
const tmpDir = await mkdtemp(join(tmpdir(), "git-worker-"));
|
|
76
|
-
const
|
|
77
|
-
const templateFile = join(tmpDir, templateFileName);
|
|
159
|
+
const updatedTemplates = [];
|
|
78
160
|
try {
|
|
161
|
+
for (let i = 0; i < templates.length; i++) {
|
|
162
|
+
const template = templates[i];
|
|
163
|
+
const res = await editSingleTemplate(template, i, templates.length, tmpDir, deps, context);
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
return err(res.error);
|
|
166
|
+
}
|
|
167
|
+
updatedTemplates.push(res.template);
|
|
168
|
+
}
|
|
169
|
+
// Interactive summary and post-edit menu if multiple templates
|
|
79
170
|
while (true) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
console.log(chalk.gray(` Tip: Use \${VAR} or \${VAR:-default} to resolve variables from each branch's .env`));
|
|
171
|
+
divider();
|
|
172
|
+
console.log(chalk.bold(" Edited Templates Summary:"));
|
|
173
|
+
for (const t of updatedTemplates) {
|
|
174
|
+
const details = t.engine === "env-reconcile"
|
|
175
|
+
? `${t.parsedEnv?.assignments.length ?? 0} assignment(s)`
|
|
176
|
+
: `${t.variables?.length ?? 0} variable placeholder(s)`;
|
|
177
|
+
statusLine("ok", t.path, details);
|
|
88
178
|
}
|
|
89
179
|
console.log("");
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
clack.log.warn(editResult.error.message);
|
|
93
|
-
const proceed = await clack.confirm({
|
|
94
|
-
message: "Editor not available. Continue with current template?",
|
|
95
|
-
initialValue: true,
|
|
96
|
-
});
|
|
97
|
-
if (clack.isCancel(proceed) || !proceed) {
|
|
98
|
-
return err(inputCancelledError("Template edit cancelled — editor unavailable"));
|
|
99
|
-
}
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
// Read edited content
|
|
103
|
-
const edited = await readFile(templateFile, "utf-8");
|
|
104
|
-
const stripped = stripInstructionHeader(edited).trimEnd() + "\n";
|
|
105
|
-
// 1. Validate syntax immediately
|
|
106
|
-
const validation = validateTemplateSyntax(targetPath, stripped);
|
|
107
|
-
if (!validation.ok) {
|
|
108
|
-
console.log("");
|
|
109
|
-
clack.log.error(`Syntax Validation Failed:\n ${chalk.red(validation.error.message)}`);
|
|
110
|
-
console.log("");
|
|
111
|
-
const fixChoice = await clack.select({
|
|
112
|
-
message: "How would you like to handle this syntax error?",
|
|
113
|
-
options: [
|
|
114
|
-
{ value: "re-edit", label: "✏️ Re-edit template to fix the error" },
|
|
115
|
-
{ value: "discard", label: "❌ Discard changes and cancel" },
|
|
116
|
-
],
|
|
117
|
-
});
|
|
118
|
-
if (clack.isCancel(fixChoice) || fixChoice === "discard") {
|
|
119
|
-
return err(inputCancelledError("Template edit cancelled due to syntax error"));
|
|
120
|
-
}
|
|
121
|
-
currentRaw = stripped;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
// 2. Syntax is valid!
|
|
125
|
-
currentRaw = stripped;
|
|
126
|
-
statusLine("ok", "Template syntax validated successfully");
|
|
127
|
-
// 3. Post-edit menu
|
|
128
|
-
const actionChoice = await clack.select({
|
|
129
|
-
message: "Template ready. What would you like to do next?",
|
|
180
|
+
const action = await clack.select({
|
|
181
|
+
message: "All templates reviewed. What would you like to do next?",
|
|
130
182
|
options: [
|
|
131
183
|
{
|
|
132
184
|
value: "proceed",
|
|
133
185
|
label: "➜ Proceed to plan changes",
|
|
134
186
|
hint: "generate diffs across target branches",
|
|
135
187
|
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
...(updatedTemplates.length > 1
|
|
189
|
+
? [
|
|
190
|
+
{
|
|
191
|
+
value: "re-edit-one",
|
|
192
|
+
label: "✏️ Re-edit a specific file",
|
|
193
|
+
hint: "choose one file to modify again",
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
: [
|
|
197
|
+
{
|
|
198
|
+
value: "re-edit-first",
|
|
199
|
+
label: "✏️ Re-edit template",
|
|
200
|
+
hint: "re-open in editor",
|
|
201
|
+
},
|
|
202
|
+
]),
|
|
141
203
|
{
|
|
142
204
|
value: "discard",
|
|
143
205
|
label: "❌ Discard changes and cancel",
|
|
144
206
|
},
|
|
145
207
|
],
|
|
146
208
|
});
|
|
147
|
-
if (clack.isCancel(
|
|
148
|
-
return err(inputCancelledError("Template
|
|
209
|
+
if (clack.isCancel(action) || action === "discard") {
|
|
210
|
+
return err(inputCancelledError("Template editing cancelled"));
|
|
149
211
|
}
|
|
150
|
-
if (
|
|
212
|
+
if (action === "proceed") {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (action === "re-edit-first") {
|
|
216
|
+
const res = await editSingleTemplate(updatedTemplates[0], 0, 1, tmpDir, deps, context);
|
|
217
|
+
if (!res.ok)
|
|
218
|
+
return err(res.error);
|
|
219
|
+
updatedTemplates[0] = res.template;
|
|
151
220
|
continue;
|
|
152
221
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
template: parseResult.value,
|
|
170
|
-
activeTemplate,
|
|
171
|
-
templates: [activeTemplate],
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
const variables = extractTemplateVariables(currentRaw);
|
|
176
|
-
const activeTemplate = {
|
|
177
|
-
path: targetPath,
|
|
178
|
-
engine: "source-of-truth",
|
|
179
|
-
raw: currentRaw,
|
|
180
|
-
variables,
|
|
181
|
-
};
|
|
182
|
-
statusLine("ok", `Template ready — ${variables.length} variable placeholder(s) detected`);
|
|
183
|
-
return ok({
|
|
184
|
-
...context,
|
|
185
|
-
template: {
|
|
186
|
-
raw: currentRaw,
|
|
187
|
-
assignments: [],
|
|
188
|
-
placeholders: variables.map((v) => v.name),
|
|
189
|
-
},
|
|
190
|
-
activeTemplate,
|
|
191
|
-
templates: [activeTemplate],
|
|
192
|
-
});
|
|
222
|
+
if (action === "re-edit-one") {
|
|
223
|
+
const pickedPath = await clack.select({
|
|
224
|
+
message: "Select file to re-edit:",
|
|
225
|
+
options: updatedTemplates.map((t, idx) => ({
|
|
226
|
+
value: idx,
|
|
227
|
+
label: t.path,
|
|
228
|
+
})),
|
|
229
|
+
});
|
|
230
|
+
if (!clack.isCancel(pickedPath)) {
|
|
231
|
+
const idx = Number(pickedPath);
|
|
232
|
+
const res = await editSingleTemplate(updatedTemplates[idx], idx, updatedTemplates.length, tmpDir, deps, context);
|
|
233
|
+
if (!res.ok)
|
|
234
|
+
return err(res.error);
|
|
235
|
+
updatedTemplates[idx] = res.template;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
193
238
|
}
|
|
239
|
+
const primaryTemplate = updatedTemplates[0];
|
|
240
|
+
return ok({
|
|
241
|
+
...context,
|
|
242
|
+
templates: updatedTemplates,
|
|
243
|
+
activeTemplate: primaryTemplate,
|
|
244
|
+
template: primaryTemplate.parsedEnv ?? {
|
|
245
|
+
raw: primaryTemplate.raw,
|
|
246
|
+
assignments: [],
|
|
247
|
+
placeholders: primaryTemplate.variables?.map((v) => v.name) ?? [],
|
|
248
|
+
},
|
|
249
|
+
});
|
|
194
250
|
}
|
|
195
251
|
finally {
|
|
196
252
|
await rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { ok, err } from "../core/types.js";
|
|
2
2
|
import { workspaceError } from "../core/errors.js";
|
|
3
|
+
import { parseEnvFile } from "../services/template/env.js";
|
|
3
4
|
import { clack, sectionHeader, statusLine } from "../utils/ui.js";
|
|
4
5
|
export const checkEligibilityStage = async (context, deps) => {
|
|
5
|
-
|
|
6
|
+
const targets = context.targets || (context.target ? [context.target] : []);
|
|
7
|
+
if (!context.selectedBranches || targets.length === 0 || !context.project) {
|
|
6
8
|
return err(workspaceError("Prerequisites missing for eligibility check"));
|
|
7
9
|
}
|
|
8
10
|
sectionHeader("Eligibility Check");
|
|
9
11
|
const eligible = [];
|
|
10
12
|
const ineligible = [];
|
|
11
|
-
const
|
|
13
|
+
const hasEnvTarget = targets.some((t) => t.path === ".env" || t.path.startsWith(".env."));
|
|
12
14
|
const total = context.selectedBranches.length;
|
|
13
15
|
const isBatchMode = total > 8;
|
|
14
16
|
const s = clack.spinner();
|
|
15
17
|
if (isBatchMode) {
|
|
16
|
-
s.start(`Checking eligibility for ${total} branches…`);
|
|
18
|
+
s.start(`Checking eligibility for ${total} branches across ${targets.length} target file(s)…`);
|
|
17
19
|
}
|
|
18
20
|
// Concurrent execution pool (up to 16 workers in parallel)
|
|
19
21
|
const concurrency = Math.min(16, total);
|
|
@@ -23,28 +25,55 @@ export const checkEligibilityStage = async (context, deps) => {
|
|
|
23
25
|
while (currentIndex < total) {
|
|
24
26
|
const idx = currentIndex++;
|
|
25
27
|
const branch = context.selectedBranches[idx];
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
// Read branch .env if available to resolve dynamic paths or check variable availability
|
|
29
|
+
let envRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, ".env");
|
|
30
|
+
const envMap = new Map();
|
|
31
|
+
if (envRead.ok) {
|
|
32
|
+
const parsed = parseEnvFile(envRead.value);
|
|
33
|
+
if (parsed.ok) {
|
|
34
|
+
for (const [k, v] of parsed.value.map) {
|
|
35
|
+
envMap.set(k.toLowerCase(), v.value);
|
|
36
|
+
envMap.set(k, v.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let existingFileCount = 0;
|
|
41
|
+
let missingDynamicVar;
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
let checkPath = target.path;
|
|
44
|
+
if (target.isDynamic && (target.dynamicVar || target.path.includes("{servicename}"))) {
|
|
45
|
+
const varName = target.dynamicVar || "servicename";
|
|
46
|
+
const resolvedVal = envMap.get(varName) ||
|
|
47
|
+
envMap.get(varName.toLowerCase()) ||
|
|
48
|
+
envMap.get(varName.toUpperCase()) ||
|
|
49
|
+
envMap.get("SERVICE_NAME");
|
|
50
|
+
if (resolvedVal) {
|
|
51
|
+
checkPath = target.path.replace(`{${varName}}`, resolvedVal);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
missingDynamicVar = varName;
|
|
55
|
+
}
|
|
35
56
|
}
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
const targetRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, checkPath);
|
|
58
|
+
if (targetRead.ok) {
|
|
59
|
+
existingFileCount++;
|
|
38
60
|
}
|
|
39
61
|
}
|
|
40
|
-
|
|
62
|
+
let envNote = "";
|
|
63
|
+
if (!hasEnvTarget) {
|
|
64
|
+
envNote = envRead.ok ? " • .env present" : " • .env missing";
|
|
65
|
+
}
|
|
66
|
+
if (missingDynamicVar) {
|
|
67
|
+
envNote += ` • note: '${missingDynamicVar}' missing in .env`;
|
|
68
|
+
}
|
|
69
|
+
const fileStatus = `${existingFileCount}/${targets.length} target file(s) found`;
|
|
41
70
|
eligible.push(branch);
|
|
42
71
|
completed++;
|
|
43
72
|
if (isBatchMode) {
|
|
44
73
|
s.message(`Checking eligibility (${completed}/${total})…`);
|
|
45
74
|
}
|
|
46
75
|
else {
|
|
47
|
-
statusLine(
|
|
76
|
+
statusLine("ok", branch.name, `${branch.sha.slice(0, 8)} — ${fileStatus}${envNote}`);
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
79
|
});
|