@samaksh/git-worker 0.0.0-stage → 0.2.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 (78) hide show
  1. package/README.md +114 -2
  2. package/dist/app.d.ts +2 -0
  3. package/dist/app.js +48 -0
  4. package/dist/cli/commands.d.ts +6 -0
  5. package/dist/cli/commands.js +111 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +115 -0
  8. package/dist/config/defaults.d.ts +2 -0
  9. package/dist/config/defaults.js +44 -0
  10. package/dist/config/load.d.ts +3 -0
  11. package/dist/config/load.js +65 -0
  12. package/dist/config/schema.d.ts +200 -0
  13. package/dist/config/schema.js +45 -0
  14. package/dist/core/errors.d.ts +12 -0
  15. package/dist/core/errors.js +34 -0
  16. package/dist/core/pipeline.d.ts +3 -0
  17. package/dist/core/pipeline.js +14 -0
  18. package/dist/core/types.d.ts +153 -0
  19. package/dist/core/types.js +16 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/services/editor/index.d.ts +9 -0
  23. package/dist/services/editor/index.js +59 -0
  24. package/dist/services/git/ops.d.ts +30 -0
  25. package/dist/services/git/ops.js +139 -0
  26. package/dist/services/git/reader.d.ts +14 -0
  27. package/dist/services/git/reader.js +195 -0
  28. package/dist/services/report/index.d.ts +18 -0
  29. package/dist/services/report/index.js +68 -0
  30. package/dist/services/review/index.d.ts +30 -0
  31. package/dist/services/review/index.js +59 -0
  32. package/dist/services/state/index.d.ts +20 -0
  33. package/dist/services/state/index.js +49 -0
  34. package/dist/services/template/env.d.ts +23 -0
  35. package/dist/services/template/env.js +146 -0
  36. package/dist/services/template/interpolator.d.ts +30 -0
  37. package/dist/services/template/interpolator.js +75 -0
  38. package/dist/services/template/transform.d.ts +22 -0
  39. package/dist/services/template/transform.js +75 -0
  40. package/dist/services/template/validator.d.ts +5 -0
  41. package/dist/services/template/validator.js +68 -0
  42. package/dist/services/workspace/cache.d.ts +32 -0
  43. package/dist/services/workspace/cache.js +107 -0
  44. package/dist/services/workspace/manager.d.ts +44 -0
  45. package/dist/services/workspace/manager.js +301 -0
  46. package/dist/stages/apply.d.ts +3 -0
  47. package/dist/stages/apply.js +161 -0
  48. package/dist/stages/edit-template.d.ts +3 -0
  49. package/dist/stages/edit-template.js +198 -0
  50. package/dist/stages/eligibility.d.ts +3 -0
  51. package/dist/stages/eligibility.js +68 -0
  52. package/dist/stages/load-template.d.ts +3 -0
  53. package/dist/stages/load-template.js +168 -0
  54. package/dist/stages/plan.d.ts +3 -0
  55. package/dist/stages/plan.js +270 -0
  56. package/dist/stages/push.d.ts +3 -0
  57. package/dist/stages/push.js +99 -0
  58. package/dist/stages/report.d.ts +3 -0
  59. package/dist/stages/report.js +103 -0
  60. package/dist/stages/resolve-project.d.ts +3 -0
  61. package/dist/stages/resolve-project.js +294 -0
  62. package/dist/stages/review.d.ts +3 -0
  63. package/dist/stages/review.js +197 -0
  64. package/dist/stages/select.d.ts +13 -0
  65. package/dist/stages/select.js +265 -0
  66. package/dist/stages/sync.d.ts +3 -0
  67. package/dist/stages/sync.js +47 -0
  68. package/dist/utils/hash.d.ts +2 -0
  69. package/dist/utils/hash.js +7 -0
  70. package/dist/utils/ui.d.ts +21 -0
  71. package/dist/utils/ui.js +113 -0
  72. package/dist/utils/which.d.ts +5 -0
  73. package/dist/utils/which.js +16 -0
  74. package/docs/FEATURES.md +84 -0
  75. package/docs/SERVICES.md +57 -0
  76. package/docs/architecture.md +38 -0
  77. package/docs/usage.md +337 -0
  78. package/package.json +58 -4
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # Git Worker
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A safe, composable TypeScript CLI/TUI for applying reviewed file changes across many Git branches.
4
+
5
+ ## Overview
6
+
7
+ Git Worker lets you target a Git repository, select branches and a file, edit a reusable template, preview exact per-branch changes without checking out branches, approve/reject them, and then commit and optionally push approved changes. It works with a handful or 100+ branches without requiring one persistent worktree per branch.
8
+
9
+ ## Design Principles
10
+
11
+ - **Readable over clever** - Small functions, explicit types, straightforward control flow
12
+ - **Functional core, imperative shell** - Pure transformations/validation; side effects injected via services
13
+ - **Inspect without checkout** - Uses `git show`, `git ls-tree`, `git cat-file` to plan changes
14
+ - **Review before mutation** - Collect missing values and build diffs before any checkout/commit
15
+ - **Safe by default** - Never modifies your original repository; only commits to managed disposable checkouts
16
+ - **Per-branch fault isolation** - One branch failure doesn't corrupt others
17
+
18
+ ## How It Works
19
+
20
+ 1. **Resolve & sync** - Detects current Git repository or prompts to clone a remote URL into `~/.git-worker`. If already cloned, reuses the cached clone and fetches latest branches.
21
+ 2. **Select** - Choose branches (glob e.g. `uat/*`, regex e.g. `/uat.*/`, select all, or invert selection) and target file
22
+ 3. **Template** - Edit a template in VS Code or Vim (or restore previously applied template):
23
+ - **For `.env` files (AST Reconciliation)**: `KEY=value` sets static values; `KEY={KEY}` reads existing branch values; unmanaged keys and comments are preserved.
24
+ - **For other files (`Dockerfile`, JSON, YAML, configs)**: The template is the declarative **Source of Truth**; variables like `${VAR}` or `${VAR:-default}` are resolved per-branch from that branch's `.env`, alongside built-ins (`${BRANCH_NAME}`, `${BRANCH_SLUG}`). The evaluated template overrides or creates the file.
25
+ - **Syntax Validation**: Checks JSON/YAML syntax immediately before planning.
26
+ 4. **Plan & review** - See exact diffs per branch, approve/reject individually
27
+ 5. **Apply & push** - Commits approved changes in a managed checkout, optionally pushes with confirmation
28
+
29
+ ## Configuration Defaults (Chosen)
30
+
31
+ We made reasonable defaults for the open decisions. These are easy to change later.
32
+
33
+ | Decision | Default | Rationale |
34
+ |---|---|---|
35
+ | **Remove key from template** | **Preserve** on branches | Safest - never deletes keys implicitly. If you need deletion, use an explicit delete operation in the future. |
36
+ | **Source repo with no remote** | **Prompt or reject with error** | When outside a repo, prompts to clone remote or target path. Requires a remote to avoid mutating local-only repos unexpectedly. |
37
+ | **Save template on partial success** | **Save after first successful commit** (or after explicit approval if no commits but reviewed) | Preserves progress if some branches succeed and others fail. Partial status is clearly reported. |
38
+ | **Editable review snapshots** | **Read-only diff for MVP** | Much safer. To change results, re-edit the template and re-run the plan. Prevents accidental manual edits creating inconsistent diffs. |
39
+ | **Secrets in diffs** | **Show in interactive diff with warning**; **redact from exported reports by default** | You need to see secrets to review `.env` changes, but exports (JSON/Markdown) redact by default to avoid accidental leaks. |
40
+
41
+ ## CLI Usage
42
+
43
+ ```bash
44
+ # Interactive mode (prompts to use current repo, select cached repo, or clone remote)
45
+ git-worker
46
+
47
+ # Select from previously cloned/cached repositories directly
48
+ git-worker --cached
49
+
50
+ # Target a remote repository URL directly
51
+ git-worker --repo https://github.com/my-org/my-project.git
52
+
53
+ # Target a local repo with specific branches and file
54
+ git-worker --repo ../my-repo --branch 'uat/*' --file .env
55
+
56
+ # Non-interactive CI mode
57
+ git-worker --repo git@github.com:org/repo.git --branch 'uat/*' --file .env --ci --push never
58
+
59
+ # Cache management
60
+ git-worker cache list # List all cached repositories in ~/.git-worker
61
+ git-worker cache clean [target] # Clean a specific project or select interactively
62
+ git-worker cache clean --all # Clean all cached checkouts (or: git-worker clean -a -y)
63
+ ```
64
+
65
+ Key flags: `--repo/-r`, `--cached/-c`, `--branch/-b`, `--invert-branch/-i`, `--file/-f`, `--editor/-e`, `--commit-msg/-m`, `--push/-p`, `--remote`, `--auto-approve`, `--ci`
66
+
67
+ ## Workspace Layout
68
+
69
+ Git Worker stores all checkouts, templates, and state under `~/.git-worker/` in the user's home directory:
70
+
71
+ ```
72
+ ~/.git-worker/
73
+ projects/
74
+ <stable-project-id>/
75
+ repo/ # Disposable checkout owned by git-worker
76
+ staging/<run-id>/ # Before/after snapshots outside repo
77
+ state/ # Saved templates (per project+target)
78
+ reports/ # Run reports (optional)
79
+ .git-worker-marker # Ownership verification marker
80
+ .git-worker-lock # Process lock
81
+ ```
82
+
83
+ **Safety guarantees:**
84
+ - Never touches your original repository
85
+ - Only stages target paths (`git add -- <path>`)
86
+ - Revalidates branch tips before applying (skips if changed)
87
+ - Resets/cleans only inside verified managed checkout
88
+ - Commits/pushes per-branch with fault isolation
89
+
90
+ ## Supported Editors
91
+
92
+ - **VS Code (preferred)**: `code --wait` for editing, `code --diff` for review
93
+ - **Vim**: `vim` for editing, `vim -d`/`vimdiff` for review
94
+
95
+ ## Development Status
96
+
97
+ This is the implementation blueprint. See [GIT_WORKER_PROJECT_PLAN.md](./GIT_WORKER_PROJECT_PLAN.md) for the full architecture, API contracts, and roadmap.
98
+
99
+ ## Non-Goals (MVP)
100
+
101
+ - One-worktree-per-branch persistent checkout farm
102
+ - Remote/team-shared template state
103
+ - Complex drift database or three-way merge
104
+ - Automatic guessing of missing values
105
+ - Implicit deletion of unmanaged keys
106
+ - Multi-repo distributed transactions
107
+ - Full support for arbitrary YAML/JSON/Dockerfile/Groovy (only `.env` in MVP)
108
+
109
+ ## Documentation
110
+
111
+ - [Architecture](docs/architecture.md) - Pipeline design, safety model, and structure
112
+ - [Features](docs/FEATURES.md) - Complete feature list and capabilities
113
+ - [Services](docs/SERVICES.md) - Service APIs and dependency injection
114
+ - [Usage Guide](docs/usage.md) - Detailed usage examples
115
+ - [Implementation Plan](GIT_WORKER_PROJECT_PLAN.md) - Full blueprint and roadmap
package/dist/app.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { CliInput, RunContext, Result } from "./core/types.js";
2
+ export declare function run(input: CliInput): Promise<Result<RunContext>>;
package/dist/app.js ADDED
@@ -0,0 +1,48 @@
1
+ import { loadConfig } from "./config/load.js";
2
+ import { pipeline } from "./core/pipeline.js";
3
+ // Services
4
+ import { createGitReader } from "./services/git/reader.js";
5
+ import { createGitOps } from "./services/git/ops.js";
6
+ import { createWorkspaceManager } from "./services/workspace/manager.js";
7
+ import { createTemplateService } from "./services/template/transform.js";
8
+ import { createStateService } from "./services/state/index.js";
9
+ import { createEditorService } from "./services/editor/index.js";
10
+ import { createReviewService } from "./services/review/index.js";
11
+ // Stages
12
+ import { resolveProjectStage } from "./stages/resolve-project.js";
13
+ import { syncRepositoryStage } from "./stages/sync.js";
14
+ import { selectTargetsStage } from "./stages/select.js";
15
+ import { checkEligibilityStage } from "./stages/eligibility.js";
16
+ import { loadTemplateStage } from "./stages/load-template.js";
17
+ import { editTemplateStage } from "./stages/edit-template.js";
18
+ import { planChangesStage } from "./stages/plan.js";
19
+ import { reviewChangesStage } from "./stages/review.js";
20
+ import { applyChangesStage } from "./stages/apply.js";
21
+ import { pushChangesStage } from "./stages/push.js";
22
+ import { reportResultsStage } from "./stages/report.js";
23
+ function createServices() {
24
+ return {
25
+ git: createGitReader(),
26
+ gitOps: createGitOps(),
27
+ workspace: createWorkspaceManager(),
28
+ template: createTemplateService(),
29
+ state: createStateService(),
30
+ editor: createEditorService(),
31
+ review: createReviewService(),
32
+ };
33
+ }
34
+ export async function run(input) {
35
+ const config = loadConfig(input);
36
+ const services = createServices();
37
+ const deps = { config, services };
38
+ const context = {
39
+ runId: `${Date.now()}`,
40
+ input,
41
+ };
42
+ const runPipeline = pipeline(resolveProjectStage, syncRepositoryStage, selectTargetsStage, checkEligibilityStage, loadTemplateStage, editTemplateStage, planChangesStage, reviewChangesStage, applyChangesStage, pushChangesStage, reportResultsStage);
43
+ const result = await runPipeline(context, deps);
44
+ if (result.ok && result.value.project) {
45
+ services.workspace.releaseLock(result.value.project);
46
+ }
47
+ return result;
48
+ }
@@ -0,0 +1,6 @@
1
+ export declare function handleCacheList(): Promise<void>;
2
+ export declare function handleCacheClean(target?: string, options?: {
3
+ all?: boolean;
4
+ yes?: boolean;
5
+ force?: boolean;
6
+ }): Promise<void>;
@@ -0,0 +1,111 @@
1
+ import chalk from "chalk";
2
+ import { clack, sectionHeader, statusLine } from "../utils/ui.js";
3
+ import { listCachedProjects, clearProjectCache, clearAllCache, } from "../services/workspace/cache.js";
4
+ import { getWorkspaceRoot } from "../services/workspace/manager.js";
5
+ export async function handleCacheList() {
6
+ const root = getWorkspaceRoot();
7
+ sectionHeader("Cached Projects (~/.git-worker)");
8
+ const projects = await listCachedProjects();
9
+ if (projects.length === 0) {
10
+ console.log("");
11
+ clack.log.info(`No cached repositories found in ${root}/projects`);
12
+ console.log("");
13
+ return;
14
+ }
15
+ console.log("");
16
+ console.log(chalk.bold(` ${"Name".padEnd(20)} ${"Remote URL / Path".padEnd(46)} Cached Date`));
17
+ console.log(chalk.gray(" " + "─".repeat(80)));
18
+ for (const p of projects) {
19
+ const name = chalk.cyan(p.name.slice(0, 18).padEnd(20));
20
+ const target = (p.remoteUrl || p.sourceRepoPath || "(unknown)").slice(0, 44).padEnd(46);
21
+ const date = p.createdAt ? p.createdAt.slice(0, 10) : "unknown";
22
+ console.log(` ${name} ${chalk.white(target)} ${chalk.gray(date)}`);
23
+ }
24
+ console.log(chalk.gray(" " + "─".repeat(80)));
25
+ console.log(chalk.gray(` Total: ${projects.length} repository/repositories cached in ${root}/projects`));
26
+ console.log("");
27
+ }
28
+ export async function handleCacheClean(target, options) {
29
+ const root = getWorkspaceRoot();
30
+ const projects = await listCachedProjects();
31
+ if (projects.length === 0) {
32
+ clack.log.info(`No cached repositories found in ${root}/projects`);
33
+ return;
34
+ }
35
+ const skipConfirm = options?.yes || options?.force || Boolean(process.env.CI);
36
+ // 1. If explicit --all flag provided
37
+ if (options?.all) {
38
+ if (!skipConfirm) {
39
+ const confirm = await clack.confirm({
40
+ message: `Are you sure you want to remove ALL ${projects.length} cached project(s) from ${root}?`,
41
+ initialValue: true,
42
+ });
43
+ if (clack.isCancel(confirm) || !confirm) {
44
+ clack.log.warn("Cache clean cancelled.");
45
+ return;
46
+ }
47
+ }
48
+ const result = await clearAllCache();
49
+ if (!result.ok) {
50
+ clack.log.error(result.error.message);
51
+ process.exit(1);
52
+ }
53
+ statusLine("ok", `Cleared all cached projects (${result.value.count} removed)`);
54
+ return;
55
+ }
56
+ // 2. If a specific target ID / URL / path was passed as positional argument
57
+ if (target) {
58
+ const result = await clearProjectCache(target);
59
+ if (!result.ok) {
60
+ clack.log.error(result.error.message);
61
+ process.exit(1);
62
+ }
63
+ statusLine("ok", `Removed cache for: ${result.value.removed}`);
64
+ return;
65
+ }
66
+ // 3. Interactive prompt
67
+ sectionHeader("Clean Cache");
68
+ const choice = await clack.select({
69
+ message: "Select cache to remove:",
70
+ options: [
71
+ {
72
+ value: "__ALL__",
73
+ label: `Clear ALL cached projects (${projects.length} projects)`,
74
+ hint: `removes all checkouts in ${root}/projects`,
75
+ },
76
+ ...projects.map((p) => ({
77
+ value: p.id,
78
+ label: `${p.name} (${p.remoteUrl || p.sourceRepoPath})`,
79
+ hint: `ID: ${p.id} (${p.createdAt ? p.createdAt.slice(0, 10) : ""})`,
80
+ })),
81
+ ],
82
+ });
83
+ if (clack.isCancel(choice)) {
84
+ clack.log.warn("Cancelled.");
85
+ return;
86
+ }
87
+ if (choice === "__ALL__") {
88
+ const confirm = await clack.confirm({
89
+ message: `Are you sure you want to remove ALL ${projects.length} cached project(s)?`,
90
+ initialValue: true,
91
+ });
92
+ if (clack.isCancel(confirm) || !confirm) {
93
+ clack.log.warn("Cancelled.");
94
+ return;
95
+ }
96
+ const result = await clearAllCache();
97
+ if (!result.ok) {
98
+ clack.log.error(result.error.message);
99
+ process.exit(1);
100
+ }
101
+ statusLine("ok", `Cleared all cached projects (${result.value.count} removed)`);
102
+ return;
103
+ }
104
+ // Specific selected project
105
+ const result = await clearProjectCache(choice);
106
+ if (!result.ok) {
107
+ clack.log.error(result.error.message);
108
+ process.exit(1);
109
+ }
110
+ statusLine("ok", `Removed cache for: ${result.value.removed}`);
111
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { run } from "../app.js";
4
+ import { handleCacheList, handleCacheClean } from "./commands.js";
5
+ import chalk from "chalk";
6
+ const program = new Command();
7
+ program
8
+ .name("git-worker")
9
+ .description("Safe, composable CLI/TUI for applying reviewed file changes across many Git branches")
10
+ .version("0.2.0")
11
+ .option("-r, --repo <path>", "Repository path or remote URL (default: current directory)")
12
+ .option("-c, --cached", "Select from previously cloned and cached repositories")
13
+ .option("-b, --branch <pattern>", "Branch pattern — glob (uat/*) or regex (/uat.*/)")
14
+ .option("-i, --invert-branch", "Invert the branch selection pattern")
15
+ .option("-f, --file <path>", "Target file path inside the repository (e.g. .env)")
16
+ .option("-e, --editor <name>", "Editor to use: code (VS Code) or vim")
17
+ .option("-m, --commit-msg <message>", "Commit message")
18
+ .option("-p, --push <policy>", "Push policy: prompt (default) | always | never")
19
+ .option("--remote <name>", "Git remote name (default: origin)")
20
+ .option("--auto-approve", "Auto-approve all changes (skip interactive review)")
21
+ .option("--ci", "CI mode: non-interactive, fail on missing dynamic values")
22
+ .action(async (opts) => {
23
+ // Validate push policy
24
+ if (opts.push && !["prompt", "always", "never"].includes(opts.push)) {
25
+ console.error(chalk.red(` ✗ Invalid push policy '${opts.push}'. Must be: prompt | always | never`));
26
+ process.exit(1);
27
+ }
28
+ // Validate incompatible combinations
29
+ if (opts.ci && opts.push === "prompt") {
30
+ console.error(chalk.yellow(` ⚠ Warning: --ci with --push prompt will not push (no interactive prompt in CI). Use --push always or --push never.`));
31
+ }
32
+ try {
33
+ const result = await run({
34
+ repo: opts.repo,
35
+ cached: opts.cached ?? false,
36
+ branch: opts.branch,
37
+ invertBranch: opts.invertBranch ?? false,
38
+ file: opts.file,
39
+ editor: opts.editor,
40
+ commitMsg: opts.commitMsg,
41
+ push: opts.push,
42
+ remote: opts.remote,
43
+ autoApprove: opts.autoApprove ?? false,
44
+ ci: opts.ci ?? false,
45
+ });
46
+ if (!result.ok) {
47
+ const { error } = result;
48
+ if (error.category === "INPUT_CANCELLED") {
49
+ console.log(chalk.gray("\n Cancelled."));
50
+ process.exit(0);
51
+ }
52
+ console.error(chalk.red(`\n ✗ [${error.category}] ${error.message}`));
53
+ process.exit(1);
54
+ }
55
+ process.exit(0);
56
+ }
57
+ catch (e) {
58
+ console.error(chalk.red(`\n ✗ Fatal: ${e.message}`));
59
+ if (process.env.DEBUG) {
60
+ console.error(e.stack);
61
+ }
62
+ process.exit(1);
63
+ }
64
+ });
65
+ // ── Cache management subcommands ──────────────────────────────────────────
66
+ const cacheCmd = program
67
+ .command("cache")
68
+ .description("Manage git-worker's cached workspaces and repositories (~/.git-worker)");
69
+ cacheCmd
70
+ .command("list")
71
+ .alias("ls")
72
+ .description("List all cached projects")
73
+ .action(async () => {
74
+ try {
75
+ await handleCacheList();
76
+ process.exit(0);
77
+ }
78
+ catch (e) {
79
+ console.error(chalk.red(` ✗ Error: ${e.message}`));
80
+ process.exit(1);
81
+ }
82
+ });
83
+ cacheCmd
84
+ .command("clean [target]")
85
+ .alias("clear")
86
+ .description("Clean workspace cache (all or a specific project)")
87
+ .option("-a, --all", "Clean all cached projects")
88
+ .option("-y, --yes", "Skip confirmation prompt")
89
+ .action(async (target, opts) => {
90
+ try {
91
+ await handleCacheClean(target, opts);
92
+ process.exit(0);
93
+ }
94
+ catch (e) {
95
+ console.error(chalk.red(` ✗ Error: ${e.message}`));
96
+ process.exit(1);
97
+ }
98
+ });
99
+ // Convenient top-level alias: git-worker clean [target] [-a]
100
+ program
101
+ .command("clean [target]")
102
+ .description("Shorthand to clean cached workspace(s)")
103
+ .option("-a, --all", "Clean all cached projects")
104
+ .option("-y, --yes", "Skip confirmation prompt")
105
+ .action(async (target, opts) => {
106
+ try {
107
+ await handleCacheClean(target, opts);
108
+ process.exit(0);
109
+ }
110
+ catch (e) {
111
+ console.error(chalk.red(` ✗ Error: ${e.message}`));
112
+ process.exit(1);
113
+ }
114
+ });
115
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import { AppConfig } from "./schema.js";
2
+ export declare const DEFAULT_CONFIG: AppConfig;
@@ -0,0 +1,44 @@
1
+ export const DEFAULT_CONFIG = {
2
+ git: {
3
+ remote: "origin",
4
+ fetchPrune: true,
5
+ cleanMode: "tracked-and-untracked",
6
+ pushPolicy: "prompt",
7
+ commitMessage: "chore: sync configuration",
8
+ },
9
+ workspace: {
10
+ root: ".git-worker",
11
+ lockTimeoutMs: 30000,
12
+ },
13
+ editor: {
14
+ name: "code",
15
+ editCommand: ["code", "--wait", "{file}"],
16
+ diffCommand: ["code", "--diff", "{before}", "{after}"],
17
+ },
18
+ template: {
19
+ placeholderOpen: "{",
20
+ placeholderClose: "}",
21
+ missingValuePolicy: "prompt-each-branch",
22
+ emptyValueIsPresent: true,
23
+ unmanagedKeyPolicy: "preserve",
24
+ removedTemplateKeyPolicy: "preserve",
25
+ },
26
+ review: {
27
+ autoApprove: false,
28
+ readOnlySnapshots: true,
29
+ },
30
+ execution: {
31
+ concurrency: 1,
32
+ stopOnBranchError: false,
33
+ },
34
+ report: {
35
+ writeJson: false,
36
+ writeMarkdown: false,
37
+ redactValues: true,
38
+ warnOnSecrets: true,
39
+ },
40
+ safety: {
41
+ rejectNoRemote: true,
42
+ saveOnPartialSuccess: true,
43
+ },
44
+ };
@@ -0,0 +1,3 @@
1
+ import { AppConfig } from "./schema.js";
2
+ import { CliInput } from "../core/types.js";
3
+ export declare function loadConfig(cliInput: CliInput): AppConfig;
@@ -0,0 +1,65 @@
1
+ import { AppConfigSchema } from "./schema.js";
2
+ import { DEFAULT_CONFIG } from "./defaults.js";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join, resolve } from "path";
5
+ function loadProjectConfig(repoPath) {
6
+ const candidates = [
7
+ join(resolve(repoPath), "git-worker.config.json"),
8
+ join(resolve(repoPath), ".git-worker.config.json"),
9
+ ];
10
+ for (const candidate of candidates) {
11
+ if (existsSync(candidate)) {
12
+ try {
13
+ const raw = readFileSync(candidate, "utf-8");
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ // ignore malformed
18
+ }
19
+ }
20
+ }
21
+ return {};
22
+ }
23
+ function deepMerge(base, override) {
24
+ const result = { ...base };
25
+ for (const key of Object.keys(override)) {
26
+ const overrideVal = override[key];
27
+ if (overrideVal === undefined)
28
+ continue;
29
+ if (typeof overrideVal === "object" &&
30
+ overrideVal !== null &&
31
+ !Array.isArray(overrideVal) &&
32
+ typeof result[key] === "object" &&
33
+ result[key] !== null) {
34
+ result[key] = deepMerge(result[key], overrideVal);
35
+ }
36
+ else {
37
+ result[key] = overrideVal;
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ export function loadConfig(cliInput) {
43
+ // Load project config from repo directory
44
+ const repoPath = cliInput.repo || process.cwd();
45
+ const projectConfig = loadProjectConfig(repoPath);
46
+ // Merge: defaults < project config < CLI overrides
47
+ const cliOverrides = {
48
+ git: {
49
+ ...DEFAULT_CONFIG.git,
50
+ ...(cliInput.push ? { pushPolicy: cliInput.push } : {}),
51
+ ...(cliInput.commitMsg ? { commitMessage: cliInput.commitMsg } : {}),
52
+ ...(cliInput.remote ? { remote: cliInput.remote } : {}),
53
+ },
54
+ editor: {
55
+ ...DEFAULT_CONFIG.editor,
56
+ ...(cliInput.editor ? { name: cliInput.editor } : {}),
57
+ },
58
+ review: {
59
+ ...DEFAULT_CONFIG.review,
60
+ ...(cliInput.autoApprove !== undefined ? { autoApprove: cliInput.autoApprove } : {}),
61
+ },
62
+ };
63
+ const merged = deepMerge(deepMerge(DEFAULT_CONFIG, projectConfig), cliOverrides);
64
+ return Object.freeze(AppConfigSchema.parse(merged));
65
+ }