@samaksh/git-worker 0.0.0-stage → 0.1.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.
Files changed (115) hide show
  1. package/GIT_WORKER_PROJECT_PLAN.md +499 -0
  2. package/README.md +114 -2
  3. package/demo/README.md +58 -0
  4. package/demo/index.ts +526 -0
  5. package/dist/app.d.ts +2 -0
  6. package/dist/app.js +48 -0
  7. package/dist/cli/commands.d.ts +6 -0
  8. package/dist/cli/commands.js +111 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +115 -0
  11. package/dist/config/defaults.d.ts +2 -0
  12. package/dist/config/defaults.js +44 -0
  13. package/dist/config/load.d.ts +3 -0
  14. package/dist/config/load.js +65 -0
  15. package/dist/config/schema.d.ts +200 -0
  16. package/dist/config/schema.js +45 -0
  17. package/dist/core/errors.d.ts +12 -0
  18. package/dist/core/errors.js +34 -0
  19. package/dist/core/pipeline.d.ts +3 -0
  20. package/dist/core/pipeline.js +14 -0
  21. package/dist/core/types.d.ts +153 -0
  22. package/dist/core/types.js +16 -0
  23. package/dist/services/editor/index.d.ts +9 -0
  24. package/dist/services/editor/index.js +59 -0
  25. package/dist/services/git/ops.d.ts +30 -0
  26. package/dist/services/git/ops.js +139 -0
  27. package/dist/services/git/reader.d.ts +14 -0
  28. package/dist/services/git/reader.js +124 -0
  29. package/dist/services/report/index.d.ts +18 -0
  30. package/dist/services/report/index.js +68 -0
  31. package/dist/services/review/index.d.ts +30 -0
  32. package/dist/services/review/index.js +59 -0
  33. package/dist/services/state/index.d.ts +20 -0
  34. package/dist/services/state/index.js +49 -0
  35. package/dist/services/template/env.d.ts +23 -0
  36. package/dist/services/template/env.js +146 -0
  37. package/dist/services/template/interpolator.d.ts +30 -0
  38. package/dist/services/template/interpolator.js +75 -0
  39. package/dist/services/template/transform.d.ts +22 -0
  40. package/dist/services/template/transform.js +75 -0
  41. package/dist/services/template/validator.d.ts +5 -0
  42. package/dist/services/template/validator.js +68 -0
  43. package/dist/services/workspace/cache.d.ts +32 -0
  44. package/dist/services/workspace/cache.js +107 -0
  45. package/dist/services/workspace/manager.d.ts +44 -0
  46. package/dist/services/workspace/manager.js +301 -0
  47. package/dist/stages/apply.d.ts +3 -0
  48. package/dist/stages/apply.js +161 -0
  49. package/dist/stages/edit-template.d.ts +3 -0
  50. package/dist/stages/edit-template.js +198 -0
  51. package/dist/stages/eligibility.d.ts +3 -0
  52. package/dist/stages/eligibility.js +44 -0
  53. package/dist/stages/load-template.d.ts +3 -0
  54. package/dist/stages/load-template.js +168 -0
  55. package/dist/stages/plan.d.ts +3 -0
  56. package/dist/stages/plan.js +270 -0
  57. package/dist/stages/push.d.ts +3 -0
  58. package/dist/stages/push.js +99 -0
  59. package/dist/stages/report.d.ts +3 -0
  60. package/dist/stages/report.js +103 -0
  61. package/dist/stages/resolve-project.d.ts +3 -0
  62. package/dist/stages/resolve-project.js +294 -0
  63. package/dist/stages/review.d.ts +3 -0
  64. package/dist/stages/review.js +197 -0
  65. package/dist/stages/select.d.ts +13 -0
  66. package/dist/stages/select.js +250 -0
  67. package/dist/stages/sync.d.ts +3 -0
  68. package/dist/stages/sync.js +47 -0
  69. package/dist/utils/hash.d.ts +2 -0
  70. package/dist/utils/hash.js +7 -0
  71. package/dist/utils/ui.d.ts +21 -0
  72. package/dist/utils/ui.js +113 -0
  73. package/dist/utils/which.d.ts +5 -0
  74. package/dist/utils/which.js +16 -0
  75. package/docs/FEATURES.md +84 -0
  76. package/docs/SERVICES.md +57 -0
  77. package/docs/architecture.md +38 -0
  78. package/docs/usage.md +337 -0
  79. package/package.json +48 -4
  80. package/src/app.ts +69 -0
  81. package/src/cli/commands.ts +142 -0
  82. package/src/cli/index.ts +123 -0
  83. package/src/config/defaults.ts +46 -0
  84. package/src/config/load.ts +70 -0
  85. package/src/config/schema.ts +48 -0
  86. package/src/core/errors.ts +45 -0
  87. package/src/core/pipeline.ts +17 -0
  88. package/src/core/types.ts +188 -0
  89. package/src/services/editor/index.ts +85 -0
  90. package/src/services/git/ops.ts +170 -0
  91. package/src/services/git/reader.ts +148 -0
  92. package/src/services/report/index.ts +104 -0
  93. package/src/services/review/index.ts +80 -0
  94. package/src/services/state/index.ts +77 -0
  95. package/src/services/template/env.ts +169 -0
  96. package/src/services/template/interpolator.ts +109 -0
  97. package/src/services/template/transform.ts +97 -0
  98. package/src/services/template/validator.ts +75 -0
  99. package/src/services/workspace/cache.ts +144 -0
  100. package/src/services/workspace/manager.ts +370 -0
  101. package/src/stages/apply.ts +194 -0
  102. package/src/stages/edit-template.ts +227 -0
  103. package/src/stages/eligibility.ts +67 -0
  104. package/src/stages/load-template.ts +188 -0
  105. package/src/stages/plan.ts +362 -0
  106. package/src/stages/push.ts +120 -0
  107. package/src/stages/report.ts +126 -0
  108. package/src/stages/resolve-project.ts +341 -0
  109. package/src/stages/review.ts +228 -0
  110. package/src/stages/select.ts +302 -0
  111. package/src/stages/sync.ts +63 -0
  112. package/src/utils/hash.ts +15 -0
  113. package/src/utils/ui.ts +137 -0
  114. package/src/utils/which.ts +18 -0
  115. package/tsconfig.json +18 -0
