@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,341 +0,0 @@
1
- import { Stage } from "../core/pipeline.js";
2
- import { RunContext, AppDeps, ok, err } from "../core/types.js";
3
- import { clack, banner, statusLine, sectionHeader, sym } from "../utils/ui.js";
4
- import { inputCancelledError } from "../core/errors.js";
5
- import { existsSync } from "fs";
6
- import { resolve } from "path";
7
- import chalk from "chalk";
8
- import { listCachedProjects, CachedProjectInfo } from "../services/workspace/cache.js";
9
- import { getWorkspaceRoot } from "../services/workspace/manager.js";
10
-
11
- async function promptCachedRepo(
12
- cachedProjects: CachedProjectInfo[],
13
- root: string
14
- ): Promise<string | "__BACK__" | null> {
15
- if (cachedProjects.length === 0) {
16
- clack.log.info(`No cached repositories found in ${root}/projects yet.`);
17
- return "__BACK__";
18
- }
19
-
20
- console.log("");
21
- console.log(
22
- chalk.bold(
23
- ` ${"Name".padEnd(20)} ${"Remote URL / Path".padEnd(46)} Cached Date`
24
- )
25
- );
26
- console.log(chalk.gray(" " + "─".repeat(80)));
27
- for (const p of cachedProjects) {
28
- const name = chalk.cyan(p.name.slice(0, 18).padEnd(20));
29
- const target = (p.remoteUrl || p.sourceRepoPath || "(unknown)").slice(0, 44).padEnd(46);
30
- const date = p.createdAt ? p.createdAt.slice(0, 10) : "unknown";
31
- console.log(` ${name} ${chalk.white(target)} ${chalk.gray(date)}`);
32
- }
33
- console.log(chalk.gray(" " + "─".repeat(80)));
34
- console.log("");
35
-
36
- const selected = await clack.select<string>({
37
- message: "Select a cached repository to target:",
38
- options: [
39
- ...cachedProjects.map((p) => ({
40
- value: p.remoteUrl || p.sourceRepoPath || p.id,
41
- label: `${sym.branch} ${chalk.bold(p.name)}`,
42
- hint: `${p.remoteUrl || p.sourceRepoPath}`,
43
- })),
44
- {
45
- value: "__BACK__",
46
- label: "← Back to repository menu",
47
- },
48
- ],
49
- });
50
-
51
- if (clack.isCancel(selected)) {
52
- return null;
53
- }
54
-
55
- return selected;
56
- }
57
-
58
- export const resolveProjectStage: Stage<RunContext, AppDeps> = async (context, deps) => {
59
- banner();
60
-
61
- let targetRepo = context.input.repo;
62
-
63
- // If repo was not supplied via CLI argument (-r / --repo)
64
- if (!targetRepo) {
65
- const isCi = context.input.ci ?? false;
66
- const cwd = process.cwd();
67
-
68
- // Check if the current working directory is a git repository
69
- const localCheck = await deps.services.workspace.checkLocalRepo(
70
- cwd,
71
- context.input.remote || deps.config.git.remote
72
- );
73
-
74
- if (isCi) {
75
- // In CI mode, cannot prompt: use cwd if git repo, otherwise fail
76
- if (localCheck.isRepo) {
77
- targetRepo = cwd;
78
- } else {
79
- clack.log.error(
80
- `Current directory '${cwd}' is not a Git repository. Pass --repo <url-or-path> in CI mode.`
81
- );
82
- return err(inputCancelledError("Not a git repository in CI mode without --repo flag"));
83
- }
84
- } else {
85
- // Interactive TUI mode
86
- sectionHeader("Target Repository");
87
-
88
- const root = getWorkspaceRoot(deps.config);
89
-
90
- while (!targetRepo) {
91
- const cachedProjects = await listCachedProjects(deps.config);
92
-
93
- // If user specifically requested --cached via CLI
94
- if (context.input.cached) {
95
- if (cachedProjects.length === 0) {
96
- clack.log.warn(`No cached repositories found in ${root}/projects yet.`);
97
- context.input.cached = false;
98
- continue;
99
- }
100
- const selected = await promptCachedRepo(cachedProjects, root);
101
- if (selected === null) {
102
- return err(inputCancelledError("Repository selection cancelled"));
103
- }
104
- if (selected === "__BACK__") {
105
- context.input.cached = false;
106
- continue;
107
- }
108
- targetRepo = selected;
109
- break;
110
- }
111
-
112
- if (localCheck.isRepo) {
113
- // Current directory IS a git repo
114
- const remoteInfo = localCheck.remoteUrl
115
- ? ` (remote: ${localCheck.remoteUrl})`
116
- : " (local-only)";
117
- statusLine(
118
- "ok",
119
- "Detected Git repository in current directory",
120
- `${cwd}${remoteInfo}`
121
- );
122
- console.log("");
123
-
124
- const choice = await clack.select<string>({
125
- message: "What repository would you like to target?",
126
- options: [
127
- {
128
- value: "current",
129
- label: "Use current repository",
130
- hint: localCheck.remoteUrl || cwd,
131
- },
132
- {
133
- value: "cached",
134
- label: `📦 Select from cached repositories (${cachedProjects.length} available)`,
135
- hint: "list already cloned repositories (name, remote URL)",
136
- },
137
- {
138
- value: "remote",
139
- label: "Clone a remote repository URL",
140
- hint: "https://... or git@...",
141
- },
142
- {
143
- value: "local-path",
144
- label: "Specify a different local repository path",
145
- },
146
- ],
147
- });
148
-
149
- if (clack.isCancel(choice)) {
150
- return err(inputCancelledError("Repository selection cancelled"));
151
- }
152
-
153
- if (choice === "current") {
154
- targetRepo = cwd;
155
- break;
156
- }
157
-
158
- if (choice === "cached") {
159
- const selected = await promptCachedRepo(cachedProjects, root);
160
- if (selected === null) {
161
- return err(inputCancelledError("Repository selection cancelled"));
162
- }
163
- if (selected === "__BACK__") {
164
- continue;
165
- }
166
- targetRepo = selected;
167
- break;
168
- }
169
-
170
- if (choice === "remote") {
171
- const remoteUrlInput = await clack.text({
172
- message: "Remote repository URL:",
173
- placeholder: "https://github.com/org/repo.git or git@github.com:org/repo.git",
174
- validate: (v) => {
175
- if (!v || v.trim() === "") return "Repository URL cannot be empty";
176
- const trimmed = v.trim();
177
- const isUrl =
178
- trimmed.startsWith("http://") ||
179
- trimmed.startsWith("https://") ||
180
- trimmed.startsWith("git@") ||
181
- trimmed.startsWith("ssh://") ||
182
- trimmed.startsWith("git://");
183
- if (!isUrl) {
184
- return "Please enter a valid remote URL (e.g. https://github.com/... or git@github.com:...)";
185
- }
186
- },
187
- });
188
- if (clack.isCancel(remoteUrlInput)) {
189
- return err(inputCancelledError("Repository selection cancelled"));
190
- }
191
- targetRepo = (remoteUrlInput as string).trim();
192
- break;
193
- }
194
-
195
- if (choice === "local-path") {
196
- const pathInput = await clack.text({
197
- message: "Path to local repository:",
198
- placeholder: "/path/to/repo or ../other-repo",
199
- validate: (v) => {
200
- if (!v || v.trim() === "") return "Path cannot be empty";
201
- if (!existsSync(resolve(v.trim()))) return `Path does not exist: ${v.trim()}`;
202
- },
203
- });
204
- if (clack.isCancel(pathInput)) {
205
- return err(inputCancelledError("Repository selection cancelled"));
206
- }
207
- targetRepo = (pathInput as string).trim();
208
- break;
209
- }
210
- } else {
211
- // Current directory IS NOT a git repo
212
- statusLine("info", "Current directory is not a Git repository", cwd);
213
- console.log("");
214
-
215
- const choice = await clack.select<string>({
216
- message: "How would you like to target a repository?",
217
- options: [
218
- ...(cachedProjects.length > 0
219
- ? [
220
- {
221
- value: "cached",
222
- label: `📦 Select from cached repositories (${cachedProjects.length} available)`,
223
- hint: "list already cloned repositories (name, remote URL)",
224
- },
225
- ]
226
- : [
227
- {
228
- value: "cached",
229
- label: "📦 Cached repositories (none yet)",
230
- hint: "view already cloned repositories in ~/.git-worker",
231
- },
232
- ]),
233
- {
234
- value: "remote",
235
- label: "Clone a remote repository URL",
236
- hint: "recommended — https://... or git@...",
237
- },
238
- {
239
- value: "local-path",
240
- label: "Specify path to a local Git repository",
241
- },
242
- ],
243
- });
244
-
245
- if (clack.isCancel(choice)) {
246
- return err(inputCancelledError("Repository selection cancelled"));
247
- }
248
-
249
- if (choice === "cached") {
250
- const selected = await promptCachedRepo(cachedProjects, root);
251
- if (selected === null) {
252
- return err(inputCancelledError("Repository selection cancelled"));
253
- }
254
- if (selected === "__BACK__") {
255
- continue;
256
- }
257
- targetRepo = selected;
258
- break;
259
- }
260
-
261
- if (choice === "remote") {
262
- const remoteUrlInput = await clack.text({
263
- message: "Remote repository URL:",
264
- placeholder: "https://github.com/org/repo.git or git@github.com:org/repo.git",
265
- validate: (v) => {
266
- if (!v || v.trim() === "") return "Repository URL cannot be empty";
267
- const trimmed = v.trim();
268
- const isUrl =
269
- trimmed.startsWith("http://") ||
270
- trimmed.startsWith("https://") ||
271
- trimmed.startsWith("git@") ||
272
- trimmed.startsWith("ssh://") ||
273
- trimmed.startsWith("git://");
274
- if (!isUrl) {
275
- return "Please enter a valid remote URL (e.g. https://github.com/... or git@github.com:...)";
276
- }
277
- },
278
- });
279
- if (clack.isCancel(remoteUrlInput)) {
280
- return err(inputCancelledError("Repository selection cancelled"));
281
- }
282
- targetRepo = (remoteUrlInput as string).trim();
283
- break;
284
- }
285
-
286
- if (choice === "local-path") {
287
- const pathInput = await clack.text({
288
- message: "Path to local repository:",
289
- placeholder: "/path/to/repo or ../other-repo",
290
- validate: (v) => {
291
- if (!v || v.trim() === "") return "Path cannot be empty";
292
- if (!existsSync(resolve(v.trim()))) return `Path does not exist: ${v.trim()}`;
293
- },
294
- });
295
- if (clack.isCancel(pathInput)) {
296
- return err(inputCancelledError("Repository selection cancelled"));
297
- }
298
- targetRepo = (pathInput as string).trim();
299
- break;
300
- }
301
- }
302
- }
303
- }
304
- }
305
-
306
- // Now resolve the project workspace
307
- const s = clack.spinner();
308
- s.start("Resolving project workspace…");
309
-
310
- const result = await deps.services.workspace.resolveProject(
311
- targetRepo!,
312
- context.input.remote,
313
- deps.config
314
- );
315
-
316
- if (!result.ok) {
317
- s.stop(chalk.red("Project resolution failed"));
318
- clack.log.error(result.error.message);
319
- return result;
320
- }
321
-
322
- s.stop(chalk.green("Project workspace ready"));
323
- if (result.value.isCached) {
324
- statusLine(
325
- "info",
326
- "Using cached repository clone (already downloaded)",
327
- result.value.remoteUrl || result.value.managedRepoPath
328
- );
329
- } else {
330
- statusLine("info", `Managed workspace: ${result.value.managedRepoPath}`);
331
- }
332
-
333
- return ok({
334
- ...context,
335
- input: {
336
- ...context.input,
337
- repo: targetRepo,
338
- },
339
- project: result.value,
340
- });
341
- };
@@ -1,228 +0,0 @@
1
- import { Stage } from "../core/pipeline.js";
2
- import { RunContext, AppDeps, Approval, ok, err } from "../core/types.js";
3
- import { workspaceError, inputCancelledError } from "../core/errors.js";
4
- import { writeSnapshots, parseDiffLines } from "../services/review/index.js";
5
- import { clack, sectionHeader, statusLine, divider, renderDiff, renderBranchTable, sym } from "../utils/ui.js";
6
- import chalk from "chalk";
7
-
8
- export const reviewChangesStage: Stage<RunContext, AppDeps> = async (context, deps) => {
9
- if (!context.plans || !context.project) {
10
- return err(workspaceError("No plans to review"));
11
- }
12
-
13
- const isCi = context.input.ci ?? false;
14
- const autoApprove = context.input.autoApprove || deps.config.review.autoApprove;
15
-
16
- sectionHeader("Review Changes");
17
-
18
- // Show summary table first
19
- renderBranchTable(
20
- context.plans.map((p) => ({
21
- name: p.branch.name,
22
- status: p.status,
23
- detail:
24
- p.status === "changed"
25
- ? `${p.updatedKeys.length} updated, ${p.addedKeys.length} added`
26
- : p.status === "error"
27
- ? p.reason ?? "error"
28
- : "no changes",
29
- }))
30
- );
31
-
32
- const changedPlans = context.plans.filter((p) => p.status === "changed");
33
- const approvals: Approval[] = [];
34
-
35
- // Auto-approve unchanged and errors
36
- for (const plan of context.plans) {
37
- if (plan.status !== "changed") {
38
- approvals.push({
39
- branchName: plan.branch.name,
40
- approved: false,
41
- planHash: plan.planHash,
42
- });
43
- }
44
- }
45
-
46
- if (changedPlans.length === 0) {
47
- statusLine("info", "No changed branches to review");
48
- return ok({ ...context, approvals });
49
- }
50
-
51
- // Auto-approve all
52
- if (autoApprove) {
53
- for (const plan of changedPlans) {
54
- approvals.push({
55
- branchName: plan.branch.name,
56
- approved: true,
57
- planHash: plan.planHash,
58
- });
59
- }
60
- statusLine("ok", `Auto-approved ${changedPlans.length} branch(es)`);
61
- return ok({ ...context, approvals });
62
- }
63
-
64
- if (isCi) {
65
- // CI without auto-approve: reject all
66
- for (const plan of changedPlans) {
67
- approvals.push({
68
- branchName: plan.branch.name,
69
- approved: false,
70
- planHash: plan.planHash,
71
- });
72
- }
73
- statusLine("warn", "CI mode — all branches rejected (use --auto-approve to commit in CI)");
74
- return ok({ ...context, approvals });
75
- }
76
-
77
- // Interactive review
78
- console.log("");
79
-
80
- // Ask if user wants batch action or per-branch review
81
- const batchChoice = await clack.select<string>({
82
- message: `Review ${changedPlans.length} changed branch(es):`,
83
- options: [
84
- { value: "per-branch", label: "Review each branch individually", hint: "recommended" },
85
- { value: "approve-all", label: `Approve all ${changedPlans.length} branches` },
86
- { value: "reject-all", label: "Reject all" },
87
- ],
88
- });
89
-
90
- if (clack.isCancel(batchChoice)) {
91
- return err(inputCancelledError("Review cancelled"));
92
- }
93
-
94
- if (batchChoice === "approve-all") {
95
- for (const plan of changedPlans) {
96
- approvals.push({ branchName: plan.branch.name, approved: true, planHash: plan.planHash });
97
- }
98
- statusLine("ok", `Approved all ${changedPlans.length} branch(es)`);
99
- return ok({ ...context, approvals });
100
- }
101
-
102
- if (batchChoice === "reject-all") {
103
- for (const plan of changedPlans) {
104
- approvals.push({ branchName: plan.branch.name, approved: false, planHash: plan.planHash });
105
- }
106
- statusLine("warn", "All branches rejected");
107
- return ok({ ...context, approvals });
108
- }
109
-
110
- // Per-branch review
111
- for (const plan of changedPlans) {
112
- console.log("");
113
- divider();
114
- console.log(` ${sym.branch} ${chalk.bold.cyan(plan.branch.name)} ${chalk.gray(plan.baseCommitSha.slice(0, 8))}`);
115
- console.log(` ${chalk.gray(plan.path)}`);
116
- console.log("");
117
-
118
- // Show inline diff
119
- if (deps.config.report.warnOnSecrets) {
120
- console.log(chalk.yellow(" ⚠ Diff may contain secret values — treat with care"));
121
- console.log("");
122
- }
123
- renderDiff(plan.diff, false);
124
-
125
- // Write snapshots for external diff tool
126
- const snapResult = await writeSnapshots(
127
- context.project.stagingDir,
128
- context.runId,
129
- plan
130
- );
131
-
132
- let openDiff = false;
133
- if (snapResult.ok) {
134
- const choice = await clack.select<string>({
135
- message: "Action:",
136
- options: [
137
- { value: "approve", label: `${chalk.green("✓")} Approve`, hint: "commit this branch" },
138
- { value: "reject", label: `${chalk.red("✗")} Reject`, hint: "skip this branch" },
139
- { value: "diff", label: `${chalk.cyan("⊞")} Open in diff viewer`, hint: deps.config.editor.name },
140
- ],
141
- });
142
-
143
- if (clack.isCancel(choice)) {
144
- return err(inputCancelledError("Review cancelled"));
145
- }
146
-
147
- if (choice === "diff") {
148
- const { beforePath, afterPath } = snapResult.value;
149
- const diffResult = await deps.services.editor.openForDiff(beforePath, afterPath, deps.config);
150
- if (!diffResult.ok) {
151
- clack.log.warn(diffResult.error.message);
152
- }
153
- // Ask again after opening diff
154
- const afterDiff = await clack.select<string>({
155
- message: "After reviewing the diff:",
156
- options: [
157
- { value: "approve", label: `${chalk.green("✓")} Approve` },
158
- { value: "reject", label: `${chalk.red("✗")} Reject` },
159
- ],
160
- });
161
-
162
- if (clack.isCancel(afterDiff)) {
163
- return err(inputCancelledError("Review cancelled"));
164
- }
165
- approvals.push({
166
- branchName: plan.branch.name,
167
- approved: afterDiff === "approve",
168
- planHash: plan.planHash,
169
- });
170
- } else {
171
- approvals.push({
172
- branchName: plan.branch.name,
173
- approved: choice === "approve",
174
- planHash: plan.planHash,
175
- });
176
- }
177
- } else {
178
- // No snapshots — just approve/reject
179
- const choice = await clack.select<string>({
180
- message: "Action:",
181
- options: [
182
- { value: "approve", label: `${chalk.green("✓")} Approve` },
183
- { value: "reject", label: `${chalk.red("✗")} Reject` },
184
- ],
185
- });
186
- if (clack.isCancel(choice)) {
187
- return err(inputCancelledError("Review cancelled"));
188
- }
189
- approvals.push({
190
- branchName: plan.branch.name,
191
- approved: choice === "approve",
192
- planHash: plan.planHash,
193
- });
194
- }
195
- }
196
-
197
- divider();
198
-
199
- const approved = approvals.filter((a) => a.approved).length;
200
- const rejected = approvals.filter((a) => !a.approved && context.plans!.find((p) => p.branch.name === a.branchName)?.status === "changed").length;
201
- statusLine("ok", `Review complete — ${approved} approved, ${rejected} rejected`);
202
-
203
- // Confirm before destructive apply
204
- if (approved > 0) {
205
- console.log("");
206
- console.log(chalk.bold(" Summary of approved changes:"));
207
- for (const plan of changedPlans) {
208
- const a = approvals.find((ap) => ap.branchName === plan.branch.name);
209
- if (a?.approved) {
210
- const detail = [
211
- plan.updatedKeys.length > 0 ? `update: ${plan.updatedKeys.join(", ")}` : "",
212
- plan.addedKeys.length > 0 ? `add: ${plan.addedKeys.join(", ")}` : "",
213
- ].filter(Boolean).join(" | ");
214
- statusLine("ok", plan.branch.name, detail);
215
- }
216
- }
217
- console.log("");
218
- const confirm = await clack.confirm({
219
- message: `Proceed to commit ${approved} branch(es)?`,
220
- initialValue: true,
221
- });
222
- if (clack.isCancel(confirm) || !confirm) {
223
- return err(inputCancelledError("Commit aborted by user"));
224
- }
225
- }
226
-
227
- return ok({ ...context, approvals });
228
- };