@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.
Files changed (47) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/services/git/reader.js +105 -34
  5. package/dist/services/workspace/manager.js +1 -1
  6. package/dist/stages/eligibility.js +38 -14
  7. package/dist/stages/select.js +46 -37
  8. package/package.json +11 -1
  9. package/GIT_WORKER_PROJECT_PLAN.md +0 -499
  10. package/demo/README.md +0 -58
  11. package/demo/index.ts +0 -526
  12. package/src/app.ts +0 -69
  13. package/src/cli/commands.ts +0 -142
  14. package/src/cli/index.ts +0 -123
  15. package/src/config/defaults.ts +0 -46
  16. package/src/config/load.ts +0 -70
  17. package/src/config/schema.ts +0 -48
  18. package/src/core/errors.ts +0 -45
  19. package/src/core/pipeline.ts +0 -17
  20. package/src/core/types.ts +0 -188
  21. package/src/services/editor/index.ts +0 -85
  22. package/src/services/git/ops.ts +0 -170
  23. package/src/services/git/reader.ts +0 -148
  24. package/src/services/report/index.ts +0 -104
  25. package/src/services/review/index.ts +0 -80
  26. package/src/services/state/index.ts +0 -77
  27. package/src/services/template/env.ts +0 -169
  28. package/src/services/template/interpolator.ts +0 -109
  29. package/src/services/template/transform.ts +0 -97
  30. package/src/services/template/validator.ts +0 -75
  31. package/src/services/workspace/cache.ts +0 -144
  32. package/src/services/workspace/manager.ts +0 -370
  33. package/src/stages/apply.ts +0 -194
  34. package/src/stages/edit-template.ts +0 -227
  35. package/src/stages/eligibility.ts +0 -67
  36. package/src/stages/load-template.ts +0 -188
  37. package/src/stages/plan.ts +0 -362
  38. package/src/stages/push.ts +0 -120
  39. package/src/stages/report.ts +0 -126
  40. package/src/stages/resolve-project.ts +0 -341
  41. package/src/stages/review.ts +0 -228
  42. package/src/stages/select.ts +0 -302
  43. package/src/stages/sync.ts +0 -63
  44. package/src/utils/hash.ts +0 -15
  45. package/src/utils/ui.ts +0 -137
  46. package/src/utils/which.ts +0 -18
  47. package/tsconfig.json +0 -18
