@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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { simpleGit } from "simple-git";
|
|
2
|
+
import { ok, err } from "../../core/types.js";
|
|
3
|
+
import { gitError } from "../../core/errors.js";
|
|
4
|
+
import { writeFile, mkdir, rename } from "fs/promises";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
export async function ensureRepo(repoPath) {
|
|
7
|
+
try {
|
|
8
|
+
const git = simpleGit(repoPath);
|
|
9
|
+
const isRepo = await git.checkIsRepo();
|
|
10
|
+
if (!isRepo)
|
|
11
|
+
return err(gitError("Not a git repository"));
|
|
12
|
+
return ok(undefined);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
return err(gitError("Failed to check repository", e));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function checkoutBranch(repoPath, branchName) {
|
|
19
|
+
try {
|
|
20
|
+
const git = simpleGit(repoPath);
|
|
21
|
+
// Align local branch to origin/branchName if remote branch exists
|
|
22
|
+
try {
|
|
23
|
+
await git.checkout(["-B", branchName, `origin/${branchName}`]);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fallback for local-only branch
|
|
27
|
+
await git.checkout(branchName);
|
|
28
|
+
}
|
|
29
|
+
return ok(undefined);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return err(gitError(`Failed to checkout '${branchName}'`, e));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function resetHard(repoPath) {
|
|
36
|
+
try {
|
|
37
|
+
const git = simpleGit(repoPath);
|
|
38
|
+
await git.reset(["--hard"]);
|
|
39
|
+
return ok(undefined);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
return err(gitError("Failed to reset --hard", e));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function clean(repoPath, mode = "tracked-and-untracked") {
|
|
46
|
+
try {
|
|
47
|
+
const git = simpleGit(repoPath);
|
|
48
|
+
if (mode === "tracked-and-untracked") {
|
|
49
|
+
await git.clean("f", ["-d"]);
|
|
50
|
+
}
|
|
51
|
+
// tracked-only: git reset --hard already handles it
|
|
52
|
+
return ok(undefined);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return err(gitError("Failed to clean working tree", e));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Atomically write a file (via temp file + rename) and stage it.
|
|
60
|
+
* Only stages the specific path — no glob or wildcard.
|
|
61
|
+
*/
|
|
62
|
+
export async function writeAndStage(repoPath, filePath, content) {
|
|
63
|
+
try {
|
|
64
|
+
const fullPath = join(repoPath, filePath);
|
|
65
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
66
|
+
// Atomic write: write to temp, then rename
|
|
67
|
+
const tmp = fullPath + ".git-worker-tmp";
|
|
68
|
+
await writeFile(tmp, content, "utf-8");
|
|
69
|
+
await rename(tmp, fullPath);
|
|
70
|
+
const git = simpleGit(repoPath);
|
|
71
|
+
await git.add([filePath]);
|
|
72
|
+
return ok(undefined);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return err(gitError(`Failed to write/stage '${filePath}'`, e));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns the list of staged file paths.
|
|
80
|
+
* Used to verify only the intended path is staged before committing.
|
|
81
|
+
*/
|
|
82
|
+
export async function getStagedFiles(repoPath) {
|
|
83
|
+
try {
|
|
84
|
+
const git = simpleGit(repoPath);
|
|
85
|
+
const diff = await git.diff(["--cached", "--name-only"]);
|
|
86
|
+
const files = diff
|
|
87
|
+
.split("\n")
|
|
88
|
+
.map((l) => l.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
return ok(files);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return err(gitError("Failed to get staged files", e));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function commit(repoPath, message) {
|
|
97
|
+
try {
|
|
98
|
+
const git = simpleGit(repoPath);
|
|
99
|
+
const result = await git.commit(message);
|
|
100
|
+
return ok(result.commit);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return err(gitError(`Failed to commit: ${e.message}`, e));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function push(repoPath, remote, branchName) {
|
|
107
|
+
try {
|
|
108
|
+
const git = simpleGit(repoPath);
|
|
109
|
+
// Non-force push only
|
|
110
|
+
await git.push([remote, `${branchName}:${branchName}`]);
|
|
111
|
+
return ok(undefined);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return err(gitError(`Failed to push '${branchName}' to '${remote}': ${e.message}`, e));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function getBranchSha(repoPath, branchName) {
|
|
118
|
+
try {
|
|
119
|
+
const git = simpleGit(repoPath);
|
|
120
|
+
const sha = await git.revparse([branchName]);
|
|
121
|
+
return ok(sha.trim());
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return err(gitError(`Failed to get SHA for '${branchName}'`, e));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function createGitOps() {
|
|
128
|
+
return {
|
|
129
|
+
ensureRepo,
|
|
130
|
+
checkoutBranch,
|
|
131
|
+
resetHard,
|
|
132
|
+
clean,
|
|
133
|
+
writeAndStage,
|
|
134
|
+
getStagedFiles,
|
|
135
|
+
commit,
|
|
136
|
+
push,
|
|
137
|
+
getBranchSha,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Result, BranchRef } from "../../core/types.js";
|
|
2
|
+
export type GitReader = {
|
|
3
|
+
readFileFromBranch(repoPath: string, branch: BranchRef, filePath: string): Promise<Result<string>>;
|
|
4
|
+
listBranches(repoPath: string, remote?: string): Promise<Result<BranchRef[]>>;
|
|
5
|
+
getCurrentBranch(repoPath: string): Promise<Result<string>>;
|
|
6
|
+
fetch(repoPath: string, remote?: string, prune?: boolean): Promise<Result<void>>;
|
|
7
|
+
getRemoteUrl(repoPath: string, remote?: string): Promise<Result<string>>;
|
|
8
|
+
};
|
|
9
|
+
export declare function readFileFromBranch(repoPath: string, branch: BranchRef, filePath: string): Promise<Result<string>>;
|
|
10
|
+
export declare function listBranches(repoPath: string, remote?: string): Promise<Result<BranchRef[]>>;
|
|
11
|
+
export declare function getCurrentBranch(repoPath: string): Promise<Result<string>>;
|
|
12
|
+
export declare function fetch(repoPath: string, remote?: string, prune?: boolean): Promise<Result<void>>;
|
|
13
|
+
export declare function getRemoteUrl(repoPath: string, remote?: string): Promise<Result<string>>;
|
|
14
|
+
export declare function createGitReader(): GitReader;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { simpleGit } from "simple-git";
|
|
2
|
+
import { ok, err } from "../../core/types.js";
|
|
3
|
+
import { gitError } from "../../core/errors.js";
|
|
4
|
+
export async function readFileFromBranch(repoPath, branch, filePath) {
|
|
5
|
+
try {
|
|
6
|
+
const git = simpleGit(repoPath);
|
|
7
|
+
// For remote branches, use the full remote ref; for local use name
|
|
8
|
+
const ref = branch.isRemote && branch.remote
|
|
9
|
+
? `${branch.remote}/${branch.name}`
|
|
10
|
+
: branch.name;
|
|
11
|
+
const content = await git.show([`${ref}:${filePath}`]);
|
|
12
|
+
return ok(content);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
if (e.message?.includes("does not exist") ||
|
|
16
|
+
e.message?.includes("fatal") ||
|
|
17
|
+
e.message?.includes("not a valid object")) {
|
|
18
|
+
return err(gitError(`File '${filePath}' not found in branch '${branch.name}'`, e));
|
|
19
|
+
}
|
|
20
|
+
return err(gitError(`Failed to read '${filePath}' from '${branch.name}'`, e));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function listBranches(repoPath, remote) {
|
|
24
|
+
try {
|
|
25
|
+
const git = simpleGit(repoPath);
|
|
26
|
+
// Fast path: use git for-each-ref plumbing command.
|
|
27
|
+
// Retrieves all branch names, SHAs, and ref types in a single process execution (<25ms for 300+ branches)
|
|
28
|
+
// instead of running hundreds of sequential rev-parse child processes.
|
|
29
|
+
try {
|
|
30
|
+
const rawOutput = await git.raw([
|
|
31
|
+
"for-each-ref",
|
|
32
|
+
"--sort=-committerdate",
|
|
33
|
+
"--format=%(objectname)\t%(refname)",
|
|
34
|
+
"refs/remotes",
|
|
35
|
+
"refs/heads",
|
|
36
|
+
]);
|
|
37
|
+
const remoteBranches = [];
|
|
38
|
+
const localBranches = [];
|
|
39
|
+
for (const rawLine of rawOutput.split("\n")) {
|
|
40
|
+
const line = rawLine.trim();
|
|
41
|
+
if (!line)
|
|
42
|
+
continue;
|
|
43
|
+
const tabIdx = line.indexOf("\t");
|
|
44
|
+
if (tabIdx === -1)
|
|
45
|
+
continue;
|
|
46
|
+
const sha = line.substring(0, tabIdx).trim();
|
|
47
|
+
const refname = line.substring(tabIdx + 1).trim();
|
|
48
|
+
if (!sha || !refname)
|
|
49
|
+
continue;
|
|
50
|
+
if (refname.startsWith("refs/remotes/")) {
|
|
51
|
+
const rest = refname.substring("refs/remotes/".length);
|
|
52
|
+
const slashIdx = rest.indexOf("/");
|
|
53
|
+
if (slashIdx === -1)
|
|
54
|
+
continue;
|
|
55
|
+
const remoteName = rest.substring(0, slashIdx);
|
|
56
|
+
const branchName = rest.substring(slashIdx + 1);
|
|
57
|
+
// Skip symbolic refs like origin/HEAD
|
|
58
|
+
if (branchName === "HEAD" || branchName.endsWith("/HEAD"))
|
|
59
|
+
continue;
|
|
60
|
+
if (remote && remoteName !== remote)
|
|
61
|
+
continue;
|
|
62
|
+
remoteBranches.push({
|
|
63
|
+
name: branchName,
|
|
64
|
+
remote: remoteName,
|
|
65
|
+
sha,
|
|
66
|
+
isRemote: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (refname.startsWith("refs/heads/")) {
|
|
70
|
+
const branchName = refname.substring("refs/heads/".length);
|
|
71
|
+
localBranches.push({
|
|
72
|
+
name: branchName,
|
|
73
|
+
sha,
|
|
74
|
+
isRemote: false,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const branches = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
// 1. Remote branches take priority (source of truth for managed clone)
|
|
81
|
+
for (const b of remoteBranches) {
|
|
82
|
+
if (!seen.has(b.name)) {
|
|
83
|
+
seen.add(b.name);
|
|
84
|
+
branches.push(b);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 2. Local branches (only if not already discovered on remote)
|
|
88
|
+
for (const b of localBranches) {
|
|
89
|
+
if (!seen.has(b.name)) {
|
|
90
|
+
seen.add(b.name);
|
|
91
|
+
branches.push(b);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return ok(branches);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Fallback path: use standard branch enumeration if for-each-ref fails
|
|
98
|
+
const branches = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
try {
|
|
101
|
+
const remoteBranches = await git.branch(["-r"]);
|
|
102
|
+
for (const fullName of remoteBranches.all) {
|
|
103
|
+
if (fullName.includes("HEAD"))
|
|
104
|
+
continue;
|
|
105
|
+
const parts = fullName.trim().split("/");
|
|
106
|
+
if (parts.length < 2)
|
|
107
|
+
continue;
|
|
108
|
+
const remoteName = parts[0];
|
|
109
|
+
const branchName = parts.slice(1).join("/");
|
|
110
|
+
if (remote && remoteName !== remote)
|
|
111
|
+
continue;
|
|
112
|
+
if (seen.has(branchName))
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const sha = await git.revparse([fullName.trim()]);
|
|
116
|
+
seen.add(branchName);
|
|
117
|
+
branches.push({
|
|
118
|
+
name: branchName,
|
|
119
|
+
remote: remoteName,
|
|
120
|
+
sha: sha.trim(),
|
|
121
|
+
isRemote: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// skip unresolvable
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// no remotes
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const local = await git.branchLocal();
|
|
134
|
+
for (const name of local.all) {
|
|
135
|
+
if (!seen.has(name)) {
|
|
136
|
+
const sha = await git.revparse([name]);
|
|
137
|
+
seen.add(name);
|
|
138
|
+
branches.push({ name, sha: sha.trim(), isRemote: false });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// ignore
|
|
144
|
+
}
|
|
145
|
+
return ok(branches);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return err(gitError("Failed to list branches", e));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export async function getCurrentBranch(repoPath) {
|
|
153
|
+
try {
|
|
154
|
+
const git = simpleGit(repoPath);
|
|
155
|
+
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
|
|
156
|
+
return ok(branch.trim());
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
return err(gitError("Failed to get current branch", e));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function fetch(repoPath, remote = "origin", prune = true) {
|
|
163
|
+
try {
|
|
164
|
+
const git = simpleGit(repoPath);
|
|
165
|
+
const args = prune ? [remote, "--prune"] : [remote];
|
|
166
|
+
await git.fetch(args);
|
|
167
|
+
return ok(undefined);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
return err(gitError(`Failed to fetch from '${remote}'`, e));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function getRemoteUrl(repoPath, remote = "origin") {
|
|
174
|
+
try {
|
|
175
|
+
const git = simpleGit(repoPath);
|
|
176
|
+
const remotes = await git.getRemotes(true);
|
|
177
|
+
const target = remotes.find((r) => r.name === remote) || remotes[0];
|
|
178
|
+
if (!target?.refs?.fetch) {
|
|
179
|
+
return err(gitError(`No remote '${remote}' found`));
|
|
180
|
+
}
|
|
181
|
+
return ok(target.refs.fetch);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
return err(gitError("Failed to get remote URL", e));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function createGitReader() {
|
|
188
|
+
return {
|
|
189
|
+
readFileFromBranch,
|
|
190
|
+
listBranches,
|
|
191
|
+
getCurrentBranch,
|
|
192
|
+
fetch,
|
|
193
|
+
getRemoteUrl,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BranchResult, PushResult, BranchPlan } from "../../core/types.js";
|
|
2
|
+
import { AppConfig } from "../../config/schema.js";
|
|
3
|
+
export type RunReport = {
|
|
4
|
+
runId: string;
|
|
5
|
+
targetPath: string;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
completedAt: string;
|
|
8
|
+
plans: BranchPlan[];
|
|
9
|
+
results: BranchResult[];
|
|
10
|
+
pushResults: PushResult[];
|
|
11
|
+
};
|
|
12
|
+
export type ReportService = {
|
|
13
|
+
saveJson(reportsDir: string, report: RunReport, config: AppConfig): Promise<void>;
|
|
14
|
+
saveMarkdown(reportsDir: string, report: RunReport, config: AppConfig): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function saveJson(reportsDir: string, report: RunReport, config: AppConfig): Promise<void>;
|
|
17
|
+
export declare function saveMarkdown(reportsDir: string, report: RunReport, config: AppConfig): Promise<void>;
|
|
18
|
+
export declare function createReportService(): ReportService;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
function redactContent(text) {
|
|
4
|
+
// Replace value portions of assignments with [REDACTED]
|
|
5
|
+
return text.replace(/^([A-Za-z_][A-Za-z0-9_]*)=.+$/gm, "$1=[REDACTED]");
|
|
6
|
+
}
|
|
7
|
+
export async function saveJson(reportsDir, report, config) {
|
|
8
|
+
if (!config.report.writeJson)
|
|
9
|
+
return;
|
|
10
|
+
await mkdir(reportsDir, { recursive: true });
|
|
11
|
+
const filePath = join(reportsDir, `${report.runId}.json`);
|
|
12
|
+
let data = report;
|
|
13
|
+
if (config.report.redactValues) {
|
|
14
|
+
data = {
|
|
15
|
+
...report,
|
|
16
|
+
plans: report.plans.map((p) => ({
|
|
17
|
+
...p,
|
|
18
|
+
beforeContent: redactContent(p.beforeContent),
|
|
19
|
+
afterContent: redactContent(p.afterContent),
|
|
20
|
+
diff: redactContent(p.diff),
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
export async function saveMarkdown(reportsDir, report, config) {
|
|
27
|
+
if (!config.report.writeMarkdown)
|
|
28
|
+
return;
|
|
29
|
+
await mkdir(reportsDir, { recursive: true });
|
|
30
|
+
const filePath = join(reportsDir, `${report.runId}.md`);
|
|
31
|
+
const statusEmoji = {
|
|
32
|
+
committed: "✅",
|
|
33
|
+
failed: "❌",
|
|
34
|
+
rejected: "⊘",
|
|
35
|
+
unchanged: "○",
|
|
36
|
+
skipped: "–",
|
|
37
|
+
};
|
|
38
|
+
const lines = [
|
|
39
|
+
`# Git Worker Run Report`,
|
|
40
|
+
``,
|
|
41
|
+
`**Run ID:** ${report.runId} `,
|
|
42
|
+
`**Target:** ${report.targetPath} `,
|
|
43
|
+
`**Started:** ${report.startedAt} `,
|
|
44
|
+
`**Completed:** ${report.completedAt} `,
|
|
45
|
+
``,
|
|
46
|
+
`## Results`,
|
|
47
|
+
``,
|
|
48
|
+
`| Branch | Status | Commit |`,
|
|
49
|
+
`|--------|--------|--------|`,
|
|
50
|
+
...report.results.map((r) => `| ${r.branch.name} | ${statusEmoji[r.status] ?? r.status} ${r.status} | ${r.commitSha ? `\`${r.commitSha.slice(0, 8)}\`` : "—"} |`),
|
|
51
|
+
``,
|
|
52
|
+
];
|
|
53
|
+
if (report.pushResults.length > 0) {
|
|
54
|
+
lines.push(`## Push Results`, ``);
|
|
55
|
+
lines.push(`| Branch | Status |`, `|--------|--------|`);
|
|
56
|
+
for (const pr of report.pushResults) {
|
|
57
|
+
lines.push(`| ${pr.branch.name} | ${pr.status} |`);
|
|
58
|
+
}
|
|
59
|
+
lines.push(``);
|
|
60
|
+
}
|
|
61
|
+
await writeFile(filePath, lines.join("\n"), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
export function createReportService() {
|
|
64
|
+
return {
|
|
65
|
+
saveJson,
|
|
66
|
+
saveMarkdown,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Result, BranchPlan } from "../../core/types.js";
|
|
2
|
+
export type ReviewService = {
|
|
3
|
+
writeSnapshots(stagingDir: string, runId: string, plan: BranchPlan): Promise<Result<{
|
|
4
|
+
beforePath: string;
|
|
5
|
+
afterPath: string;
|
|
6
|
+
}>>;
|
|
7
|
+
computeDiff(before: string, after: string, filePath: string): string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Write before/after snapshots to the staging directory for a plan.
|
|
11
|
+
* Returns the file paths for opening in a diff viewer.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeSnapshots(stagingDir: string, runId: string, plan: BranchPlan): Promise<Result<{
|
|
14
|
+
beforePath: string;
|
|
15
|
+
afterPath: string;
|
|
16
|
+
}>>;
|
|
17
|
+
/**
|
|
18
|
+
* Compute a unified diff string between before and after content.
|
|
19
|
+
* Uses the `diff` package for accurate line-level diffing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeDiff(before: string, after: string, filePath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Format a diff for terminal display with color markers.
|
|
24
|
+
* Returns an array of lines with +/- prefixes for coloring.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseDiffLines(diff: string): Array<{
|
|
27
|
+
line: string;
|
|
28
|
+
type: "added" | "removed" | "header" | "context";
|
|
29
|
+
}>;
|
|
30
|
+
export declare function createReviewService(): ReviewService;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ok, err } from "../../core/types.js";
|
|
2
|
+
import { workspaceError } from "../../core/errors.js";
|
|
3
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { createPatch } from "diff";
|
|
6
|
+
/**
|
|
7
|
+
* Write before/after snapshots to the staging directory for a plan.
|
|
8
|
+
* Returns the file paths for opening in a diff viewer.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeSnapshots(stagingDir, runId, plan) {
|
|
11
|
+
try {
|
|
12
|
+
const planDir = join(stagingDir, runId, plan.branch.name.replace(/\//g, "__"));
|
|
13
|
+
await mkdir(planDir, { recursive: true });
|
|
14
|
+
const beforePath = join(planDir, `before.${plan.path.split("/").pop()}`);
|
|
15
|
+
const afterPath = join(planDir, `after.${plan.path.split("/").pop()}`);
|
|
16
|
+
await writeFile(beforePath, plan.beforeContent, "utf-8");
|
|
17
|
+
await writeFile(afterPath, plan.afterContent, "utf-8");
|
|
18
|
+
return ok({ beforePath, afterPath });
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return err(workspaceError(`Failed to write snapshots: ${e.message}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute a unified diff string between before and after content.
|
|
26
|
+
* Uses the `diff` package for accurate line-level diffing.
|
|
27
|
+
*/
|
|
28
|
+
export function computeDiff(before, after, filePath) {
|
|
29
|
+
if (before === after)
|
|
30
|
+
return "";
|
|
31
|
+
return createPatch(filePath, before, after, "before", "after");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Format a diff for terminal display with color markers.
|
|
35
|
+
* Returns an array of lines with +/- prefixes for coloring.
|
|
36
|
+
*/
|
|
37
|
+
export function parseDiffLines(diff) {
|
|
38
|
+
const lines = diff.split("\n");
|
|
39
|
+
return lines.map((line) => {
|
|
40
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@")) {
|
|
41
|
+
return { line, type: "header" };
|
|
42
|
+
}
|
|
43
|
+
else if (line.startsWith("+")) {
|
|
44
|
+
return { line, type: "added" };
|
|
45
|
+
}
|
|
46
|
+
else if (line.startsWith("-")) {
|
|
47
|
+
return { line, type: "removed" };
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return { line, type: "context" };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function createReviewService() {
|
|
55
|
+
return {
|
|
56
|
+
writeSnapshots,
|
|
57
|
+
computeDiff,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Result } from "../../core/types.js";
|
|
2
|
+
export type TemplateState = {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
targetPath: string;
|
|
5
|
+
template: string;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
lastRunSummary?: {
|
|
8
|
+
committed: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
skipped: number;
|
|
11
|
+
total: number;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type StateService = {
|
|
15
|
+
loadTemplate(stateDir: string, targetPath: string): Promise<Result<TemplateState | null>>;
|
|
16
|
+
saveTemplate(stateDir: string, targetPath: string, templateRaw: string, summary?: TemplateState["lastRunSummary"]): Promise<Result<void>>;
|
|
17
|
+
};
|
|
18
|
+
export declare function loadTemplate(stateDir: string, targetPath: string): Promise<Result<TemplateState | null>>;
|
|
19
|
+
export declare function saveTemplate(stateDir: string, targetPath: string, templateRaw: string, summary?: TemplateState["lastRunSummary"]): Promise<Result<void>>;
|
|
20
|
+
export declare function createStateService(): StateService;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ok, err } from "../../core/types.js";
|
|
2
|
+
import { workspaceError } from "../../core/errors.js";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
function stateFileName(targetPath) {
|
|
7
|
+
return targetPath.replace(/[/\\]/g, "__") + ".state.json";
|
|
8
|
+
}
|
|
9
|
+
export async function loadTemplate(stateDir, targetPath) {
|
|
10
|
+
const filePath = join(stateDir, stateFileName(targetPath));
|
|
11
|
+
if (!existsSync(filePath)) {
|
|
12
|
+
return ok(null);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(filePath, "utf-8");
|
|
16
|
+
const data = JSON.parse(raw);
|
|
17
|
+
if (data.schemaVersion !== 1) {
|
|
18
|
+
return ok(null); // Unknown schema — start fresh
|
|
19
|
+
}
|
|
20
|
+
return ok(data);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
return err(workspaceError(`Failed to load template state: ${e.message}`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function saveTemplate(stateDir, targetPath, templateRaw, summary) {
|
|
27
|
+
try {
|
|
28
|
+
await mkdir(stateDir, { recursive: true });
|
|
29
|
+
const filePath = join(stateDir, stateFileName(targetPath));
|
|
30
|
+
const state = {
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
targetPath,
|
|
33
|
+
template: templateRaw,
|
|
34
|
+
updatedAt: new Date().toISOString(),
|
|
35
|
+
...(summary ? { lastRunSummary: summary } : {}),
|
|
36
|
+
};
|
|
37
|
+
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
38
|
+
return ok(undefined);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return err(workspaceError(`Failed to save template state: ${e.message}`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function createStateService() {
|
|
45
|
+
return {
|
|
46
|
+
loadTemplate,
|
|
47
|
+
saveTemplate,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TemplateDocument, Result, EnvEntry } from "../../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a .env template file into structured assignments.
|
|
4
|
+
* Template uses {KEY} for dynamic placeholders, KEY=value for static.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseEnvTemplate(raw: string): Result<TemplateDocument>;
|
|
7
|
+
/**
|
|
8
|
+
* Parse a .env file into a map of key → EnvEntry (preserving export, quoting, lineIndex).
|
|
9
|
+
* Returns an error if duplicate keys are found.
|
|
10
|
+
* Lines that cannot be parsed as assignments are tracked but not included in the map.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseEnvFile(content: string): Result<{
|
|
13
|
+
map: Map<string, EnvEntry>;
|
|
14
|
+
lines: string[];
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Synthesize a first-run template draft from common keys across branches.
|
|
18
|
+
* Uniform values → static assignment. Varying values → dynamic placeholder.
|
|
19
|
+
*/
|
|
20
|
+
export declare function synthesizeTemplate(branchContents: Array<{
|
|
21
|
+
branchName: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}>): string;
|