@@ -0,0 +1,250 @@
1
+ import { ok, err } from "../core/types.js";
2
+ import { workspaceError, inputCancelledError } from "../core/errors.js";
3
+ import { clack, sectionHeader, statusLine, sym } from "../utils/ui.js";
4
+ import chalk from "chalk";
5
+ /**
6
+ * Matches a branch name against a pattern.
7
+ * Supports:
8
+ * - Regex: /uat.*\/, /^release\//, /pattern/i
9
+ * - Glob: uat/*, *orders, uat*
10
+ * - Exact name: main, develop
11
+ * - Trailing slash prefix: uat/
12
+ * - Substring fallback: orders
13
+ */
14
+ export function matchesBranchPattern(name, pattern) {
15
+ const trimmed = pattern.trim();
16
+ if (!trimmed)
17
+ return false;
18
+ // 1. Regex pattern: /regex/ or /regex/flags
19
+ const regexMatch = trimmed.match(/^\/(.+)\/([a-z]*)$/i);
20
+ if (regexMatch) {
21
+ try {
22
+ return new RegExp(regexMatch[1], regexMatch[2]).test(name);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ // 2. Glob pattern with * or ?
29
+ if (trimmed.includes("*") || trimmed.includes("?")) {
30
+ const escaped = trimmed
31
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
32
+ .replace(/\*/g, ".*")
33
+ .replace(/\?/g, ".");
34
+ try {
35
+ return new RegExp(`^${escaped}$`).test(name);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ // 3. Exact match
42
+ if (name === trimmed) {
43
+ return true;
44
+ }
45
+ // 4. Prefix match if pattern ends with '/'
46
+ if (trimmed.endsWith("/") && name.startsWith(trimmed)) {
47
+ return true;
48
+ }
49
+ // 5. Fallback substring match
50
+ return name.includes(trimmed);
51
+ }
52
+ export const selectTargetsStage = async (context, deps) => {
53
+ if (!context.project) {
54
+ return err(workspaceError("Project not resolved"));
55
+ }
56
+ sectionHeader("Branch & Target Selection");
57
+ const s = clack.spinner();
58
+ s.start("Listing branches…");
59
+ const result = await deps.services.git.listBranches(context.project.managedRepoPath, deps.config.git.remote);
60
+ if (!result.ok) {
61
+ s.stop(chalk.red("Failed to list branches"));
62
+ return result;
63
+ }
64
+ s.stop(`Found ${result.value.length} branches`);
65
+ let branches = result.value;
66
+ // De-duplicate by name (prefer remote over local)
67
+ const seen = new Map();
68
+ for (const b of branches) {
69
+ if (!seen.has(b.name) || b.isRemote) {
70
+ seen.set(b.name, b);
71
+ }
72
+ }
73
+ branches = Array.from(seen.values());
74
+ // Filter by pattern if supplied via CLI
75
+ if (context.input.branch) {
76
+ const pattern = context.input.branch;
77
+ const invert = Boolean(context.input.invertBranch);
78
+ branches = branches.filter((b) => {
79
+ const matched = matchesBranchPattern(b.name, pattern);
80
+ return invert ? !matched : matched;
81
+ });
82
+ statusLine("info", `Pattern '${pattern}'${invert ? " (inverted)" : ""} matched ${branches.length} branch(es)`);
83
+ }
84
+ // Interactive branch selection (non-CI when no --branch provided)
85
+ if (!context.input.ci && !context.input.branch) {
86
+ if (branches.length === 0) {
87
+ clack.log.warn("No branches found in this repository.");
88
+ return err(inputCancelledError("No branches available"));
89
+ }
90
+ const allBranches = branches;
91
+ let selectedNames = new Set();
92
+ while (true) {
93
+ const selectedCount = selectedNames.size;
94
+ const totalCount = allBranches.length;
95
+ const options = [];
96
+ if (selectedCount > 0) {
97
+ options.push({
98
+ value: "confirm",
99
+ label: `➜ Confirm and proceed with ${selectedCount} branch(es)`,
100
+ hint: "proceed to target file selection",
101
+ });
102
+ options.push({
103
+ value: "invert",
104
+ label: `🔄 Invert selection (select the other ${totalCount - selectedCount} branches)`,
105
+ hint: "flip current selection",
106
+ });
107
+ options.push({
108
+ value: "pattern",
109
+ label: "🔍 Filter by pattern or regex",
110
+ hint: "glob (uat/*) or regex (/uat.*/)",
111
+ });
112
+ options.push({
113
+ value: "list",
114
+ label: "📋 Customize selection from list",
115
+ hint: "toggle individual checkboxes",
116
+ });
117
+ options.push({
118
+ value: "all",
119
+ label: `🌟 Select ALL branches (${totalCount} branches)`,
120
+ });
121
+ }
122
+ else {
123
+ options.push({
124
+ value: "pattern",
125
+ label: "🔍 Filter by pattern or regex",
126
+ hint: "glob (uat/*) or regex (/uat.*/)",
127
+ });
128
+ options.push({
129
+ value: "all",
130
+ label: `🌟 Select ALL branches (${totalCount} branches)`,
131
+ hint: "select every branch in repository",
132
+ });
133
+ options.push({
134
+ value: "list",
135
+ label: "📋 Choose individually from list",
136
+ hint: "select branches with checkboxes",
137
+ });
138
+ options.push({
139
+ value: "invert",
140
+ label: `🔄 Invert selection (select all ${totalCount} branches)`,
141
+ });
142
+ }
143
+ const previewText = selectedCount > 0
144
+ ? `${selectedCount} of ${totalCount} selected: ${chalk.cyan(Array.from(selectedNames).slice(0, 3).join(", ") +
145
+ (selectedCount > 3 ? `, +${selectedCount - 3} more` : ""))}`
146
+ : `0 of ${totalCount} branches selected`;
147
+ const choice = await clack.select({
148
+ message: `Branch selection (${previewText}):`,
149
+ options,
150
+ });
151
+ if (clack.isCancel(choice)) {
152
+ return err(inputCancelledError("Branch selection cancelled"));
153
+ }
154
+ if (choice === "confirm") {
155
+ if (selectedNames.size === 0) {
156
+ clack.log.warn("Please select at least one branch before proceeding.");
157
+ continue;
158
+ }
159
+ break;
160
+ }
161
+ if (choice === "all") {
162
+ selectedNames = new Set(allBranches.map((b) => b.name));
163
+ statusLine("ok", `Selected all ${totalCount} branch(es)`);
164
+ continue;
165
+ }
166
+ if (choice === "invert") {
167
+ const inverted = allBranches.filter((b) => !selectedNames.has(b.name));
168
+ selectedNames = new Set(inverted.map((b) => b.name));
169
+ statusLine("ok", `Inverted selection: ${selectedNames.size} of ${totalCount} branch(es) selected`);
170
+ continue;
171
+ }
172
+ if (choice === "pattern") {
173
+ const patternInput = await clack.text({
174
+ message: "Enter glob pattern (e.g. uat/*) or regex (e.g. /uat.*/):",
175
+ placeholder: "/uat.*/ or uat/*",
176
+ validate: (v) => {
177
+ if (!v || v.trim().length === 0) {
178
+ return "Please enter a pattern or regex";
179
+ }
180
+ },
181
+ });
182
+ if (clack.isCancel(patternInput)) {
183
+ continue;
184
+ }
185
+ const pattern = patternInput.trim();
186
+ const matched = allBranches.filter((b) => matchesBranchPattern(b.name, pattern));
187
+ if (matched.length === 0) {
188
+ clack.log.warn(`Pattern '${pattern}' matched 0 of ${totalCount} branches.`);
189
+ continue;
190
+ }
191
+ selectedNames = new Set(matched.map((b) => b.name));
192
+ statusLine("ok", `Pattern '${pattern}' matched ${matched.length} branch(es):`);
193
+ for (const b of matched.slice(0, 5)) {
194
+ console.log(chalk.gray(` • ${b.name}`));
195
+ }
196
+ if (matched.length > 5) {
197
+ console.log(chalk.gray(` ... and ${matched.length - 5} more`));
198
+ }
199
+ continue;
200
+ }
201
+ if (choice === "list") {
202
+ const picked = await clack.multiselect({
203
+ message: "Select branches to process (Space to toggle, Enter to confirm):",
204
+ options: allBranches.map((b) => ({
205
+ value: b.name,
206
+ label: `${sym.branch} ${b.name}`,
207
+ hint: b.isRemote
208
+ ? `remote/${b.remote} ${b.sha.slice(0, 8)}`
209
+ : `local ${b.sha.slice(0, 8)}`,
210
+ })),
211
+ initialValues: Array.from(selectedNames),
212
+ required: false,
213
+ });
214
+ if (clack.isCancel(picked)) {
215
+ continue;
216
+ }
217
+ selectedNames = new Set(picked);
218
+ statusLine("ok", `Selection updated: ${selectedNames.size} branch(es) selected`);
219
+ continue;
220
+ }
221
+ }
222
+ branches = allBranches.filter((b) => selectedNames.has(b.name));
223
+ }
224
+ if (branches.length === 0) {
225
+ clack.log.warn("No branches selected.");
226
+ return err(inputCancelledError("No branches selected"));
227
+ }
228
+ // Target file selection
229
+ let targetPath = context.input.file;
230
+ if (!targetPath && !context.input.ci) {
231
+ const fileInput = await clack.text({
232
+ message: "Target file path:",
233
+ placeholder: ".env",
234
+ defaultValue: ".env",
235
+ });
236
+ if (clack.isCancel(fileInput)) {
237
+ return err(inputCancelledError("File selection cancelled"));
238
+ }
239
+ targetPath = fileInput || ".env";
240
+ }
241
+ else if (!targetPath) {
242
+ targetPath = ".env";
243
+ }
244
+ statusLine("ok", `Selected ${branches.length} branch(es) → ${targetPath}`);
245
+ return ok({
246
+ ...context,
247
+ selectedBranches: branches,
248
+ target: { path: targetPath },
249
+ });
250
+ };
@@ -0,0 +1,3 @@
1
+ import { Stage } from "../core/pipeline.js";
2
+ import { RunContext, AppDeps } from "../core/types.js";
3
+ export declare const syncRepositoryStage: Stage<RunContext, AppDeps>;
@@ -0,0 +1,47 @@
1
+ import { ok, err } from "../core/types.js";
2
+ import { workspaceError } from "../core/errors.js";
3
+ import { clack, statusLine } from "../utils/ui.js";
4
+ import chalk from "chalk";
5
+ export const syncRepositoryStage = async (context, deps) => {
6
+ if (!context.project) {
7
+ return err(workspaceError("Project not resolved"));
8
+ }
9
+ const s = clack.spinner();
10
+ s.start(context.project.isCached
11
+ ? "Syncing cached repository with remote…"
12
+ : "Cloning repository into managed workspace…");
13
+ const cloneResult = await deps.services.workspace.ensureManagedClone(context.project);
14
+ if (!cloneResult.ok) {
15
+ s.stop(chalk.red("Clone failed"));
16
+ return cloneResult;
17
+ }
18
+ if (cloneResult.value.isCached) {
19
+ s.message("Using cached repository — fetching latest branches…");
20
+ }
21
+ // Acquire workspace lock
22
+ const lockResult = await deps.services.workspace.acquireLock(context.project, deps.config.workspace.lockTimeoutMs);
23
+ if (!lockResult.ok) {
24
+ s.stop(chalk.red("Could not acquire workspace lock"));
25
+ return lockResult;
26
+ }
27
+ // Register lock release on process exit
28
+ const releaseLock = lockResult.value;
29
+ process.on("exit", () => { releaseLock(); });
30
+ process.on("SIGINT", () => { releaseLock(); process.exit(130); });
31
+ process.on("SIGTERM", () => { releaseLock(); process.exit(143); });
32
+ // Fetch latest from remote
33
+ if (context.project.remoteUrl) {
34
+ const fetchResult = await deps.services.git.fetch(context.project.managedRepoPath, deps.config.git.remote, deps.config.git.fetchPrune);
35
+ if (!fetchResult.ok) {
36
+ s.stop(chalk.yellow("Fetch warning — continuing with cached refs"));
37
+ statusLine("warn", fetchResult.error.message);
38
+ }
39
+ else {
40
+ s.stop(chalk.green("Repository synced"));
41
+ }
42
+ }
43
+ else {
44
+ s.stop(chalk.green("Checkout ready (no remote to fetch)"));
45
+ }
46
+ return ok(context);
47
+ };
@@ -0,0 +1,2 @@
1
+ export declare function sha256(content: string): string;
2
+ export declare function planHash(branchName: string, baseCommitSha: string, targetPath: string, beforeContentHash: string, afterContentHash: string): string;
@@ -0,0 +1,7 @@
1
+ import { createHash } from "crypto";
2
+ export function sha256(content) {
3
+ return createHash("sha256").update(content).digest("hex");
4
+ }
5
+ export function planHash(branchName, baseCommitSha, targetPath, beforeContentHash, afterContentHash) {
6
+ return sha256(`${branchName}|${baseCommitSha}|${targetPath}|${beforeContentHash}|${afterContentHash}`);
7
+ }
@@ -0,0 +1,21 @@
1
+ import * as clack from "@clack/prompts";
2
+ export { clack };
3
+ export declare const sym: {
4
+ check: string;
5
+ cross: string;
6
+ skip: string;
7
+ dot: string;
8
+ warn: string;
9
+ arrow: string;
10
+ branch: string;
11
+ };
12
+ export declare function banner(): void;
13
+ export declare function sectionHeader(title: string): void;
14
+ export declare function divider(): void;
15
+ export declare function statusLine(status: "ok" | "fail" | "warn" | "skip" | "info", message: string, detail?: string): void;
16
+ export declare function renderDiff(diff: string, redact?: boolean): void;
17
+ export declare function renderBranchTable(rows: Array<{
18
+ name: string;
19
+ status: string;
20
+ detail?: string;
21
+ }>): void;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Terminal UI utilities using chalk for color and @clack/prompts for interactive prompts.
3
+ * All UX-facing output goes through this module.
4
+ */
5
+ import chalk from "chalk";
6
+ import * as clack from "@clack/prompts";
7
+ export { clack };
8
+ // ── Symbols ────────────────────────────────────────────────────────────────
9
+ export const sym = {
10
+ check: chalk.green("✓"),
11
+ cross: chalk.red("✗"),
12
+ skip: chalk.gray("–"),
13
+ dot: chalk.gray("•"),
14
+ warn: chalk.yellow("⚠"),
15
+ arrow: chalk.cyan("→"),
16
+ branch: chalk.cyan("⎇"),
17
+ };
18
+ // ── Box / header ───────────────────────────────────────────────────────────
19
+ export function banner() {
20
+ console.log("");
21
+ console.log(chalk.bold.cyan(" ◆ git-worker"));
22
+ console.log(chalk.gray(" Safe multi-branch file sync"));
23
+ console.log("");
24
+ }
25
+ export function sectionHeader(title) {
26
+ console.log("");
27
+ console.log(chalk.bold(` ${title}`));
28
+ console.log(chalk.gray(" " + "─".repeat(Math.max(title.length, 40))));
29
+ }
30
+ export function divider() {
31
+ console.log(chalk.gray(" " + "─".repeat(60)));
32
+ }
33
+ // ── Status lines ──────────────────────────────────────────────────────────
34
+ export function statusLine(status, message, detail) {
35
+ const icon = status === "ok" ? sym.check :
36
+ status === "fail" ? sym.cross :
37
+ status === "warn" ? sym.warn :
38
+ status === "skip" ? sym.skip :
39
+ sym.dot;
40
+ const color = status === "ok" ? chalk.green :
41
+ status === "fail" ? chalk.red :
42
+ status === "warn" ? chalk.yellow :
43
+ status === "skip" ? chalk.gray :
44
+ chalk.white;
45
+ console.log(` ${icon} ${color(message)}`);
46
+ if (detail)
47
+ console.log(chalk.gray(` ${detail}`));
48
+ }
49
+ // ── Diff rendering ────────────────────────────────────────────────────────
50
+ export function renderDiff(diff, redact = false) {
51
+ if (!diff) {
52
+ console.log(chalk.gray(" (no changes)"));
53
+ return;
54
+ }
55
+ const lines = diff.split("\n");
56
+ for (const line of lines) {
57
+ if (line.startsWith("+++") || line.startsWith("---")) {
58
+ console.log(chalk.gray(` ${line}`));
59
+ }
60
+ else if (line.startsWith("@@")) {
61
+ console.log(chalk.cyan(` ${line}`));
62
+ }
63
+ else if (line.startsWith("+")) {
64
+ const display = redact ? redactLine(line) : line;
65
+ console.log(chalk.green(` ${display}`));
66
+ }
67
+ else if (line.startsWith("-")) {
68
+ const display = redact ? redactLine(line) : line;
69
+ console.log(chalk.red(` ${display}`));
70
+ }
71
+ else {
72
+ console.log(chalk.gray(` ${line}`));
73
+ }
74
+ }
75
+ }
76
+ function redactLine(line) {
77
+ // Redact value portion: +KEY=value → +KEY=[REDACTED]
78
+ return line.replace(/^([+-][A-Za-z_][A-Za-z0-9_]*)=.+$/, "$1=[REDACTED]");
79
+ }
80
+ // ── Branch status table ───────────────────────────────────────────────────
81
+ export function renderBranchTable(rows) {
82
+ const maxName = Math.max(...rows.map((r) => r.name.length), 10);
83
+ for (const row of rows) {
84
+ const name = row.name.padEnd(maxName);
85
+ let statusStr;
86
+ switch (row.status) {
87
+ case "committed":
88
+ statusStr = chalk.green("committed");
89
+ break;
90
+ case "failed":
91
+ statusStr = chalk.red("failed ");
92
+ break;
93
+ case "rejected":
94
+ statusStr = chalk.yellow("rejected ");
95
+ break;
96
+ case "unchanged":
97
+ statusStr = chalk.gray("unchanged");
98
+ break;
99
+ case "skipped":
100
+ statusStr = chalk.gray("skipped ");
101
+ break;
102
+ case "changed":
103
+ statusStr = chalk.cyan("changed ");
104
+ break;
105
+ case "error":
106
+ statusStr = chalk.red("error ");
107
+ break;
108
+ default:
109
+ statusStr = chalk.gray(row.status.padEnd(9));
110
+ }
111
+ console.log(` ${sym.branch} ${chalk.white(name)} ${statusStr} ${chalk.gray(row.detail ?? "")}`);
112
+ }
113
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Find the full path of an executable on PATH.
3
+ * Returns null if not found.
4
+ */
5
+ export declare function which(name: string): Promise<string | null>;
@@ -0,0 +1,16 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Find the full path of an executable on PATH.
6
+ * Returns null if not found.
7
+ */
8
+ export async function which(name) {
9
+ try {
10
+ const { stdout } = await execAsync(`which ${name}`);
11
+ return stdout.trim() || null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
@@ -0,0 +1,84 @@
1
+ # Features
2
+
3
+ ## Core Features
4
+
5
+ ### Multi-branch Operations
6
+ - Target multiple branches simultaneously via glob patterns (`uat/*`) or regex patterns (`/uat.*/`)
7
+ - Interactive branch selector with **Select All**, **Invert Selection**, **Pattern Filter**, and manual checkbox list
8
+ - Command-line pattern inversion support via `--invert-branch` (`-i`)
9
+ - No persistent worktree per branch - uses git object reads for planning
10
+ - Per-branch fault isolation - failures don't affect other branches
11
+
12
+ ### Safe .env Template System
13
+ - **Static values**: `KEY=value` enforced on every selected branch
14
+ - **Dynamic placeholders**: `KEY={KEY}` reads existing value from each branch
15
+ - **Missing values**: Prompted individually per branch during planning
16
+ - **Unmanaged keys preserved**: Keys not in template are never touched
17
+ - **Comment preservation**: Comments and formatting preserved
18
+ - **Export support**: Handles `export KEY=value` syntax
19
+ - **Quoted values**: Preserves quoted values and spaces correctly
20
+ - **Empty values**: `KEY=` treated as present with empty value
21
+ - **Duplicate detection**: Fails safely if duplicates found
22
+
23
+ ### Declarative Source-of-Truth Templates (Dockerfile, JSON, YAML)
24
+ - **Full File Override / Creation**: The template represents the authoritative source of truth.
25
+ - **Variable Substitution**: Replaces `${VAR}` placeholders with values resolved from each branch's `.env`.
26
+ - **Fallback Defaults**: Supports `${VAR:-default}` syntax (e.g. `${PORT:-8080}`) to use fallback values when missing.
27
+ - **Built-in Branch Variables**: Injects `${BRANCH_NAME}`, `${BRANCH_SLUG}`, `${REPO_NAME}`, `${COMMIT_SHA}`, and `${SHORT_SHA}`.
28
+ - **Pre-Plan Syntax Validation**: Validates JSON syntax, YAML indentation/structure, and template delimiters before planning.
29
+ - **State Persistence**: Applied templates are saved in `~/.git-worker/projects/<id>/state/` and automatically restored on subsequent runs.
30
+
31
+ ### Review Workflow
32
+ - Plan all changes before any mutations
33
+ - Preview diffs per branch in terminal or external viewer (VS Code/Vim)
34
+ - Approve/reject branches individually
35
+ - Pre-destructive checkout confirmation
36
+ - Branch tip revalidation before apply (stale plans skipped)
37
+
38
+ ### Safe Application
39
+ - Uses managed disposable checkout in `~/.git-worker` (separate from original repo)
40
+ - **Requires remote** - refuses repos without remotes by default
41
+ - Workspace ownership verification
42
+ - Cross-process locking
43
+ - Resets/cleans only in managed checkout
44
+ - Stages only target paths
45
+ - Commits per branch independently
46
+ - Push with policy (prompt/always/never), respects CI mode
47
+
48
+ ### Repository Discovery & Remote Clones
49
+ - Automatically detects if current directory is a Git repository
50
+ - Lists and selects from already cloned and cached repositories (`name`, `remote-url`)
51
+ - If run outside a repository, prompts to select from cache, clone a remote URL (HTTP/HTTPS/SSH), or provide a local path
52
+ - Direct command-line targeting via `--repo <url-or-path>` or `--cached` (`-c`)
53
+
54
+ ### Cache Management & Notifications
55
+ - Centralized storage in `~/.git-worker/projects/<project-id>`
56
+ - Detects existing clones and notifies user of cached repository reuse
57
+ - Interactive cached repository picker showing Name and Remote URL
58
+ - `git-worker cache list` (or `ls`) displays all cached workspaces in a formatted table
59
+ - `git-worker cache clean [target]` (or `clear`) removes specific projects or provides an interactive removal menu
60
+ - `git-worker cache clean --all -y` batch cleans all workspaces safely
61
+
62
+ ### State Persistence
63
+ - Saves last successfully applied template
64
+ - Saves on partial success (after first commit or explicit approval)
65
+ - Loaded on subsequent runs
66
+ - No persistent branch-specific bindings
67
+ - Loaded on subsequent runs
68
+ - No persistent branch-specific bindings
69
+
70
+ ## Interactive TUI
71
+ - Modern prompts with @clack/prompts
72
+ - Multi-select for branches
73
+ - Per-branch value input
74
+ - Diff review with viewer options
75
+ - Confirmation before destructive operations
76
+
77
+ ## CI Mode
78
+ - Non-interactive operation
79
+ - Missing values cause explicit failure/skip (no hanging)
80
+ - Auto-approve or explicit flags required
81
+ - Push policy enforced
82
+
83
+ ## Demo Suite
84
+ Run `npm run demo` to test with self-contained multi-branch scenarios.
@@ -0,0 +1,57 @@
1
+ # Services Documentation
2
+
3
+ Git Worker uses dependency injection with services for all side effects. Each service is created via a factory function.
4
+
5
+ ## Core Services
6
+
7
+ ### Git Reader (`services/git/reader.ts`)
8
+ - `listBranches(repoPath)` - Lists all local and remote branches
9
+ - `readFileFromBranch(repoPath, branch, filePath)` - Reads file content from a branch without checkout using `git show`
10
+ - `getBranchSha(repoPath, branchName)` - Gets current SHA of a branch
11
+ - `fetch(repoPath, remote)` - Fetches from remote with prune
12
+ - `getCurrentBranch(repoPath)` - Gets current branch name
13
+
14
+ ### Git Ops (`services/git/ops.ts`)
15
+ - `ensureRepo(repoPath)` - Validates it's a git repo
16
+ - `checkoutBranch(repoPath, branchName)` - Checks out a branch
17
+ - `resetHard(repoPath)` - Hard reset (managed checkout only)
18
+ - `clean(repoPath)` - Clean untracked files/dirs
19
+ - `writeAndStage(repoPath, filePath, content)` - Write file and stage it
20
+ - `commit(repoPath, message)` - Create commit
21
+ - `push(repoPath, remote, branchName)` - Push to remote
22
+ - `verifyOwnership(managedRepoPath, markerPath)` - Verifies workspace ownership via marker
23
+
24
+ ### Workspace Manager (`services/workspace/manager.ts`)
25
+ - `resolveProject(sourceRepoPath, remote?)` - Resolves/creates project, **requires remote** (fails if no remote by default)
26
+ - `ensureManagedClone(project)` - Clones or ensures managed repo exists
27
+ - `acquireLock(project)` - Acquires per-project lock with stale PID detection
28
+ - `releaseLock(project)` - Releases lock
29
+
30
+ ### Template Service (`services/template/`)
31
+ **env.ts**: Parsing and validation
32
+ - `parseEnvTemplate(raw)` - Parses template, validates syntax, detects duplicates, extracts placeholders
33
+
34
+ **transform.ts**: Transformation logic
35
+ - `parseEnvFile(content)` - Parses existing .env file
36
+ - `transformEnv(branchContent, template, branchValues)` - Applies template: enforces static values, preserves dynamic values from branch, prompts for missing
37
+
38
+ ### State Service (`services/state/index.ts`)
39
+ Persists templates per project+target. Saves on success/partial success per configured policy.
40
+ - `loadState(project, targetPath)` - Load saved template
41
+ - `saveState(project, targetPath, template)` - Save template
42
+ - `getStatePath(project, targetPath)` - Get state file path
43
+
44
+ ### Editor Service (`services/editor/index.ts`)
45
+ Opens files in VS Code or Vim.
46
+ - `editFile(filePath, editorName)` - Opens editor and waits for close
47
+ - `diffFiles(before, after, editorName)` - Opens diff viewer
48
+
49
+ ### Review Service (`services/review/index.ts`)
50
+ Manages review snapshots and diff generation.
51
+ - `createSnapshot(content, branch, targetPath)` - Creates before/after snapshots in staging
52
+ - `generateDiff(before, after)` - Generates unified diff
53
+
54
+ ### Report Service (`services/report/index.ts`)
55
+ Generates reports. **Redacts values by default** for exported reports.
56
+ - `generateReport(context)` - Creates report object
57
+ - `writeReport(project, report)` - Writes JSON/Markdown reports (values redacted)
@@ -0,0 +1,38 @@
1
+ # Architecture
2
+
3
+ Git Worker follows a functional core, imperative shell pattern with a pipeline composition model.
4
+
5
+ ## Core Concepts
6
+
7
+ - **Pipeline**: Stages compose sequentially, each receiving context and deps, returning `Result<T>`
8
+ - **Context**: Single evolving typed object passed through all stages
9
+ - **Services**: Injected dependencies for side effects (git, fs, editor, prompts)
10
+ - **Pure Functions**: Template parsing, transformation, validation are pure
11
+
12
+ ## Directory Structure
13
+
14
+ ```
15
+ src/
16
+ cli/ # CLI entry point
17
+ config/ # Configuration schema, defaults, loading
18
+ core/ # Types, errors, pipeline
19
+ services/ # Side effect implementations
20
+ git/ # Git operations (read, write)
21
+ workspace/ # Managed workspace management
22
+ template/ # .env parsing and transformation
23
+ stages/ # Pipeline stages
24
+ utils/ # Utilities
25
+ ```
26
+
27
+ ## Safety Model
28
+
29
+ 1. **No mutation of original repo** - All changes go through managed checkout
30
+ 2. **Remote required** - Rejects repos without remotes by default
31
+ 3. **Per-branch isolation** - Failures don't cascade
32
+ 4. **Tip revalidation** - Branch tips checked before apply (stale plans skipped)
33
+ 5. **Path-scoped staging** - Only target files staged
34
+ 6. **Read-only diff review by default** - Snapshots not editable in MVP
35
+
36
+ ## Decisions
37
+
38
+ See [README](../README.md) for all chosen defaults.