@@ -1,302 +0,0 @@
1
- import { Stage } from "../core/pipeline.js";
2
- import { RunContext, AppDeps, BranchRef, ok, err } from "../core/types.js";
3
- import { workspaceError, inputCancelledError } from "../core/errors.js";
4
- import { clack, sectionHeader, statusLine, sym } from "../utils/ui.js";
5
- import chalk from "chalk";
6
-
7
- /**
8
- * Matches a branch name against a pattern.
9
- * Supports:
10
- * - Regex: /uat.*\/, /^release\//, /pattern/i
11
- * - Glob: uat/*, *orders, uat*
12
- * - Exact name: main, develop
13
- * - Trailing slash prefix: uat/
14
- * - Substring fallback: orders
15
- */
16
- export function matchesBranchPattern(name: string, pattern: string): boolean {
17
- const trimmed = pattern.trim();
18
- if (!trimmed) return false;
19
-
20
- // 1. Regex pattern: /regex/ or /regex/flags
21
- const regexMatch = trimmed.match(/^\/(.+)\/([a-z]*)$/i);
22
- if (regexMatch) {
23
- try {
24
- return new RegExp(regexMatch[1], regexMatch[2]).test(name);
25
- } catch {
26
- return false;
27
- }
28
- }
29
-
30
- // 2. Glob pattern with * or ?
31
- if (trimmed.includes("*") || trimmed.includes("?")) {
32
- const escaped = trimmed
33
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
34
- .replace(/\*/g, ".*")
35
- .replace(/\?/g, ".");
36
- try {
37
- return new RegExp(`^${escaped}$`).test(name);
38
- } catch {
39
- return false;
40
- }
41
- }
42
-
43
- // 3. Exact match
44
- if (name === trimmed) {
45
- return true;
46
- }
47
-
48
- // 4. Prefix match if pattern ends with '/'
49
- if (trimmed.endsWith("/") && name.startsWith(trimmed)) {
50
- return true;
51
- }
52
-
53
- // 5. Fallback substring match
54
- return name.includes(trimmed);
55
- }
56
-
57
- export const selectTargetsStage: Stage<RunContext, AppDeps> = async (context, deps) => {
58
- if (!context.project) {
59
- return err(workspaceError("Project not resolved"));
60
- }
61
-
62
- sectionHeader("Branch & Target Selection");
63
-
64
- const s = clack.spinner();
65
- s.start("Listing branches…");
66
-
67
- const result = await deps.services.git.listBranches(
68
- context.project.managedRepoPath,
69
- deps.config.git.remote
70
- );
71
-
72
- if (!result.ok) {
73
- s.stop(chalk.red("Failed to list branches"));
74
- return result;
75
- }
76
-
77
- s.stop(`Found ${result.value.length} branches`);
78
-
79
- let branches = result.value;
80
-
81
- // De-duplicate by name (prefer remote over local)
82
- const seen = new Map<string, BranchRef>();
83
- for (const b of branches) {
84
- if (!seen.has(b.name) || b.isRemote) {
85
- seen.set(b.name, b);
86
- }
87
- }
88
- branches = Array.from(seen.values());
89
-
90
- // Filter by pattern if supplied via CLI
91
- if (context.input.branch) {
92
- const pattern = context.input.branch;
93
- const invert = Boolean(context.input.invertBranch);
94
- branches = branches.filter((b) => {
95
- const matched = matchesBranchPattern(b.name, pattern);
96
- return invert ? !matched : matched;
97
- });
98
- statusLine(
99
- "info",
100
- `Pattern '${pattern}'${invert ? " (inverted)" : ""} matched ${branches.length} branch(es)`
101
- );
102
- }
103
-
104
- // Interactive branch selection (non-CI when no --branch provided)
105
- if (!context.input.ci && !context.input.branch) {
106
- if (branches.length === 0) {
107
- clack.log.warn("No branches found in this repository.");
108
- return err(inputCancelledError("No branches available"));
109
- }
110
-
111
- const allBranches = branches;
112
- let selectedNames = new Set<string>();
113
-
114
- while (true) {
115
- const selectedCount = selectedNames.size;
116
- const totalCount = allBranches.length;
117
-
118
- type BranchAction = "confirm" | "pattern" | "all" | "invert" | "list";
119
-
120
- const options: { value: BranchAction; label: string; hint?: string }[] = [];
121
-
122
- if (selectedCount > 0) {
123
- options.push({
124
- value: "confirm",
125
- label: `➜ Confirm and proceed with ${selectedCount} branch(es)`,
126
- hint: "proceed to target file selection",
127
- });
128
- options.push({
129
- value: "invert",
130
- label: `🔄 Invert selection (select the other ${totalCount - selectedCount} branches)`,
131
- hint: "flip current selection",
132
- });
133
- options.push({
134
- value: "pattern",
135
- label: "🔍 Filter by pattern or regex",
136
- hint: "glob (uat/*) or regex (/uat.*/)",
137
- });
138
- options.push({
139
- value: "list",
140
- label: "📋 Customize selection from list",
141
- hint: "toggle individual checkboxes",
142
- });
143
- options.push({
144
- value: "all",
145
- label: `🌟 Select ALL branches (${totalCount} branches)`,
146
- });
147
- } else {
148
- options.push({
149
- value: "pattern",
150
- label: "🔍 Filter by pattern or regex",
151
- hint: "glob (uat/*) or regex (/uat.*/)",
152
- });
153
- options.push({
154
- value: "all",
155
- label: `🌟 Select ALL branches (${totalCount} branches)`,
156
- hint: "select every branch in repository",
157
- });
158
- options.push({
159
- value: "list",
160
- label: "📋 Choose individually from list",
161
- hint: "select branches with checkboxes",
162
- });
163
- options.push({
164
- value: "invert",
165
- label: `🔄 Invert selection (select all ${totalCount} branches)`,
166
- });
167
- }
168
-
169
- const previewText =
170
- selectedCount > 0
171
- ? `${selectedCount} of ${totalCount} selected: ${chalk.cyan(
172
- Array.from(selectedNames).slice(0, 3).join(", ") +
173
- (selectedCount > 3 ? `, +${selectedCount - 3} more` : "")
174
- )}`
175
- : `0 of ${totalCount} branches selected`;
176
-
177
- const choice = await clack.select<BranchAction>({
178
- message: `Branch selection (${previewText}):`,
179
- options,
180
- });
181
-
182
- if (clack.isCancel(choice)) {
183
- return err(inputCancelledError("Branch selection cancelled"));
184
- }
185
-
186
- if (choice === "confirm") {
187
- if (selectedNames.size === 0) {
188
- clack.log.warn("Please select at least one branch before proceeding.");
189
- continue;
190
- }
191
- break;
192
- }
193
-
194
- if (choice === "all") {
195
- selectedNames = new Set(allBranches.map((b) => b.name));
196
- statusLine("ok", `Selected all ${totalCount} branch(es)`);
197
- continue;
198
- }
199
-
200
- if (choice === "invert") {
201
- const inverted = allBranches.filter((b) => !selectedNames.has(b.name));
202
- selectedNames = new Set(inverted.map((b) => b.name));
203
- statusLine(
204
- "ok",
205
- `Inverted selection: ${selectedNames.size} of ${totalCount} branch(es) selected`
206
- );
207
- continue;
208
- }
209
-
210
- if (choice === "pattern") {
211
- const patternInput = await clack.text({
212
- message: "Enter glob pattern (e.g. uat/*) or regex (e.g. /uat.*/):",
213
- placeholder: "/uat.*/ or uat/*",
214
- validate: (v) => {
215
- if (!v || v.trim().length === 0) {
216
- return "Please enter a pattern or regex";
217
- }
218
- },
219
- });
220
-
221
- if (clack.isCancel(patternInput)) {
222
- continue;
223
- }
224
-
225
- const pattern = (patternInput as string).trim();
226
- const matched = allBranches.filter((b) => matchesBranchPattern(b.name, pattern));
227
-
228
- if (matched.length === 0) {
229
- clack.log.warn(`Pattern '${pattern}' matched 0 of ${totalCount} branches.`);
230
- continue;
231
- }
232
-
233
- selectedNames = new Set(matched.map((b) => b.name));
234
- statusLine(
235
- "ok",
236
- `Pattern '${pattern}' matched ${matched.length} branch(es):`
237
- );
238
- for (const b of matched.slice(0, 5)) {
239
- console.log(chalk.gray(` • ${b.name}`));
240
- }
241
- if (matched.length > 5) {
242
- console.log(chalk.gray(` ... and ${matched.length - 5} more`));
243
- }
244
- continue;
245
- }
246
-
247
- if (choice === "list") {
248
- const picked = await clack.multiselect<string>({
249
- message: "Select branches to process (Space to toggle, Enter to confirm):",
250
- options: allBranches.map((b) => ({
251
- value: b.name,
252
- label: `${sym.branch} ${b.name}`,
253
- hint: b.isRemote
254
- ? `remote/${b.remote} ${b.sha.slice(0, 8)}`
255
- : `local ${b.sha.slice(0, 8)}`,
256
- })),
257
- initialValues: Array.from(selectedNames),
258
- required: false,
259
- });
260
-
261
- if (clack.isCancel(picked)) {
262
- continue;
263
- }
264
-
265
- selectedNames = new Set(picked as string[]);
266
- statusLine("ok", `Selection updated: ${selectedNames.size} branch(es) selected`);
267
- continue;
268
- }
269
- }
270
-
271
- branches = allBranches.filter((b) => selectedNames.has(b.name));
272
- }
273
-
274
- if (branches.length === 0) {
275
- clack.log.warn("No branches selected.");
276
- return err(inputCancelledError("No branches selected"));
277
- }
278
-
279
- // Target file selection
280
- let targetPath = context.input.file;
281
- if (!targetPath && !context.input.ci) {
282
- const fileInput = await clack.text({
283
- message: "Target file path:",
284
- placeholder: ".env",
285
- defaultValue: ".env",
286
- });
287
- if (clack.isCancel(fileInput)) {
288
- return err(inputCancelledError("File selection cancelled"));
289
- }
290
- targetPath = (fileInput as string) || ".env";
291
- } else if (!targetPath) {
292
- targetPath = ".env";
293
- }
294
-
295
- statusLine("ok", `Selected ${branches.length} branch(es) → ${targetPath}`);
296
-
297
- return ok({
298
- ...context,
299
- selectedBranches: branches,
300
- target: { path: targetPath },
301
- });
302
- };
@@ -1,63 +0,0 @@
1
- import { Stage } from "../core/pipeline.js";
2
- import { RunContext, AppDeps, ok, err } from "../core/types.js";
3
- import { workspaceError } from "../core/errors.js";
4
- import { clack, statusLine } from "../utils/ui.js";
5
- import chalk from "chalk";
6
-
7
- export const syncRepositoryStage: Stage<RunContext, AppDeps> = async (context, deps) => {
8
- if (!context.project) {
9
- return err(workspaceError("Project not resolved"));
10
- }
11
-
12
- const s = clack.spinner();
13
- s.start(
14
- context.project.isCached
15
- ? "Syncing cached repository with remote…"
16
- : "Cloning repository into managed workspace…"
17
- );
18
-
19
- const cloneResult = await deps.services.workspace.ensureManagedClone(context.project);
20
- if (!cloneResult.ok) {
21
- s.stop(chalk.red("Clone failed"));
22
- return cloneResult;
23
- }
24
-
25
- if (cloneResult.value.isCached) {
26
- s.message("Using cached repository — fetching latest branches…");
27
- }
28
-
29
- // Acquire workspace lock
30
- const lockResult = await deps.services.workspace.acquireLock(
31
- context.project,
32
- deps.config.workspace.lockTimeoutMs
33
- );
34
- if (!lockResult.ok) {
35
- s.stop(chalk.red("Could not acquire workspace lock"));
36
- return lockResult;
37
- }
38
-
39
- // Register lock release on process exit
40
- const releaseLock = lockResult.value;
41
- process.on("exit", () => { releaseLock(); });
42
- process.on("SIGINT", () => { releaseLock(); process.exit(130); });
43
- process.on("SIGTERM", () => { releaseLock(); process.exit(143); });
44
-
45
- // Fetch latest from remote
46
- if (context.project.remoteUrl) {
47
- const fetchResult = await deps.services.git.fetch(
48
- context.project.managedRepoPath,
49
- deps.config.git.remote,
50
- deps.config.git.fetchPrune
51
- );
52
- if (!fetchResult.ok) {
53
- s.stop(chalk.yellow("Fetch warning — continuing with cached refs"));
54
- statusLine("warn", fetchResult.error.message);
55
- } else {
56
- s.stop(chalk.green("Repository synced"));
57
- }
58
- } else {
59
- s.stop(chalk.green("Checkout ready (no remote to fetch)"));
60
- }
61
-
62
- return ok(context);
63
- };
package/src/utils/hash.ts DELETED
@@ -1,15 +0,0 @@
1
- import { createHash } from "crypto";
2
-
3
- export function sha256(content: string): string {
4
- return createHash("sha256").update(content).digest("hex");
5
- }
6
-
7
- export function planHash(
8
- branchName: string,
9
- baseCommitSha: string,
10
- targetPath: string,
11
- beforeContentHash: string,
12
- afterContentHash: string
13
- ): string {
14
- return sha256(`${branchName}|${baseCommitSha}|${targetPath}|${beforeContentHash}|${afterContentHash}`);
15
- }
package/src/utils/ui.ts DELETED
@@ -1,137 +0,0 @@
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
-
8
- export { clack };
9
-
10
- // ── Symbols ────────────────────────────────────────────────────────────────
11
-
12
- export const sym = {
13
- check: chalk.green("✓"),
14
- cross: chalk.red("✗"),
15
- skip: chalk.gray("–"),
16
- dot: chalk.gray("•"),
17
- warn: chalk.yellow("⚠"),
18
- arrow: chalk.cyan("→"),
19
- branch: chalk.cyan("⎇"),
20
- };
21
-
22
- // ── Box / header ───────────────────────────────────────────────────────────
23
-
24
- export function banner(): void {
25
- console.log("");
26
- console.log(chalk.bold.cyan(" ◆ git-worker"));
27
- console.log(chalk.gray(" Safe multi-branch file sync"));
28
- console.log("");
29
- }
30
-
31
- export function sectionHeader(title: string): void {
32
- console.log("");
33
- console.log(chalk.bold(` ${title}`));
34
- console.log(chalk.gray(" " + "─".repeat(Math.max(title.length, 40))));
35
- }
36
-
37
- export function divider(): void {
38
- console.log(chalk.gray(" " + "─".repeat(60)));
39
- }
40
-
41
- // ── Status lines ──────────────────────────────────────────────────────────
42
-
43
- export function statusLine(
44
- status: "ok" | "fail" | "warn" | "skip" | "info",
45
- message: string,
46
- detail?: string
47
- ): void {
48
- const icon =
49
- status === "ok" ? sym.check :
50
- status === "fail" ? sym.cross :
51
- status === "warn" ? sym.warn :
52
- status === "skip" ? sym.skip :
53
- sym.dot;
54
-
55
- const color =
56
- status === "ok" ? chalk.green :
57
- status === "fail" ? chalk.red :
58
- status === "warn" ? chalk.yellow :
59
- status === "skip" ? chalk.gray :
60
- chalk.white;
61
-
62
- console.log(` ${icon} ${color(message)}`);
63
- if (detail) console.log(chalk.gray(` ${detail}`));
64
- }
65
-
66
- // ── Diff rendering ────────────────────────────────────────────────────────
67
-
68
- export function renderDiff(diff: string, redact = false): void {
69
- if (!diff) {
70
- console.log(chalk.gray(" (no changes)"));
71
- return;
72
- }
73
-
74
- const lines = diff.split("\n");
75
- for (const line of lines) {
76
- if (line.startsWith("+++") || line.startsWith("---")) {
77
- console.log(chalk.gray(` ${line}`));
78
- } else if (line.startsWith("@@")) {
79
- console.log(chalk.cyan(` ${line}`));
80
- } else if (line.startsWith("+")) {
81
- const display = redact ? redactLine(line) : line;
82
- console.log(chalk.green(` ${display}`));
83
- } else if (line.startsWith("-")) {
84
- const display = redact ? redactLine(line) : line;
85
- console.log(chalk.red(` ${display}`));
86
- } else {
87
- console.log(chalk.gray(` ${line}`));
88
- }
89
- }
90
- }
91
-
92
- function redactLine(line: string): string {
93
- // Redact value portion: +KEY=value → +KEY=[REDACTED]
94
- return line.replace(/^([+-][A-Za-z_][A-Za-z0-9_]*)=.+$/, "$1=[REDACTED]");
95
- }
96
-
97
- // ── Branch status table ───────────────────────────────────────────────────
98
-
99
- export function renderBranchTable(
100
- rows: Array<{
101
- name: string;
102
- status: string;
103
- detail?: string;
104
- }>
105
- ): void {
106
- const maxName = Math.max(...rows.map((r) => r.name.length), 10);
107
- for (const row of rows) {
108
- const name = row.name.padEnd(maxName);
109
- let statusStr: string;
110
- switch (row.status) {
111
- case "committed":
112
- statusStr = chalk.green("committed");
113
- break;
114
- case "failed":
115
- statusStr = chalk.red("failed ");
116
- break;
117
- case "rejected":
118
- statusStr = chalk.yellow("rejected ");
119
- break;
120
- case "unchanged":
121
- statusStr = chalk.gray("unchanged");
122
- break;
123
- case "skipped":
124
- statusStr = chalk.gray("skipped ");
125
- break;
126
- case "changed":
127
- statusStr = chalk.cyan("changed ");
128
- break;
129
- case "error":
130
- statusStr = chalk.red("error ");
131
- break;
132
- default:
133
- statusStr = chalk.gray(row.status.padEnd(9));
134
- }
135
- console.log(` ${sym.branch} ${chalk.white(name)} ${statusStr} ${chalk.gray(row.detail ?? "")}`);
136
- }
137
- }
@@ -1,18 +0,0 @@
1
- import { existsSync } from "fs";
2
- import { exec } from "child_process";
3
- import { promisify } from "util";
4
-
5
- const execAsync = promisify(exec);
6
-
7
- /**
8
- * Find the full path of an executable on PATH.
9
- * Returns null if not found.
10
- */
11
- export async function which(name: string): Promise<string | null> {
12
- try {
13
- const { stdout } = await execAsync(`which ${name}`);
14
- return stdout.trim() || null;
15
- } catch {
16
- return null;
17
- }
18
- }
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "types": ["node"],
6
- "moduleResolution": "bundler",
7
- "esModuleInterop": true,
8
- "strict": true,
9
- "skipLibCheck": true,
10
- "outDir": "./dist",
11
- "rootDir": "./src",
12
- "declaration": true,
13
- "resolveJsonModule": true,
14
- "allowSyntheticDefaultImports": true
15
- },
16
- "include": ["src/**/*"],
17
- "exclude": ["node_modules", "dist"]
18
- }