@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,227 +0,0 @@
|
|
|
1
|
-
import { Stage } from "../core/pipeline.js";
|
|
2
|
-
import { RunContext, AppDeps, TargetTemplate, ok, err } from "../core/types.js";
|
|
3
|
-
import { workspaceError, inputCancelledError } from "../core/errors.js";
|
|
4
|
-
import { parseEnvTemplate } from "../services/template/env.js";
|
|
5
|
-
import { extractTemplateVariables } from "../services/template/interpolator.js";
|
|
6
|
-
import { validateTemplateSyntax } from "../services/template/validator.js";
|
|
7
|
-
import { clack, sectionHeader, statusLine } from "../utils/ui.js";
|
|
8
|
-
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
9
|
-
import { tmpdir } from "os";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import chalk from "chalk";
|
|
12
|
-
|
|
13
|
-
const HEADER_SENTINEL = "Git Worker — Template Editor";
|
|
14
|
-
|
|
15
|
-
function buildInstructionHeader(targetPath: string): string {
|
|
16
|
-
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
17
|
-
const isJson = targetPath.endsWith(".json");
|
|
18
|
-
|
|
19
|
-
// Avoid inserting comments into JSON files
|
|
20
|
-
if (isJson) return "";
|
|
21
|
-
|
|
22
|
-
if (isEnv) {
|
|
23
|
-
return [
|
|
24
|
-
"# ─────────────────────────────────────────────────────────────────",
|
|
25
|
-
`# ${HEADER_SENTINEL}`,
|
|
26
|
-
"# Use KEY=value → static value enforced on every branch",
|
|
27
|
-
"# Use KEY={KEY} → dynamic: reads existing value from each branch",
|
|
28
|
-
"# Lines starting with # are comments and are preserved in the file",
|
|
29
|
-
"# ─────────────────────────────────────────────────────────────────",
|
|
30
|
-
"",
|
|
31
|
-
].join("\n");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return [
|
|
35
|
-
"# ─────────────────────────────────────────────────────────────────",
|
|
36
|
-
`# ${HEADER_SENTINEL}`,
|
|
37
|
-
"# This template is the declarative SOURCE OF TRUTH for this file.",
|
|
38
|
-
"# Variables: ${VAR} or ${VAR:-default} are resolved from branch .env",
|
|
39
|
-
"# Built-ins: ${BRANCH_NAME}, ${BRANCH_SLUG}, ${REPO_NAME}, ${COMMIT_SHA}",
|
|
40
|
-
"# ─────────────────────────────────────────────────────────────────",
|
|
41
|
-
"",
|
|
42
|
-
].join("\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function stripInstructionHeader(content: string): string {
|
|
46
|
-
if (!content.includes(HEADER_SENTINEL)) {
|
|
47
|
-
return content;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const lines = content.split("\n");
|
|
51
|
-
let headerEnd = 0;
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < lines.length; i++) {
|
|
54
|
-
if (lines[i].includes(HEADER_SENTINEL)) {
|
|
55
|
-
let j = i + 1;
|
|
56
|
-
while (j < lines.length && (lines[j].startsWith("#") || lines[j].trim() === "")) {
|
|
57
|
-
j++;
|
|
58
|
-
}
|
|
59
|
-
headerEnd = j;
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return lines.slice(headerEnd).join("\n");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const editTemplateStage: Stage<RunContext, AppDeps> = async (context, deps) => {
|
|
68
|
-
if (!context.template && !context.activeTemplate) {
|
|
69
|
-
return err(workspaceError("No template loaded"));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const targetPath = context.target?.path ?? ".env";
|
|
73
|
-
const isEnv = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
74
|
-
let currentRaw = context.activeTemplate?.raw ?? context.template?.raw ?? "";
|
|
75
|
-
|
|
76
|
-
// In CI mode, validate syntax and proceed without interactive editor
|
|
77
|
-
if (context.input.ci) {
|
|
78
|
-
statusLine("info", "CI mode — skipping template editor");
|
|
79
|
-
const validation = validateTemplateSyntax(targetPath, currentRaw);
|
|
80
|
-
if (!validation.ok) {
|
|
81
|
-
clack.log.error(`Template syntax error: ${validation.error.message}`);
|
|
82
|
-
return validation;
|
|
83
|
-
}
|
|
84
|
-
return ok(context);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
sectionHeader("Edit Template");
|
|
88
|
-
|
|
89
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "git-worker-"));
|
|
90
|
-
const templateFileName = targetPath.split("/").pop() ?? "template.env";
|
|
91
|
-
const templateFile = join(tmpDir, templateFileName);
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
while (true) {
|
|
95
|
-
const header = buildInstructionHeader(targetPath);
|
|
96
|
-
const content = header ? `${header}${currentRaw}` : currentRaw;
|
|
97
|
-
await writeFile(templateFile, content, "utf-8");
|
|
98
|
-
|
|
99
|
-
const editorName = context.input.editor || deps.config.editor.name;
|
|
100
|
-
statusLine("info", `Opening ${targetPath} in ${editorName} — save and close when done`);
|
|
101
|
-
console.log(chalk.gray(` File: ${templateFile}`));
|
|
102
|
-
if (!header) {
|
|
103
|
-
console.log(
|
|
104
|
-
chalk.gray(
|
|
105
|
-
` Tip: Use \${VAR} or \${VAR:-default} to resolve variables from each branch's .env`
|
|
106
|
-
)
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
console.log("");
|
|
110
|
-
|
|
111
|
-
const editResult = await deps.services.editor.openForEdit(templateFile, deps.config);
|
|
112
|
-
if (!editResult.ok) {
|
|
113
|
-
clack.log.warn(editResult.error.message);
|
|
114
|
-
const proceed = await clack.confirm({
|
|
115
|
-
message: "Editor not available. Continue with current template?",
|
|
116
|
-
initialValue: true,
|
|
117
|
-
});
|
|
118
|
-
if (clack.isCancel(proceed) || !proceed) {
|
|
119
|
-
return err(inputCancelledError("Template edit cancelled — editor unavailable"));
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Read edited content
|
|
125
|
-
const edited = await readFile(templateFile, "utf-8");
|
|
126
|
-
const stripped = stripInstructionHeader(edited).trimEnd() + "\n";
|
|
127
|
-
|
|
128
|
-
// 1. Validate syntax immediately
|
|
129
|
-
const validation = validateTemplateSyntax(targetPath, stripped);
|
|
130
|
-
if (!validation.ok) {
|
|
131
|
-
console.log("");
|
|
132
|
-
clack.log.error(`Syntax Validation Failed:\n ${chalk.red(validation.error.message)}`);
|
|
133
|
-
console.log("");
|
|
134
|
-
|
|
135
|
-
const fixChoice = await clack.select({
|
|
136
|
-
message: "How would you like to handle this syntax error?",
|
|
137
|
-
options: [
|
|
138
|
-
{ value: "re-edit", label: "✏️ Re-edit template to fix the error" },
|
|
139
|
-
{ value: "discard", label: "❌ Discard changes and cancel" },
|
|
140
|
-
],
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (clack.isCancel(fixChoice) || fixChoice === "discard") {
|
|
144
|
-
return err(inputCancelledError("Template edit cancelled due to syntax error"));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
currentRaw = stripped;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 2. Syntax is valid!
|
|
152
|
-
currentRaw = stripped;
|
|
153
|
-
statusLine("ok", "Template syntax validated successfully");
|
|
154
|
-
|
|
155
|
-
// 3. Post-edit menu
|
|
156
|
-
const actionChoice = await clack.select({
|
|
157
|
-
message: "Template ready. What would you like to do next?",
|
|
158
|
-
options: [
|
|
159
|
-
{
|
|
160
|
-
value: "proceed",
|
|
161
|
-
label: "➜ Proceed to plan changes",
|
|
162
|
-
hint: "generate diffs across target branches",
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
value: "re-edit",
|
|
166
|
-
label: "✏️ Edit template again",
|
|
167
|
-
hint: "re-open in editor",
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
value: "discard",
|
|
171
|
-
label: "❌ Discard changes and cancel",
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (clack.isCancel(actionChoice) || actionChoice === "discard") {
|
|
177
|
-
return err(inputCancelledError("Template edit cancelled"));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (actionChoice === "re-edit") {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Update context with the validated template
|
|
188
|
-
if (isEnv) {
|
|
189
|
-
const parseResult = parseEnvTemplate(currentRaw);
|
|
190
|
-
if (!parseResult.ok) return parseResult;
|
|
191
|
-
const activeTemplate: TargetTemplate = {
|
|
192
|
-
path: targetPath,
|
|
193
|
-
engine: "env-reconcile",
|
|
194
|
-
raw: currentRaw,
|
|
195
|
-
parsedEnv: parseResult.value,
|
|
196
|
-
};
|
|
197
|
-
statusLine("ok", `Template ready — ${parseResult.value.assignments.length} assignment(s)`);
|
|
198
|
-
return ok({
|
|
199
|
-
...context,
|
|
200
|
-
template: parseResult.value,
|
|
201
|
-
activeTemplate,
|
|
202
|
-
templates: [activeTemplate],
|
|
203
|
-
});
|
|
204
|
-
} else {
|
|
205
|
-
const variables = extractTemplateVariables(currentRaw);
|
|
206
|
-
const activeTemplate: TargetTemplate = {
|
|
207
|
-
path: targetPath,
|
|
208
|
-
engine: "source-of-truth",
|
|
209
|
-
raw: currentRaw,
|
|
210
|
-
variables,
|
|
211
|
-
};
|
|
212
|
-
statusLine("ok", `Template ready — ${variables.length} variable placeholder(s) detected`);
|
|
213
|
-
return ok({
|
|
214
|
-
...context,
|
|
215
|
-
template: {
|
|
216
|
-
raw: currentRaw,
|
|
217
|
-
assignments: [],
|
|
218
|
-
placeholders: variables.map((v) => v.name),
|
|
219
|
-
},
|
|
220
|
-
activeTemplate,
|
|
221
|
-
templates: [activeTemplate],
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
} finally {
|
|
225
|
-
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
226
|
-
}
|
|
227
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Stage } from "../core/pipeline.js";
|
|
2
|
-
import { RunContext, AppDeps, BranchRef, ok, err } from "../core/types.js";
|
|
3
|
-
import { workspaceError } from "../core/errors.js";
|
|
4
|
-
import { clack, sectionHeader, statusLine } from "../utils/ui.js";
|
|
5
|
-
|
|
6
|
-
export const checkEligibilityStage: Stage<RunContext, AppDeps> = async (context, deps) => {
|
|
7
|
-
if (!context.selectedBranches || !context.target || !context.project) {
|
|
8
|
-
return err(workspaceError("Prerequisites missing for eligibility check"));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
sectionHeader("Eligibility Check");
|
|
12
|
-
|
|
13
|
-
const eligible: BranchRef[] = [];
|
|
14
|
-
const ineligible: Array<{ branch: BranchRef; reason: string }> = [];
|
|
15
|
-
const isEnvTarget =
|
|
16
|
-
context.target.path === ".env" || context.target.path.startsWith(".env.");
|
|
17
|
-
|
|
18
|
-
for (const branch of context.selectedBranches) {
|
|
19
|
-
// 1. Check if the target file exists in the branch
|
|
20
|
-
const targetRead = await deps.services.git.readFileFromBranch(
|
|
21
|
-
context.project.managedRepoPath,
|
|
22
|
-
branch,
|
|
23
|
-
context.target.path
|
|
24
|
-
);
|
|
25
|
-
const targetFound = targetRead.ok;
|
|
26
|
-
|
|
27
|
-
// 2. If target is a non-.env file, check if .env is available as variable source
|
|
28
|
-
let envNote = "";
|
|
29
|
-
if (!isEnvTarget) {
|
|
30
|
-
const envRead = await deps.services.git.readFileFromBranch(
|
|
31
|
-
context.project.managedRepoPath,
|
|
32
|
-
branch,
|
|
33
|
-
".env"
|
|
34
|
-
);
|
|
35
|
-
if (envRead.ok) {
|
|
36
|
-
envNote = " • .env present";
|
|
37
|
-
} else {
|
|
38
|
-
envNote = " • .env missing (variables will require fallbacks)";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const fileStatus = targetFound ? "target file found" : "target file will be created";
|
|
43
|
-
eligible.push(branch);
|
|
44
|
-
statusLine(
|
|
45
|
-
targetFound ? "ok" : "info",
|
|
46
|
-
branch.name,
|
|
47
|
-
`${branch.sha.slice(0, 8)} — ${fileStatus}${envNote}`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (ineligible.length > 0) {
|
|
52
|
-
console.log("");
|
|
53
|
-
clack.log.warn(`${ineligible.length} branch(es) ineligible:`);
|
|
54
|
-
for (const { branch, reason } of ineligible) {
|
|
55
|
-
statusLine("warn", branch.name, reason);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (eligible.length === 0) {
|
|
60
|
-
clack.log.error("No eligible branches found.");
|
|
61
|
-
return err(workspaceError("No eligible branches"));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
statusLine("ok", `${eligible.length} eligible branch(es) ready`);
|
|
65
|
-
|
|
66
|
-
return ok({ ...context, eligible });
|
|
67
|
-
};
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { Stage } from "../core/pipeline.js";
|
|
2
|
-
import { RunContext, AppDeps, TargetTemplate, ok, err } from "../core/types.js";
|
|
3
|
-
import { workspaceError } from "../core/errors.js";
|
|
4
|
-
import { parseEnvTemplate, synthesizeTemplate } from "../services/template/env.js";
|
|
5
|
-
import { extractTemplateVariables } from "../services/template/interpolator.js";
|
|
6
|
-
import { sectionHeader, statusLine } from "../utils/ui.js";
|
|
7
|
-
|
|
8
|
-
export const loadTemplateStage: Stage<RunContext, AppDeps> = async (context, deps) => {
|
|
9
|
-
if (!context.project || !context.target) {
|
|
10
|
-
return err(workspaceError("Prerequisites missing for template load"));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
sectionHeader("Template");
|
|
14
|
-
|
|
15
|
-
const targetPath = context.target.path;
|
|
16
|
-
const isEnvTarget = targetPath === ".env" || targetPath.startsWith(".env.");
|
|
17
|
-
|
|
18
|
-
// 1. Try to load saved template from ~/.git-worker state
|
|
19
|
-
const stateResult = await deps.services.state.loadTemplate(
|
|
20
|
-
context.project.stateDir,
|
|
21
|
-
targetPath
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
if (stateResult.ok && stateResult.value) {
|
|
25
|
-
const state = stateResult.value;
|
|
26
|
-
statusLine("ok", "Loaded saved template", `last updated ${state.updatedAt}`);
|
|
27
|
-
if (state.lastRunSummary) {
|
|
28
|
-
const s = state.lastRunSummary;
|
|
29
|
-
statusLine(
|
|
30
|
-
"info",
|
|
31
|
-
`Last run: ${s.committed} committed, ${s.failed} failed, ${s.skipped} skipped of ${s.total}`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (isEnvTarget) {
|
|
36
|
-
const parseResult = parseEnvTemplate(state.template);
|
|
37
|
-
if (parseResult.ok) {
|
|
38
|
-
const activeTemplate: TargetTemplate = {
|
|
39
|
-
path: targetPath,
|
|
40
|
-
engine: "env-reconcile",
|
|
41
|
-
raw: state.template,
|
|
42
|
-
parsedEnv: parseResult.value,
|
|
43
|
-
};
|
|
44
|
-
return ok({
|
|
45
|
-
...context,
|
|
46
|
-
template: parseResult.value,
|
|
47
|
-
activeTemplate,
|
|
48
|
-
templates: [activeTemplate],
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
const activeTemplate: TargetTemplate = {
|
|
53
|
-
path: targetPath,
|
|
54
|
-
engine: "source-of-truth",
|
|
55
|
-
raw: state.template,
|
|
56
|
-
variables: extractTemplateVariables(state.template),
|
|
57
|
-
};
|
|
58
|
-
return ok({
|
|
59
|
-
...context,
|
|
60
|
-
template: {
|
|
61
|
-
raw: state.template,
|
|
62
|
-
assignments: [],
|
|
63
|
-
placeholders: activeTemplate.variables?.map((v) => v.name) ?? [],
|
|
64
|
-
},
|
|
65
|
-
activeTemplate,
|
|
66
|
-
templates: [activeTemplate],
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
statusLine("warn", "Saved template was invalid — synthesizing a new one");
|
|
71
|
-
} else {
|
|
72
|
-
statusLine("info", "No saved template — synthesizing first-run draft");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 2. First run: synthesize from existing branch content
|
|
76
|
-
if (context.eligible && context.eligible.length > 0 && context.project) {
|
|
77
|
-
if (isEnvTarget) {
|
|
78
|
-
const contents: Array<{ branchName: string; content: string }> = [];
|
|
79
|
-
|
|
80
|
-
for (const branch of context.eligible.slice(0, 5)) {
|
|
81
|
-
const readResult = await deps.services.git.readFileFromBranch(
|
|
82
|
-
context.project.managedRepoPath,
|
|
83
|
-
branch,
|
|
84
|
-
targetPath
|
|
85
|
-
);
|
|
86
|
-
if (readResult.ok) {
|
|
87
|
-
contents.push({ branchName: branch.name, content: readResult.value });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const synthesized = synthesizeTemplate(contents);
|
|
92
|
-
const parseResult = parseEnvTemplate(synthesized);
|
|
93
|
-
if (parseResult.ok) {
|
|
94
|
-
statusLine("ok", "Synthesized .env template from branch content");
|
|
95
|
-
const activeTemplate: TargetTemplate = {
|
|
96
|
-
path: targetPath,
|
|
97
|
-
engine: "env-reconcile",
|
|
98
|
-
raw: synthesized,
|
|
99
|
-
parsedEnv: parseResult.value,
|
|
100
|
-
};
|
|
101
|
-
return ok({
|
|
102
|
-
...context,
|
|
103
|
-
template: parseResult.value,
|
|
104
|
-
activeTemplate,
|
|
105
|
-
templates: [activeTemplate],
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
// Source-of-truth: read sample from first branch that has the file
|
|
110
|
-
for (const branch of context.eligible) {
|
|
111
|
-
const readResult = await deps.services.git.readFileFromBranch(
|
|
112
|
-
context.project.managedRepoPath,
|
|
113
|
-
branch,
|
|
114
|
-
targetPath
|
|
115
|
-
);
|
|
116
|
-
if (readResult.ok) {
|
|
117
|
-
statusLine("ok", `Loaded draft template from branch '${branch.name}'`);
|
|
118
|
-
const activeTemplate: TargetTemplate = {
|
|
119
|
-
path: targetPath,
|
|
120
|
-
engine: "source-of-truth",
|
|
121
|
-
raw: readResult.value,
|
|
122
|
-
variables: extractTemplateVariables(readResult.value),
|
|
123
|
-
};
|
|
124
|
-
return ok({
|
|
125
|
-
...context,
|
|
126
|
-
template: {
|
|
127
|
-
raw: readResult.value,
|
|
128
|
-
assignments: [],
|
|
129
|
-
placeholders: activeTemplate.variables?.map((v) => v.name) ?? [],
|
|
130
|
-
},
|
|
131
|
-
activeTemplate,
|
|
132
|
-
templates: [activeTemplate],
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 3. Fallback starters for new files
|
|
140
|
-
let starterContent = "";
|
|
141
|
-
if (isEnvTarget) {
|
|
142
|
-
starterContent =
|
|
143
|
-
"# Git Worker Template\n# Use KEY=value for static values enforced on all branches\n# Use KEY={KEY} to read existing value from each branch\n";
|
|
144
|
-
const parseResult = parseEnvTemplate(starterContent);
|
|
145
|
-
if (!parseResult.ok) return parseResult;
|
|
146
|
-
const activeTemplate: TargetTemplate = {
|
|
147
|
-
path: targetPath,
|
|
148
|
-
engine: "env-reconcile",
|
|
149
|
-
raw: starterContent,
|
|
150
|
-
parsedEnv: parseResult.value,
|
|
151
|
-
};
|
|
152
|
-
statusLine("info", "Using starter template for .env");
|
|
153
|
-
return ok({
|
|
154
|
-
...context,
|
|
155
|
-
template: parseResult.value,
|
|
156
|
-
activeTemplate,
|
|
157
|
-
templates: [activeTemplate],
|
|
158
|
-
});
|
|
159
|
-
} else if (targetPath.endsWith(".json")) {
|
|
160
|
-
starterContent = `{\n "name": "\${BRANCH_SLUG}",\n "version": "1.0.0"\n}\n`;
|
|
161
|
-
} else if (targetPath.endsWith(".yaml") || targetPath.endsWith(".yml")) {
|
|
162
|
-
starterContent = `# Git Worker YAML Template\nname: "\${BRANCH_SLUG}"\nservice:\n port: \${PORT:-8080}\n`;
|
|
163
|
-
} else if (targetPath.toLowerCase().includes("dockerfile")) {
|
|
164
|
-
starterContent = `# Git Worker Dockerfile Template\nFROM node:20-alpine\nENV SERVICE=\${BRANCH_SLUG}\nEXPOSE \${PORT:-8080}\n`;
|
|
165
|
-
} else {
|
|
166
|
-
starterContent = `# Git Worker Template for ${targetPath}\n`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
statusLine("info", `Using starter template for ${targetPath}`);
|
|
170
|
-
const activeTemplate: TargetTemplate = {
|
|
171
|
-
path: targetPath,
|
|
172
|
-
engine: "source-of-truth",
|
|
173
|
-
raw: starterContent,
|
|
174
|
-
variables: extractTemplateVariables(starterContent),
|
|
175
|
-
isNewFile: true,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
return ok({
|
|
179
|
-
...context,
|
|
180
|
-
template: {
|
|
181
|
-
raw: starterContent,
|
|
182
|
-
assignments: [],
|
|
183
|
-
placeholders: activeTemplate.variables?.map((v) => v.name) ?? [],
|
|
184
|
-
},
|
|
185
|
-
activeTemplate,
|
|
186
|
-
templates: [activeTemplate],
|
|
187
|
-
});
|
|
188
|
-
};
|