@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
package/demo/index.ts
DELETED
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* git-worker demo / functional verification script
|
|
4
|
-
*
|
|
5
|
-
* Sets up a realistic local Git repository with 4 branches, each containing
|
|
6
|
-
* a .env file with different values, then runs the full git-worker pipeline
|
|
7
|
-
* against it in non-interactive (--ci --auto-approve) mode so you can verify
|
|
8
|
-
* every stage end-to-end without clicking through prompts.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* npx tsx demo/index.ts # full interactive run
|
|
12
|
-
* npx tsx demo/index.ts --ci # non-interactive (CI) run
|
|
13
|
-
* npx tsx demo/index.ts --clean # remove demo repo only
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { execSync, spawnSync } from "child_process";
|
|
17
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "fs";
|
|
18
|
-
import { join, resolve } from "path";
|
|
19
|
-
import { tmpdir, homedir } from "os";
|
|
20
|
-
import { createHash } from "crypto";
|
|
21
|
-
import chalk from "chalk";
|
|
22
|
-
|
|
23
|
-
// ── Config ─────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
const DEMO_DIR = join(tmpdir(), "git-worker-demo");
|
|
26
|
-
const BARE_REPO = join(DEMO_DIR, "remote.git"); // acts as the "remote"
|
|
27
|
-
const WORK_REPO = join(DEMO_DIR, "workspace"); // working clone for branch setup
|
|
28
|
-
|
|
29
|
-
// ── Arg parsing ────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
const args = process.argv.slice(2);
|
|
32
|
-
const isCI = args.includes("--ci");
|
|
33
|
-
const cleanOnly = args.includes("--clean");
|
|
34
|
-
|
|
35
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
function section(title: string) {
|
|
38
|
-
console.log("");
|
|
39
|
-
console.log(chalk.bold.cyan(" ══════════════════════════════════════════════"));
|
|
40
|
-
console.log(chalk.bold.cyan(` ${title}`));
|
|
41
|
-
console.log(chalk.bold.cyan(" ══════════════════════════════════════════════"));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function git(args: string[], cwd: string): string {
|
|
45
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
46
|
-
if (result.status !== 0) {
|
|
47
|
-
throw new Error(`git ${args.join(" ")} failed:\n${result.stderr}`);
|
|
48
|
-
}
|
|
49
|
-
return result.stdout.trim();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function step(msg: string) {
|
|
53
|
-
console.log(chalk.gray(` → ${msg}`));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function ok(msg: string) {
|
|
57
|
-
console.log(chalk.green(` ✓ ${msg}`));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ── Branches + .env content ────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The demo repo models a microservices monorepo with four UAT branches.
|
|
64
|
-
* Each branch has a .env file with:
|
|
65
|
-
* - SERVICE_NAME (varies per branch — will become a dynamic placeholder)
|
|
66
|
-
* - DB_PORT (varies — will become a dynamic placeholder)
|
|
67
|
-
* - DB_NAME (same on all — will become a static value)
|
|
68
|
-
* - LOG_LEVEL (missing on uat/orders — triggers per-branch prompt in interactive mode)
|
|
69
|
-
* - CUSTOM_FLAG (unmanaged key — must be preserved)
|
|
70
|
-
*/
|
|
71
|
-
const BRANCHES: Array<{
|
|
72
|
-
name: string;
|
|
73
|
-
env: string;
|
|
74
|
-
note: string;
|
|
75
|
-
}> = [
|
|
76
|
-
{
|
|
77
|
-
name: "uat/payments",
|
|
78
|
-
env: `# UAT payments service
|
|
79
|
-
DB_NAME=uat_db
|
|
80
|
-
DB_PORT=54321
|
|
81
|
-
SERVICE_NAME=payments
|
|
82
|
-
LOG_LEVEL=info
|
|
83
|
-
CUSTOM_FLAG=payments-only-feature
|
|
84
|
-
`,
|
|
85
|
-
note: "All keys present",
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: "uat/orders",
|
|
89
|
-
env: `# UAT orders service
|
|
90
|
-
DB_NAME=uat_db
|
|
91
|
-
DB_PORT=54322
|
|
92
|
-
SERVICE_NAME=orders
|
|
93
|
-
# LOG_LEVEL is intentionally missing here — will trigger a prompt in interactive mode
|
|
94
|
-
CUSTOM_FLAG=orders-flag
|
|
95
|
-
`,
|
|
96
|
-
note: "LOG_LEVEL missing (will prompt or fail in CI)",
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: "uat/inventory",
|
|
100
|
-
env: `# UAT inventory service
|
|
101
|
-
DB_NAME=uat_db
|
|
102
|
-
DB_PORT=54323
|
|
103
|
-
SERVICE_NAME=inventory
|
|
104
|
-
LOG_LEVEL=warn
|
|
105
|
-
CUSTOM_FLAG=inventory-flag
|
|
106
|
-
export LEGACY_VAR=keep-export-prefix
|
|
107
|
-
`,
|
|
108
|
-
note: "Has 'export' prefix on one key — must be preserved",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: "uat/notifications",
|
|
112
|
-
env: `# UAT notifications service
|
|
113
|
-
DB_NAME=legacy_db
|
|
114
|
-
DB_PORT=54324
|
|
115
|
-
SERVICE_NAME=notifications
|
|
116
|
-
LOG_LEVEL=debug
|
|
117
|
-
CUSTOM_FLAG=notif-flag
|
|
118
|
-
`,
|
|
119
|
-
note: "DB_NAME is wrong ('legacy_db') — static template will correct it",
|
|
120
|
-
},
|
|
121
|
-
];
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The template we'll use.
|
|
125
|
-
* - DB_NAME=uat_db → static: enforced on all branches
|
|
126
|
-
* - DB_PORT={DB_PORT} → dynamic: read per branch
|
|
127
|
-
* - SERVICE_NAME={SERVICE_NAME} → dynamic
|
|
128
|
-
* - LOG_LEVEL={LOG_LEVEL} → dynamic (missing on uat/orders — prompts in interactive)
|
|
129
|
-
* CUSTOM_FLAG is not declared → preserved untouched on each branch
|
|
130
|
-
*/
|
|
131
|
-
const TEMPLATE = `# Git Worker Demo Template
|
|
132
|
-
# Static values are enforced on every branch
|
|
133
|
-
DB_NAME=uat_db
|
|
134
|
-
|
|
135
|
-
# Dynamic values are read from each branch
|
|
136
|
-
DB_PORT={DB_PORT}
|
|
137
|
-
SERVICE_NAME={SERVICE_NAME}
|
|
138
|
-
LOG_LEVEL={LOG_LEVEL}
|
|
139
|
-
`;
|
|
140
|
-
|
|
141
|
-
// ── Setup ──────────────────────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
function setupDemoRepo() {
|
|
144
|
-
section("Setting up demo repository");
|
|
145
|
-
|
|
146
|
-
if (existsSync(DEMO_DIR)) {
|
|
147
|
-
step("Removing existing demo directory…");
|
|
148
|
-
rmSync(DEMO_DIR, { recursive: true, force: true });
|
|
149
|
-
}
|
|
150
|
-
mkdirSync(DEMO_DIR, { recursive: true });
|
|
151
|
-
|
|
152
|
-
// 1. Create bare repo (the "remote")
|
|
153
|
-
step("Creating bare repo (remote)…");
|
|
154
|
-
git(["init", "--bare", "remote.git"], DEMO_DIR);
|
|
155
|
-
ok(`Bare repo: ${BARE_REPO}`);
|
|
156
|
-
|
|
157
|
-
// 2. Clone into workspace
|
|
158
|
-
step("Cloning into workspace…");
|
|
159
|
-
git(["clone", BARE_REPO, "workspace"], DEMO_DIR);
|
|
160
|
-
ok(`Workspace: ${WORK_REPO}`);
|
|
161
|
-
|
|
162
|
-
// Configure git identity for commits
|
|
163
|
-
git(["config", "user.email", "demo@git-worker.local"], WORK_REPO);
|
|
164
|
-
git(["config", "user.name", "Git Worker Demo"], WORK_REPO);
|
|
165
|
-
|
|
166
|
-
// 3. Create initial commit on main
|
|
167
|
-
step("Creating initial commit on main…");
|
|
168
|
-
writeFileSync(join(WORK_REPO, "README.md"), "# Demo Repo\nManaged by git-worker demo.\n");
|
|
169
|
-
git(["add", "."], WORK_REPO);
|
|
170
|
-
git(["commit", "-m", "chore: initial commit"], WORK_REPO);
|
|
171
|
-
git(["push", "-u", "origin", "main"], WORK_REPO);
|
|
172
|
-
|
|
173
|
-
// 4. Create branches with .env files
|
|
174
|
-
for (const branch of BRANCHES) {
|
|
175
|
-
step(`Creating branch ${chalk.cyan(branch.name)} — ${branch.note}`);
|
|
176
|
-
git(["checkout", "-b", branch.name], WORK_REPO);
|
|
177
|
-
writeFileSync(join(WORK_REPO, ".env"), branch.env);
|
|
178
|
-
git(["add", ".env"], WORK_REPO);
|
|
179
|
-
git(["commit", "-m", `chore: add .env for ${branch.name}`], WORK_REPO);
|
|
180
|
-
git(["push", "-u", "origin", branch.name], WORK_REPO);
|
|
181
|
-
git(["checkout", "main"], WORK_REPO);
|
|
182
|
-
ok(` ${branch.name}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 5. Save the template as the saved state so git-worker loads it automatically
|
|
186
|
-
// (skipping the editor step in CI mode)
|
|
187
|
-
const stateDir = join(DEMO_DIR, "state");
|
|
188
|
-
mkdirSync(stateDir, { recursive: true });
|
|
189
|
-
|
|
190
|
-
console.log("");
|
|
191
|
-
section("Demo Repository Contents");
|
|
192
|
-
|
|
193
|
-
console.log("");
|
|
194
|
-
console.log(chalk.bold(" Branches created:"));
|
|
195
|
-
for (const branch of BRANCHES) {
|
|
196
|
-
console.log(chalk.gray(` ${branch.name.padEnd(22)} ${branch.note}`));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.log("");
|
|
200
|
-
console.log(chalk.bold(" Template that will be applied:"));
|
|
201
|
-
console.log(chalk.gray(" ┌" + "─".repeat(58) + "┐"));
|
|
202
|
-
for (const line of TEMPLATE.split("\n")) {
|
|
203
|
-
console.log(chalk.gray(` │ ${line.padEnd(57)}│`));
|
|
204
|
-
}
|
|
205
|
-
console.log(chalk.gray(" └" + "─".repeat(58) + "┘"));
|
|
206
|
-
|
|
207
|
-
console.log("");
|
|
208
|
-
console.log(chalk.bold(" Expected outcome per branch:"));
|
|
209
|
-
console.log(chalk.gray(" uat/payments — DB_NAME corrected if wrong, LOG_LEVEL preserved"));
|
|
210
|
-
console.log(chalk.gray(" uat/orders — LOG_LEVEL missing → prompt (or skip in CI)"));
|
|
211
|
-
console.log(chalk.gray(" uat/inventory — 'export' prefix on LEGACY_VAR preserved"));
|
|
212
|
-
console.log(chalk.gray(" uat/notifications— DB_NAME: legacy_db → uat_db (static enforcement)"));
|
|
213
|
-
|
|
214
|
-
return WORK_REPO;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ── Run git-worker ─────────────────────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
function runGitWorker(workRepoPath: string) {
|
|
220
|
-
section("Running git-worker");
|
|
221
|
-
|
|
222
|
-
// Discover the remote URL that git-worker will use to identify the project
|
|
223
|
-
// (the bare repo path is what workspace/manager.ts will resolve from WORK_REPO's origin)
|
|
224
|
-
const remoteUrl = BARE_REPO;
|
|
225
|
-
|
|
226
|
-
// Save the template into the workspace state dir so the tool loads it
|
|
227
|
-
// (avoids the editor opening in the demo)
|
|
228
|
-
const workerStateRoot = join(homedir(), ".git-worker");
|
|
229
|
-
|
|
230
|
-
const projectId = createHash("sha256").update(remoteUrl).digest("hex").slice(0, 16);
|
|
231
|
-
const stateDir = join(workerStateRoot, "projects", projectId, "state");
|
|
232
|
-
mkdirSync(stateDir, { recursive: true });
|
|
233
|
-
|
|
234
|
-
const stateFile = join(stateDir, ".env.state.json");
|
|
235
|
-
writeFileSync(
|
|
236
|
-
stateFile,
|
|
237
|
-
JSON.stringify(
|
|
238
|
-
{
|
|
239
|
-
schemaVersion: 1,
|
|
240
|
-
targetPath: ".env",
|
|
241
|
-
template: TEMPLATE,
|
|
242
|
-
updatedAt: new Date().toISOString(),
|
|
243
|
-
},
|
|
244
|
-
null,
|
|
245
|
-
2
|
|
246
|
-
)
|
|
247
|
-
);
|
|
248
|
-
ok(`Pre-loaded template into state: ${stateFile}`);
|
|
249
|
-
|
|
250
|
-
const cliPath = resolve("dist/cli/index.js");
|
|
251
|
-
if (!existsSync(cliPath)) {
|
|
252
|
-
console.log(chalk.yellow(" ⚠ dist/ not found — building first…"));
|
|
253
|
-
execSync("node_modules/.bin/tsc", { stdio: "inherit" });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const pushPolicy = isCI ? "always" : "never";
|
|
257
|
-
|
|
258
|
-
const flags = [
|
|
259
|
-
"--repo", WORK_REPO,
|
|
260
|
-
"--branch", "/^uat\\//",
|
|
261
|
-
"--file", ".env",
|
|
262
|
-
"--push", pushPolicy,
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
if (isCI) {
|
|
266
|
-
// In CI mode: non-interactive, auto-approve, LOG_LEVEL missing branch will be skipped
|
|
267
|
-
flags.push("--ci", "--auto-approve");
|
|
268
|
-
console.log("");
|
|
269
|
-
console.log(chalk.bold(" Mode: CI (non-interactive, auto-approve, push: always)"));
|
|
270
|
-
console.log(chalk.gray(" Note: uat/orders will fail because LOG_LEVEL is missing (expected in CI)"));
|
|
271
|
-
} else {
|
|
272
|
-
// Interactive mode: git-worker will prompt for LOG_LEVEL on uat/orders
|
|
273
|
-
console.log("");
|
|
274
|
-
console.log(chalk.bold(" Mode: Interactive"));
|
|
275
|
-
console.log(chalk.yellow(" ℹ The editor will NOT open (template is pre-loaded)"));
|
|
276
|
-
console.log(chalk.yellow(" ℹ You will be prompted to enter LOG_LEVEL for uat/orders"));
|
|
277
|
-
console.log(chalk.yellow(" ℹ Review each branch diff and approve/reject individually"));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
console.log("");
|
|
281
|
-
console.log(chalk.bold(" Command:"));
|
|
282
|
-
console.log(chalk.gray(` node ${cliPath} ${flags.join(" ")}`));
|
|
283
|
-
console.log("");
|
|
284
|
-
console.log(chalk.cyan("─".repeat(66)));
|
|
285
|
-
console.log("");
|
|
286
|
-
|
|
287
|
-
// Run git-worker
|
|
288
|
-
const result = spawnSync("node", [cliPath, ...flags], {
|
|
289
|
-
stdio: "inherit",
|
|
290
|
-
env: { ...process.env },
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
console.log("");
|
|
294
|
-
console.log(chalk.cyan("─".repeat(66)));
|
|
295
|
-
|
|
296
|
-
if (result.status === 0) {
|
|
297
|
-
section("Verification");
|
|
298
|
-
verifyResults(BARE_REPO);
|
|
299
|
-
} else {
|
|
300
|
-
console.log(chalk.red(`\n git-worker exited with code ${result.status}`));
|
|
301
|
-
// Still run verification to see partial results
|
|
302
|
-
section("Partial Verification");
|
|
303
|
-
verifyResults(BARE_REPO);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ── Verify results ─────────────────────────────────────────────────────────
|
|
308
|
-
|
|
309
|
-
function verifyResults(remoteUrl: string) {
|
|
310
|
-
console.log("");
|
|
311
|
-
console.log(chalk.bold(" Checking .env on each branch after git-worker run:"));
|
|
312
|
-
console.log("");
|
|
313
|
-
|
|
314
|
-
const verifyRepo = join(DEMO_DIR, "verify-clone");
|
|
315
|
-
if (!existsSync(verifyRepo)) {
|
|
316
|
-
git(["clone", remoteUrl, "verify-clone"], DEMO_DIR);
|
|
317
|
-
git(["config", "user.email", "verify@git-worker.local"], verifyRepo);
|
|
318
|
-
git(["config", "user.name", "Verify"], verifyRepo);
|
|
319
|
-
} else {
|
|
320
|
-
git(["fetch", "--all"], verifyRepo);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for (const branch of BRANCHES) {
|
|
324
|
-
try {
|
|
325
|
-
const content = git(["show", `origin/${branch.name}:.env`], verifyRepo);
|
|
326
|
-
const lines = content.split("\n").filter((l) => !l.startsWith("#") && l.trim());
|
|
327
|
-
const map = new Map<string, string>();
|
|
328
|
-
for (const line of lines) {
|
|
329
|
-
const eq = line.indexOf("=");
|
|
330
|
-
if (eq !== -1) {
|
|
331
|
-
const key = line.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
332
|
-
const val = line.slice(eq + 1).trim();
|
|
333
|
-
map.set(key, val);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const checks: Array<{ key: string; expected: string; actual: string | undefined; ok: boolean }> = [];
|
|
338
|
-
|
|
339
|
-
// DB_NAME must be uat_db (static enforcement)
|
|
340
|
-
checks.push({
|
|
341
|
-
key: "DB_NAME",
|
|
342
|
-
expected: "uat_db",
|
|
343
|
-
actual: map.get("DB_NAME"),
|
|
344
|
-
ok: map.get("DB_NAME") === "uat_db",
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// CUSTOM_FLAG must still exist (unmanaged key preservation)
|
|
348
|
-
checks.push({
|
|
349
|
-
key: "CUSTOM_FLAG",
|
|
350
|
-
expected: "(present)",
|
|
351
|
-
actual: map.has("CUSTOM_FLAG") ? "(present)" : "(missing)",
|
|
352
|
-
ok: map.has("CUSTOM_FLAG"),
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// SERVICE_NAME must still be set (dynamic)
|
|
356
|
-
checks.push({
|
|
357
|
-
key: "SERVICE_NAME",
|
|
358
|
-
expected: "(present)",
|
|
359
|
-
actual: map.has("SERVICE_NAME") ? "(present)" : "(missing)",
|
|
360
|
-
ok: map.has("SERVICE_NAME"),
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const branchOk = checks.every((c) => c.ok);
|
|
364
|
-
const icon = branchOk ? chalk.green("✓") : chalk.red("✗");
|
|
365
|
-
console.log(` ${icon} ${chalk.bold(branch.name.padEnd(24))}`);
|
|
366
|
-
for (const c of checks) {
|
|
367
|
-
const cIcon = c.ok ? chalk.green(" ✓") : chalk.red(" ✗");
|
|
368
|
-
const val = c.ok
|
|
369
|
-
? chalk.gray(`${c.key}=${c.actual}`)
|
|
370
|
-
: chalk.red(`${c.key}: expected ${c.expected}, got ${c.actual ?? "(missing)"}`);
|
|
371
|
-
console.log(` ${cIcon} ${val}`);
|
|
372
|
-
}
|
|
373
|
-
} catch {
|
|
374
|
-
console.log(chalk.yellow(` ⚠ ${branch.name} — could not read (may not have been committed)`));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
console.log("");
|
|
379
|
-
console.log(chalk.bold(" Demo workspace:"));
|
|
380
|
-
console.log(chalk.gray(` Remote: ${BARE_REPO}`));
|
|
381
|
-
console.log(chalk.gray(` Clone: ${WORK_REPO}`));
|
|
382
|
-
console.log(chalk.gray(` Verify: ${verifyRepo}`));
|
|
383
|
-
console.log("");
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const DOCKERFILE_TEMPLATE = `# Source-of-Truth Dockerfile Template
|
|
387
|
-
FROM node:20-alpine AS base
|
|
388
|
-
ENV SERVICE_NAME=\${SERVICE_NAME}
|
|
389
|
-
ENV BRANCH_NAME=\${BRANCH_NAME}
|
|
390
|
-
ENV BRANCH_SLUG=\${BRANCH_SLUG}
|
|
391
|
-
EXPOSE \${PORT:-8080}
|
|
392
|
-
`;
|
|
393
|
-
|
|
394
|
-
function runDockerfileDemo(remoteUrl: string) {
|
|
395
|
-
section("Phase 2: Dockerfile Synchronization (Source of Truth Engine)");
|
|
396
|
-
|
|
397
|
-
const workerStateRoot = join(homedir(), ".git-worker");
|
|
398
|
-
const projectId = createHash("sha256").update(remoteUrl).digest("hex").slice(0, 16);
|
|
399
|
-
const stateDir = join(workerStateRoot, "projects", projectId, "state");
|
|
400
|
-
mkdirSync(stateDir, { recursive: true });
|
|
401
|
-
|
|
402
|
-
const stateFile = join(stateDir, "Dockerfile.state.json");
|
|
403
|
-
writeFileSync(
|
|
404
|
-
stateFile,
|
|
405
|
-
JSON.stringify(
|
|
406
|
-
{
|
|
407
|
-
schemaVersion: 1,
|
|
408
|
-
targetPath: "Dockerfile",
|
|
409
|
-
template: DOCKERFILE_TEMPLATE,
|
|
410
|
-
updatedAt: new Date().toISOString(),
|
|
411
|
-
},
|
|
412
|
-
null,
|
|
413
|
-
2
|
|
414
|
-
)
|
|
415
|
-
);
|
|
416
|
-
ok(`Pre-loaded Dockerfile template into state: ${stateFile}`);
|
|
417
|
-
|
|
418
|
-
const cliPath = resolve("dist/cli/index.js");
|
|
419
|
-
const pushPolicy = isCI ? "always" : "never";
|
|
420
|
-
const flags = [
|
|
421
|
-
"--repo", WORK_REPO,
|
|
422
|
-
"--branch", "/^uat\\//",
|
|
423
|
-
"--file", "Dockerfile",
|
|
424
|
-
"--push", pushPolicy,
|
|
425
|
-
];
|
|
426
|
-
|
|
427
|
-
if (isCI) {
|
|
428
|
-
flags.push("--ci", "--auto-approve");
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
console.log("");
|
|
432
|
-
console.log(chalk.bold(" Command:"));
|
|
433
|
-
console.log(chalk.gray(` node ${cliPath} ${flags.join(" ")}`));
|
|
434
|
-
console.log("");
|
|
435
|
-
console.log(chalk.cyan("─".repeat(66)));
|
|
436
|
-
console.log("");
|
|
437
|
-
|
|
438
|
-
const result = spawnSync("node", [cliPath, ...flags], {
|
|
439
|
-
stdio: "inherit",
|
|
440
|
-
env: { ...process.env },
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
console.log("");
|
|
444
|
-
console.log(chalk.cyan("─".repeat(66)));
|
|
445
|
-
|
|
446
|
-
if (result.status === 0) {
|
|
447
|
-
section("Dockerfile Verification");
|
|
448
|
-
verifyDockerfileResults(BARE_REPO);
|
|
449
|
-
} else {
|
|
450
|
-
console.log(chalk.red(`\n git-worker exited with code ${result.status}`));
|
|
451
|
-
section("Partial Dockerfile Verification");
|
|
452
|
-
verifyDockerfileResults(BARE_REPO);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function verifyDockerfileResults(remoteUrl: string) {
|
|
457
|
-
console.log("");
|
|
458
|
-
console.log(chalk.bold(" Checking Dockerfile on each branch after git-worker run:"));
|
|
459
|
-
console.log("");
|
|
460
|
-
|
|
461
|
-
const verifyRepo = join(DEMO_DIR, "verify-clone");
|
|
462
|
-
git(["fetch", "--all"], verifyRepo);
|
|
463
|
-
|
|
464
|
-
for (const branch of BRANCHES) {
|
|
465
|
-
try {
|
|
466
|
-
const content = git(["show", `origin/${branch.name}:Dockerfile`], verifyRepo);
|
|
467
|
-
const hasService = content.includes(`ENV SERVICE_NAME=`);
|
|
468
|
-
const hasBranch = content.includes(`ENV BRANCH_NAME=${branch.name}`);
|
|
469
|
-
const hasPort = content.includes(`EXPOSE 8080`);
|
|
470
|
-
|
|
471
|
-
const checks = [
|
|
472
|
-
{ label: "SERVICE_NAME resolved from .env", ok: hasService },
|
|
473
|
-
{ label: `BRANCH_NAME built-in (${branch.name})`, ok: hasBranch },
|
|
474
|
-
{ label: "PORT fallback used (8080)", ok: hasPort },
|
|
475
|
-
];
|
|
476
|
-
|
|
477
|
-
const allOk = checks.every((c) => c.ok);
|
|
478
|
-
const icon = allOk ? chalk.green("✓") : chalk.red("✗");
|
|
479
|
-
console.log(` ${icon} ${chalk.bold(branch.name.padEnd(24))}`);
|
|
480
|
-
for (const c of checks) {
|
|
481
|
-
const cIcon = c.ok ? chalk.green(" ✓") : chalk.red(" ✗");
|
|
482
|
-
console.log(` ${cIcon} ${c.label}`);
|
|
483
|
-
}
|
|
484
|
-
} catch {
|
|
485
|
-
console.log(chalk.yellow(` ⚠ ${branch.name} — could not read Dockerfile`));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Verify persistence: check that state file was saved
|
|
490
|
-
const workerStateRoot = join(homedir(), ".git-worker");
|
|
491
|
-
const projectId = createHash("sha256").update(remoteUrl).digest("hex").slice(0, 16);
|
|
492
|
-
const stateFile = join(workerStateRoot, "projects", projectId, "state", "Dockerfile.state.json");
|
|
493
|
-
if (existsSync(stateFile)) {
|
|
494
|
-
console.log("");
|
|
495
|
-
ok(`State persistence verified: ${stateFile} updated with latest applied template`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ── Entry point ────────────────────────────────────────────────────────────
|
|
500
|
-
|
|
501
|
-
async function main() {
|
|
502
|
-
console.log("");
|
|
503
|
-
console.log(chalk.bold.cyan(" ◆ git-worker — Demo & Functional Verification"));
|
|
504
|
-
console.log(chalk.gray(" Exercises every stage of the pipeline with a real local Git repo"));
|
|
505
|
-
console.log("");
|
|
506
|
-
|
|
507
|
-
if (cleanOnly) {
|
|
508
|
-
if (existsSync(DEMO_DIR)) {
|
|
509
|
-
rmSync(DEMO_DIR, { recursive: true, force: true });
|
|
510
|
-
console.log(chalk.green(` ✓ Removed ${DEMO_DIR}`));
|
|
511
|
-
} else {
|
|
512
|
-
console.log(chalk.gray(` Nothing to clean at ${DEMO_DIR}`));
|
|
513
|
-
}
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const _workRepo = setupDemoRepo();
|
|
518
|
-
runGitWorker(BARE_REPO);
|
|
519
|
-
runDockerfileDemo(BARE_REPO);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
main().catch((e) => {
|
|
523
|
-
console.error(chalk.red(`\nDemo error: ${e.message}`));
|
|
524
|
-
if (process.env.DEBUG) console.error(e.stack);
|
|
525
|
-
process.exit(1);
|
|
526
|
-
});
|
package/src/app.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { CliInput, RunContext, AppDeps, AppServices, Result, ok } from "./core/types.js";
|
|
2
|
-
import { loadConfig } from "./config/load.js";
|
|
3
|
-
import { pipeline } from "./core/pipeline.js";
|
|
4
|
-
|
|
5
|
-
// Services
|
|
6
|
-
import { createGitReader } from "./services/git/reader.js";
|
|
7
|
-
import { createGitOps } from "./services/git/ops.js";
|
|
8
|
-
import { createWorkspaceManager } from "./services/workspace/manager.js";
|
|
9
|
-
import { createTemplateService } from "./services/template/transform.js";
|
|
10
|
-
import { createStateService } from "./services/state/index.js";
|
|
11
|
-
import { createEditorService } from "./services/editor/index.js";
|
|
12
|
-
import { createReviewService } from "./services/review/index.js";
|
|
13
|
-
|
|
14
|
-
// Stages
|
|
15
|
-
import { resolveProjectStage } from "./stages/resolve-project.js";
|
|
16
|
-
import { syncRepositoryStage } from "./stages/sync.js";
|
|
17
|
-
import { selectTargetsStage } from "./stages/select.js";
|
|
18
|
-
import { checkEligibilityStage } from "./stages/eligibility.js";
|
|
19
|
-
import { loadTemplateStage } from "./stages/load-template.js";
|
|
20
|
-
import { editTemplateStage } from "./stages/edit-template.js";
|
|
21
|
-
import { planChangesStage } from "./stages/plan.js";
|
|
22
|
-
import { reviewChangesStage } from "./stages/review.js";
|
|
23
|
-
import { applyChangesStage } from "./stages/apply.js";
|
|
24
|
-
import { pushChangesStage } from "./stages/push.js";
|
|
25
|
-
import { reportResultsStage } from "./stages/report.js";
|
|
26
|
-
|
|
27
|
-
function createServices(): AppServices {
|
|
28
|
-
return {
|
|
29
|
-
git: createGitReader(),
|
|
30
|
-
gitOps: createGitOps(),
|
|
31
|
-
workspace: createWorkspaceManager(),
|
|
32
|
-
template: createTemplateService(),
|
|
33
|
-
state: createStateService(),
|
|
34
|
-
editor: createEditorService(),
|
|
35
|
-
review: createReviewService(),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function run(input: CliInput): Promise<Result<RunContext>> {
|
|
40
|
-
const config = loadConfig(input);
|
|
41
|
-
const services = createServices();
|
|
42
|
-
|
|
43
|
-
const deps: AppDeps = { config, services };
|
|
44
|
-
|
|
45
|
-
const context: RunContext = {
|
|
46
|
-
runId: `${Date.now()}`,
|
|
47
|
-
input,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const runPipeline = pipeline<RunContext, AppDeps>(
|
|
51
|
-
resolveProjectStage,
|
|
52
|
-
syncRepositoryStage,
|
|
53
|
-
selectTargetsStage,
|
|
54
|
-
checkEligibilityStage,
|
|
55
|
-
loadTemplateStage,
|
|
56
|
-
editTemplateStage,
|
|
57
|
-
planChangesStage,
|
|
58
|
-
reviewChangesStage,
|
|
59
|
-
applyChangesStage,
|
|
60
|
-
pushChangesStage,
|
|
61
|
-
reportResultsStage,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const result = await runPipeline(context, deps);
|
|
65
|
-
if (result.ok && result.value.project) {
|
|
66
|
-
services.workspace.releaseLock(result.value.project);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|