@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.
- package/README.md +114 -2
- package/dist/app.d.ts +2 -0
- package/dist/app.js +48 -0
- package/dist/cli/commands.d.ts +6 -0
- package/dist/cli/commands.js +111 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +115 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +44 -0
- package/dist/config/load.d.ts +3 -0
- package/dist/config/load.js +65 -0
- package/dist/config/schema.d.ts +200 -0
- package/dist/config/schema.js +45 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/pipeline.d.ts +3 -0
- package/dist/core/pipeline.js +14 -0
- package/dist/core/types.d.ts +153 -0
- package/dist/core/types.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/services/editor/index.d.ts +9 -0
- package/dist/services/editor/index.js +59 -0
- package/dist/services/git/ops.d.ts +30 -0
- package/dist/services/git/ops.js +139 -0
- package/dist/services/git/reader.d.ts +14 -0
- package/dist/services/git/reader.js +195 -0
- package/dist/services/report/index.d.ts +18 -0
- package/dist/services/report/index.js +68 -0
- package/dist/services/review/index.d.ts +30 -0
- package/dist/services/review/index.js +59 -0
- package/dist/services/state/index.d.ts +20 -0
- package/dist/services/state/index.js +49 -0
- package/dist/services/template/env.d.ts +23 -0
- package/dist/services/template/env.js +146 -0
- package/dist/services/template/interpolator.d.ts +30 -0
- package/dist/services/template/interpolator.js +75 -0
- package/dist/services/template/transform.d.ts +22 -0
- package/dist/services/template/transform.js +75 -0
- package/dist/services/template/validator.d.ts +5 -0
- package/dist/services/template/validator.js +68 -0
- package/dist/services/workspace/cache.d.ts +32 -0
- package/dist/services/workspace/cache.js +107 -0
- package/dist/services/workspace/manager.d.ts +44 -0
- package/dist/services/workspace/manager.js +301 -0
- package/dist/stages/apply.d.ts +3 -0
- package/dist/stages/apply.js +161 -0
- package/dist/stages/edit-template.d.ts +3 -0
- package/dist/stages/edit-template.js +198 -0
- package/dist/stages/eligibility.d.ts +3 -0
- package/dist/stages/eligibility.js +68 -0
- package/dist/stages/load-template.d.ts +3 -0
- package/dist/stages/load-template.js +168 -0
- package/dist/stages/plan.d.ts +3 -0
- package/dist/stages/plan.js +270 -0
- package/dist/stages/push.d.ts +3 -0
- package/dist/stages/push.js +99 -0
- package/dist/stages/report.d.ts +3 -0
- package/dist/stages/report.js +103 -0
- package/dist/stages/resolve-project.d.ts +3 -0
- package/dist/stages/resolve-project.js +294 -0
- package/dist/stages/review.d.ts +3 -0
- package/dist/stages/review.js +197 -0
- package/dist/stages/select.d.ts +13 -0
- package/dist/stages/select.js +265 -0
- package/dist/stages/sync.d.ts +3 -0
- package/dist/stages/sync.js +47 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +7 -0
- package/dist/utils/ui.d.ts +21 -0
- package/dist/utils/ui.js +113 -0
- package/dist/utils/which.d.ts +5 -0
- package/dist/utils/which.js +16 -0
- package/docs/FEATURES.md +84 -0
- package/docs/SERVICES.md +57 -0
- package/docs/architecture.md +38 -0
- package/docs/usage.md +337 -0
- package/package.json +58 -4
package/README.md
CHANGED
|
@@ -1,3 +1,115 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Git Worker
|
|
2
2
|
|
|
3
|
-
|
|
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
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,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,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,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,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
|
+
}
|