@cvr/stacked 0.1.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 +81 -0
- package/bin/stacked +0 -0
- package/package.json +46 -0
- package/scripts/build.ts +53 -0
- package/skills/stacked/SKILL.md +181 -0
- package/src/commands/adopt.ts +39 -0
- package/src/commands/bottom.ts +29 -0
- package/src/commands/checkout.ts +15 -0
- package/src/commands/create.ts +46 -0
- package/src/commands/delete.ts +55 -0
- package/src/commands/index.ts +34 -0
- package/src/commands/list.ts +50 -0
- package/src/commands/log.ts +34 -0
- package/src/commands/restack.ts +40 -0
- package/src/commands/submit.ts +68 -0
- package/src/commands/sync.ts +42 -0
- package/src/commands/top.ts +29 -0
- package/src/commands/trunk.ts +21 -0
- package/src/errors/index.ts +15 -0
- package/src/main.ts +22 -0
- package/src/services/Git.ts +127 -0
- package/src/services/GitHub.ts +126 -0
- package/src/services/Stack.ts +299 -0
- package/tsconfig.json +64 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { StackService } from "../services/Stack.js";
|
|
5
|
+
|
|
6
|
+
export const restack = Command.make("restack").pipe(
|
|
7
|
+
Command.withDescription("Rebase children after mid-stack changes"),
|
|
8
|
+
Command.withHandler(() =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const git = yield* GitService;
|
|
11
|
+
const stacks = yield* StackService;
|
|
12
|
+
|
|
13
|
+
const currentBranch = yield* git.currentBranch();
|
|
14
|
+
const result = yield* stacks.currentStack();
|
|
15
|
+
if (result === null) {
|
|
16
|
+
yield* Console.error("Not on a stacked branch");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { branches } = result.stack;
|
|
21
|
+
const idx = branches.indexOf(currentBranch);
|
|
22
|
+
if (idx === -1) {
|
|
23
|
+
yield* Console.error("Current branch not found in stack");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (let i = idx + 1; i < branches.length; i++) {
|
|
28
|
+
const branch = branches[i];
|
|
29
|
+
if (branch === undefined) continue;
|
|
30
|
+
const base = branches[i - 1] ?? currentBranch;
|
|
31
|
+
yield* Console.log(`Rebasing ${branch} onto ${base}...`);
|
|
32
|
+
yield* git.checkout(branch);
|
|
33
|
+
yield* git.rebase(base);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
yield* git.checkout(currentBranch);
|
|
37
|
+
yield* Console.log("Stack restacked");
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { StackService } from "../services/Stack.js";
|
|
5
|
+
import { GitHubService } from "../services/GitHub.js";
|
|
6
|
+
|
|
7
|
+
const draftFlag = Flag.boolean("draft").pipe(Flag.withAlias("d"));
|
|
8
|
+
const forceFlag = Flag.boolean("force").pipe(Flag.withAlias("f"));
|
|
9
|
+
const dryRunFlag = Flag.boolean("dry-run");
|
|
10
|
+
|
|
11
|
+
export const submit = Command.make("submit", {
|
|
12
|
+
draft: draftFlag,
|
|
13
|
+
force: forceFlag,
|
|
14
|
+
dryRun: dryRunFlag,
|
|
15
|
+
}).pipe(
|
|
16
|
+
Command.withDescription("Push all stack branches and create/update PRs via gh"),
|
|
17
|
+
Command.withHandler(({ draft, force, dryRun }) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const git = yield* GitService;
|
|
20
|
+
const stacks = yield* StackService;
|
|
21
|
+
const gh = yield* GitHubService;
|
|
22
|
+
|
|
23
|
+
const result = yield* stacks.currentStack();
|
|
24
|
+
if (result === null) {
|
|
25
|
+
yield* Console.error("Not on a stacked branch");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const trunk = yield* stacks.getTrunk();
|
|
30
|
+
const { branches } = result.stack;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < branches.length; i++) {
|
|
33
|
+
const branch = branches[i];
|
|
34
|
+
if (branch === undefined) continue;
|
|
35
|
+
const base = i === 0 ? trunk : (branches[i - 1] ?? trunk);
|
|
36
|
+
|
|
37
|
+
if (dryRun) {
|
|
38
|
+
yield* Console.log(`Would push ${branch} and create/update PR (base: ${base})`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
yield* Console.log(`Pushing ${branch}...`);
|
|
43
|
+
yield* git.push(branch, { force });
|
|
44
|
+
|
|
45
|
+
const existingPR = yield* gh.getPR(branch);
|
|
46
|
+
|
|
47
|
+
if (existingPR !== null) {
|
|
48
|
+
if (existingPR.base !== base) {
|
|
49
|
+
yield* Console.log(`Updating PR #${existingPR.number} base to ${base}`);
|
|
50
|
+
yield* gh.updatePR({ branch, base });
|
|
51
|
+
} else {
|
|
52
|
+
yield* Console.log(`PR #${existingPR.number} already exists: ${existingPR.url}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
const pr = yield* gh.createPR({
|
|
56
|
+
head: branch,
|
|
57
|
+
base,
|
|
58
|
+
title: branch,
|
|
59
|
+
draft,
|
|
60
|
+
});
|
|
61
|
+
yield* Console.log(`Created PR #${pr.number}: ${pr.url}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yield* Console.log("Done");
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { StackService } from "../services/Stack.js";
|
|
5
|
+
|
|
6
|
+
const trunkFlag = Flag.string("trunk").pipe(Flag.optional, Flag.withAlias("t"));
|
|
7
|
+
|
|
8
|
+
export const sync = Command.make("sync", { trunk: trunkFlag }).pipe(
|
|
9
|
+
Command.withDescription("Rebase entire stack on latest trunk"),
|
|
10
|
+
Command.withHandler(({ trunk: trunkOpt }) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const git = yield* GitService;
|
|
13
|
+
const stacks = yield* StackService;
|
|
14
|
+
|
|
15
|
+
const trunk = Option.isSome(trunkOpt) ? trunkOpt.value : yield* stacks.getTrunk();
|
|
16
|
+
const currentBranch = yield* git.currentBranch();
|
|
17
|
+
|
|
18
|
+
yield* Console.log(`Fetching ${trunk}...`);
|
|
19
|
+
yield* git.fetch();
|
|
20
|
+
|
|
21
|
+
const result = yield* stacks.currentStack();
|
|
22
|
+
if (result === null) {
|
|
23
|
+
yield* Console.error("Not on a stacked branch");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { branches } = result.stack;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < branches.length; i++) {
|
|
30
|
+
const branch = branches[i];
|
|
31
|
+
if (branch === undefined) continue;
|
|
32
|
+
const base = i === 0 ? `origin/${trunk}` : (branches[i - 1] ?? `origin/${trunk}`);
|
|
33
|
+
yield* Console.log(`Rebasing ${branch} onto ${base}...`);
|
|
34
|
+
yield* git.checkout(branch);
|
|
35
|
+
yield* git.rebase(base);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
yield* git.checkout(currentBranch);
|
|
39
|
+
yield* Console.log("Stack synced");
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { StackService } from "../services/Stack.js";
|
|
5
|
+
|
|
6
|
+
export const top = Command.make("top").pipe(
|
|
7
|
+
Command.withDescription("Checkout top branch of stack"),
|
|
8
|
+
Command.withHandler(() =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const git = yield* GitService;
|
|
11
|
+
const stacks = yield* StackService;
|
|
12
|
+
|
|
13
|
+
const result = yield* stacks.currentStack();
|
|
14
|
+
if (result === null) {
|
|
15
|
+
yield* Console.error("Not on a stacked branch");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const topBranch = result.stack.branches[result.stack.branches.length - 1];
|
|
20
|
+
if (topBranch === undefined) {
|
|
21
|
+
yield* Console.error("Stack is empty");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
yield* git.checkout(topBranch);
|
|
26
|
+
yield* Console.log(`Switched to ${topBranch}`);
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Argument, Command } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
import { StackService } from "../services/Stack.js";
|
|
4
|
+
|
|
5
|
+
const nameArg = Argument.string("name").pipe(Argument.optional);
|
|
6
|
+
|
|
7
|
+
export const trunk = Command.make("trunk", { name: nameArg }).pipe(
|
|
8
|
+
Command.withDescription("Get or set the trunk branch"),
|
|
9
|
+
Command.withHandler(({ name }) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const stacks = yield* StackService;
|
|
12
|
+
if (Option.isSome(name)) {
|
|
13
|
+
yield* stacks.setTrunk(name.value);
|
|
14
|
+
yield* Console.log(`Trunk set to ${name.value}`);
|
|
15
|
+
} else {
|
|
16
|
+
const current = yield* stacks.getTrunk();
|
|
17
|
+
yield* Console.log(current);
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class GitError extends Schema.TaggedErrorClass<GitError>()("GitError", {
|
|
4
|
+
message: Schema.String,
|
|
5
|
+
command: Schema.optional(Schema.String),
|
|
6
|
+
}) {}
|
|
7
|
+
|
|
8
|
+
export class StackError extends Schema.TaggedErrorClass<StackError>()("StackError", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export class GitHubError extends Schema.TaggedErrorClass<GitHubError>()("GitHubError", {
|
|
13
|
+
message: Schema.String,
|
|
14
|
+
command: Schema.optional(Schema.String),
|
|
15
|
+
}) {}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "effect/unstable/cli";
|
|
3
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { command } from "./commands/index.js";
|
|
6
|
+
import { GitService } from "./services/Git.js";
|
|
7
|
+
import { StackService } from "./services/Stack.js";
|
|
8
|
+
import { GitHubService } from "./services/GitHub.js";
|
|
9
|
+
|
|
10
|
+
const cli = Command.run(command, {
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ServiceLayer = StackService.layer.pipe(
|
|
15
|
+
Layer.provideMerge(GitService.layer),
|
|
16
|
+
Layer.provideMerge(GitHubService.layer),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const AppLayer = Layer.mergeAll(ServiceLayer, BunServices.layer);
|
|
20
|
+
|
|
21
|
+
// @effect-diagnostics-next-line effect/strictEffectProvide:off
|
|
22
|
+
BunRuntime.runMain(cli.pipe(Effect.provide(AppLayer)));
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect, Layer, ServiceMap } from "effect";
|
|
2
|
+
import { GitError } from "../errors/index.js";
|
|
3
|
+
|
|
4
|
+
export class GitService extends ServiceMap.Service<
|
|
5
|
+
GitService,
|
|
6
|
+
{
|
|
7
|
+
readonly currentBranch: () => Effect.Effect<string, GitError>;
|
|
8
|
+
readonly branchExists: (name: string) => Effect.Effect<boolean, GitError>;
|
|
9
|
+
readonly createBranch: (name: string, from?: string) => Effect.Effect<void, GitError>;
|
|
10
|
+
readonly deleteBranch: (name: string, force?: boolean) => Effect.Effect<void, GitError>;
|
|
11
|
+
readonly checkout: (name: string) => Effect.Effect<void, GitError>;
|
|
12
|
+
readonly rebase: (onto: string) => Effect.Effect<void, GitError>;
|
|
13
|
+
readonly push: (branch: string, options?: { force?: boolean }) => Effect.Effect<void, GitError>;
|
|
14
|
+
readonly log: (
|
|
15
|
+
branch: string,
|
|
16
|
+
options?: { limit?: number; oneline?: boolean },
|
|
17
|
+
) => Effect.Effect<string, GitError>;
|
|
18
|
+
readonly mergeBase: (a: string, b: string) => Effect.Effect<string, GitError>;
|
|
19
|
+
readonly isClean: () => Effect.Effect<boolean, GitError>;
|
|
20
|
+
readonly revParse: (ref: string) => Effect.Effect<string, GitError>;
|
|
21
|
+
readonly diff: (
|
|
22
|
+
a: string,
|
|
23
|
+
b: string,
|
|
24
|
+
options?: { stat?: boolean },
|
|
25
|
+
) => Effect.Effect<string, GitError>;
|
|
26
|
+
readonly isAncestor: (ancestor: string, descendant: string) => Effect.Effect<boolean, GitError>;
|
|
27
|
+
readonly remote: () => Effect.Effect<string, GitError>;
|
|
28
|
+
readonly fetch: (remote?: string) => Effect.Effect<void, GitError>;
|
|
29
|
+
}
|
|
30
|
+
>()("@cvr/stacked/services/Git/GitService") {
|
|
31
|
+
static layer: Layer.Layer<GitService> = Layer.sync(GitService, () => {
|
|
32
|
+
const run = Effect.fn("git.run")(function* (args: readonly string[]) {
|
|
33
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
});
|
|
37
|
+
const exitCode = yield* Effect.promise(() => proc.exited);
|
|
38
|
+
const stdout = yield* Effect.promise(() => new Response(proc.stdout).text());
|
|
39
|
+
const stderr = yield* Effect.promise(() => new Response(proc.stderr).text());
|
|
40
|
+
|
|
41
|
+
if (exitCode !== 0) {
|
|
42
|
+
return yield* new GitError({
|
|
43
|
+
message: stderr.trim() || `git ${args[0]} failed with exit code ${exitCode}`,
|
|
44
|
+
command: `git ${args.join(" ")}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return stdout.trim();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
currentBranch: () => run(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
52
|
+
|
|
53
|
+
branchExists: (name) =>
|
|
54
|
+
run(["rev-parse", "--verify", name]).pipe(
|
|
55
|
+
Effect.as(true),
|
|
56
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
57
|
+
),
|
|
58
|
+
|
|
59
|
+
createBranch: (name, from) => {
|
|
60
|
+
const args = from !== undefined ? ["checkout", "-b", name, from] : ["checkout", "-b", name];
|
|
61
|
+
return run(args).pipe(Effect.asVoid);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
deleteBranch: (name, force) =>
|
|
65
|
+
run(["branch", force === true ? "-D" : "-d", name]).pipe(Effect.asVoid),
|
|
66
|
+
|
|
67
|
+
checkout: (name) => run(["checkout", name]).pipe(Effect.asVoid),
|
|
68
|
+
|
|
69
|
+
rebase: (onto) => run(["rebase", onto]).pipe(Effect.asVoid),
|
|
70
|
+
|
|
71
|
+
push: (branch, options) => {
|
|
72
|
+
const args = ["push", "-u", "origin", branch];
|
|
73
|
+
if (options?.force === true) args.splice(1, 0, "--force-with-lease");
|
|
74
|
+
return run(args).pipe(Effect.asVoid);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
log: (branch, options) => {
|
|
78
|
+
const args = ["log", branch];
|
|
79
|
+
if (options?.oneline === true) args.push("--oneline");
|
|
80
|
+
if (options?.limit !== undefined) args.push("-n", `${options.limit}`);
|
|
81
|
+
return run(args);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
mergeBase: (a, b) => run(["merge-base", a, b]),
|
|
85
|
+
|
|
86
|
+
isClean: () => run(["status", "--porcelain"]).pipe(Effect.map((r) => r === "")),
|
|
87
|
+
|
|
88
|
+
revParse: (ref) => run(["rev-parse", ref]),
|
|
89
|
+
|
|
90
|
+
diff: (a, b, options) => {
|
|
91
|
+
const args = ["diff", a, b];
|
|
92
|
+
if (options?.stat === true) args.push("--stat");
|
|
93
|
+
return run(args);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
isAncestor: (ancestor, descendant) =>
|
|
97
|
+
run(["merge-base", "--is-ancestor", ancestor, descendant]).pipe(
|
|
98
|
+
Effect.as(true),
|
|
99
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
100
|
+
),
|
|
101
|
+
|
|
102
|
+
remote: () => run(["remote"]),
|
|
103
|
+
|
|
104
|
+
fetch: (remote) => run(["fetch", remote ?? "origin"]).pipe(Effect.asVoid),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
static layerTest = (impl: Partial<ServiceMap.Service.Shape<typeof GitService>> = {}) =>
|
|
109
|
+
Layer.succeed(GitService, {
|
|
110
|
+
currentBranch: () => Effect.succeed("main"),
|
|
111
|
+
branchExists: () => Effect.succeed(false),
|
|
112
|
+
createBranch: () => Effect.void,
|
|
113
|
+
deleteBranch: () => Effect.void,
|
|
114
|
+
checkout: () => Effect.void,
|
|
115
|
+
rebase: () => Effect.void,
|
|
116
|
+
push: () => Effect.void,
|
|
117
|
+
log: () => Effect.succeed(""),
|
|
118
|
+
mergeBase: () => Effect.succeed("abc123"),
|
|
119
|
+
isClean: () => Effect.succeed(true),
|
|
120
|
+
revParse: () => Effect.succeed("abc123"),
|
|
121
|
+
diff: () => Effect.succeed(""),
|
|
122
|
+
isAncestor: () => Effect.succeed(true),
|
|
123
|
+
remote: () => Effect.succeed("origin"),
|
|
124
|
+
fetch: () => Effect.void,
|
|
125
|
+
...impl,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Effect, Layer, Schema, ServiceMap } from "effect";
|
|
2
|
+
import { GitHubError } from "../errors/index.js";
|
|
3
|
+
|
|
4
|
+
const GhPrResponse = Schema.Struct({
|
|
5
|
+
number: Schema.Number,
|
|
6
|
+
url: Schema.String,
|
|
7
|
+
state: Schema.String,
|
|
8
|
+
baseRefName: Schema.String,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export class GitHubService extends ServiceMap.Service<
|
|
12
|
+
GitHubService,
|
|
13
|
+
{
|
|
14
|
+
readonly createPR: (options: {
|
|
15
|
+
head: string;
|
|
16
|
+
base: string;
|
|
17
|
+
title: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
draft?: boolean;
|
|
20
|
+
}) => Effect.Effect<{ url: string; number: number }, GitHubError>;
|
|
21
|
+
readonly updatePR: (options: {
|
|
22
|
+
branch: string;
|
|
23
|
+
base?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
body?: string;
|
|
26
|
+
}) => Effect.Effect<void, GitHubError>;
|
|
27
|
+
readonly getPR: (
|
|
28
|
+
branch: string,
|
|
29
|
+
) => Effect.Effect<
|
|
30
|
+
{ number: number; url: string; state: string; base: string } | null,
|
|
31
|
+
GitHubError
|
|
32
|
+
>;
|
|
33
|
+
readonly isGhInstalled: () => Effect.Effect<boolean>;
|
|
34
|
+
}
|
|
35
|
+
>()("@cvr/stacked/services/GitHub/GitHubService") {
|
|
36
|
+
static layer: Layer.Layer<GitHubService> = Layer.sync(GitHubService, () => {
|
|
37
|
+
const run = Effect.fn("gh.run")(function* (args: readonly string[]) {
|
|
38
|
+
const proc = Bun.spawn(["gh", ...args], {
|
|
39
|
+
stdout: "pipe",
|
|
40
|
+
stderr: "pipe",
|
|
41
|
+
});
|
|
42
|
+
const exitCode = yield* Effect.promise(() => proc.exited);
|
|
43
|
+
const stdout = yield* Effect.promise(() => new Response(proc.stdout).text());
|
|
44
|
+
const stderr = yield* Effect.promise(() => new Response(proc.stderr).text());
|
|
45
|
+
|
|
46
|
+
if (exitCode !== 0) {
|
|
47
|
+
return yield* new GitHubError({
|
|
48
|
+
message: stderr.trim() || `gh ${args[0]} failed with exit code ${exitCode}`,
|
|
49
|
+
command: `gh ${args.join(" ")}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return stdout.trim();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
createPR: Effect.fn("GitHubService.createPR")(function* (options) {
|
|
57
|
+
const args = [
|
|
58
|
+
"pr",
|
|
59
|
+
"create",
|
|
60
|
+
"--head",
|
|
61
|
+
options.head,
|
|
62
|
+
"--base",
|
|
63
|
+
options.base,
|
|
64
|
+
"--title",
|
|
65
|
+
options.title,
|
|
66
|
+
];
|
|
67
|
+
if (options.body !== undefined) args.push("--body", options.body);
|
|
68
|
+
if (options.draft === true) args.push("--draft");
|
|
69
|
+
const output = yield* run(args);
|
|
70
|
+
const url = output.trim();
|
|
71
|
+
const match = url.match(/\/(\d+)$/);
|
|
72
|
+
const number = match?.[1] !== undefined ? parseInt(match[1], 10) : 0;
|
|
73
|
+
return { url, number };
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
updatePR: Effect.fn("GitHubService.updatePR")(function* (options) {
|
|
77
|
+
const args = ["pr", "edit", options.branch];
|
|
78
|
+
if (options.base !== undefined) args.push("--base", options.base);
|
|
79
|
+
if (options.title !== undefined) args.push("--title", options.title);
|
|
80
|
+
if (options.body !== undefined) args.push("--body", options.body);
|
|
81
|
+
yield* run(args);
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
getPR: Effect.fn("GitHubService.getPR")(function* (branch) {
|
|
85
|
+
const result = yield* run([
|
|
86
|
+
"pr",
|
|
87
|
+
"view",
|
|
88
|
+
branch,
|
|
89
|
+
"--json",
|
|
90
|
+
"number,url,state,baseRefName",
|
|
91
|
+
]).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
92
|
+
|
|
93
|
+
if (result === null) return null;
|
|
94
|
+
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GhPrResponse))(
|
|
95
|
+
result,
|
|
96
|
+
).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
97
|
+
if (data === null) return null;
|
|
98
|
+
return {
|
|
99
|
+
number: data.number,
|
|
100
|
+
url: data.url,
|
|
101
|
+
state: data.state,
|
|
102
|
+
base: data.baseRefName,
|
|
103
|
+
};
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
isGhInstalled: () =>
|
|
107
|
+
Effect.sync(() => {
|
|
108
|
+
try {
|
|
109
|
+
Bun.spawnSync(["gh", "--version"]);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
static layerTest = (impl: Partial<ServiceMap.Service.Shape<typeof GitHubService>> = {}) =>
|
|
119
|
+
Layer.succeed(GitHubService, {
|
|
120
|
+
createPR: () => Effect.succeed({ url: "https://github.com/test/repo/pull/1", number: 1 }),
|
|
121
|
+
updatePR: () => Effect.void,
|
|
122
|
+
getPR: () => Effect.succeed(null),
|
|
123
|
+
isGhInstalled: () => Effect.succeed(true),
|
|
124
|
+
...impl,
|
|
125
|
+
});
|
|
126
|
+
}
|