@oisincoveney/pipeline 2.8.0 → 2.8.2

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 (38) hide show
  1. package/dist/argo-submit.d.ts +2 -4
  2. package/dist/argo-submit.js +80 -80
  3. package/dist/cluster-doctor.js +89 -101
  4. package/dist/config/defaults.js +9 -19
  5. package/dist/config/load.js +32 -39
  6. package/dist/config/schemas.d.ts +1 -1
  7. package/dist/gates.js +6 -225
  8. package/dist/mcp/gateway-error.js +15 -0
  9. package/dist/mcp/gateway.js +119 -220
  10. package/dist/moka-global-config.js +20 -20
  11. package/dist/moka-submit.d.ts +1 -1
  12. package/dist/pipeline-runtime.js +580 -371
  13. package/dist/run-state/git-refs.js +124 -94
  14. package/dist/runner-command-contract.d.ts +2 -2
  15. package/dist/runner-event-sink.js +37 -69
  16. package/dist/runtime/agent-node/agent-node.js +214 -173
  17. package/dist/runtime/builtins/builtins.js +344 -57
  18. package/dist/runtime/changed-files/changed-files.js +15 -27
  19. package/dist/runtime/changed-files/index.js +2 -0
  20. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  21. package/dist/runtime/gates/gates.js +46 -28
  22. package/dist/runtime/hooks/hooks.js +74 -29
  23. package/dist/runtime/opencode-server.js +27 -21
  24. package/dist/runtime/opencode-session-executor.js +101 -44
  25. package/dist/runtime/parallel-node/parallel-node.js +24 -5
  26. package/dist/runtime/select-candidate/select-candidate.js +45 -29
  27. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  28. package/dist/runtime/services/command-executor-service.js +8 -0
  29. package/dist/runtime/services/config-io-service.js +42 -0
  30. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  31. package/dist/runtime/services/git-porcelain-service.js +38 -0
  32. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  33. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  34. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  35. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  36. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  37. package/dist/runtime/services/select-candidate-service.js +13 -0
  38. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
+ import { OpencodeSdkService, OpencodeSdkServiceLive } from "./services/opencode-sdk-service.js";
1
2
  import { opencodeAgentName } from "./opencode-agent-name.js";
3
+ import { Effect } from "effect";
2
4
  //#region src/runtime/opencode-session-executor.ts
3
5
  function createOpencodeSessionRegistry() {
4
6
  return { sessions: /* @__PURE__ */ new Map() };
@@ -18,45 +20,69 @@ const EXIT_INFRA = 70;
18
20
  */
19
21
  function createOpencodeExecutor(deps) {
20
22
  return async function execute(plan, options = {}) {
21
- if (plan.type !== "opencode") throw new Error(`opencode executor cannot drive runner type '${plan.type}'`);
22
- try {
23
- return successResult(plan, await driveSession(deps, plan, options));
24
- } catch (error) {
25
- return failureResult(plan, error);
26
- }
23
+ return await Effect.runPromise(Effect.provide(executeOpencodeEffect(deps, plan, options), OpencodeSdkServiceLive));
27
24
  };
28
25
  }
26
+ function executeOpencodeEffect(deps, plan, options) {
27
+ return Effect.gen(function* () {
28
+ yield* validateOpencodePlan(plan);
29
+ return yield* executeOpencodeSession(deps, plan, options);
30
+ });
31
+ }
32
+ function executeOpencodeSession(deps, plan, options) {
33
+ return Effect.gen(function* () {
34
+ return successResult(plan, yield* driveSession(deps, plan, options));
35
+ }).pipe(Effect.catchAll((error) => Effect.succeed(failureResult(plan, error))));
36
+ }
37
+ function validateOpencodePlan(plan) {
38
+ if (plan.type === "opencode") return Effect.void;
39
+ return Effect.fail(/* @__PURE__ */ new Error(`opencode executor cannot drive runner type '${plan.type}'`));
40
+ }
29
41
  function sessionDirectory(deps, plan) {
30
42
  return plan.cwd ?? deps.directory;
31
43
  }
32
- async function driveSession(deps, plan, options) {
33
- const sessionId = await resolveSessionId(deps, plan);
34
- deps.onSession?.(plan.nodeId, sessionId);
35
- const stream = await streamEventsToOutput(deps, sessionId, plan, options);
36
- try {
37
- const data = unwrap(await deps.client.session.prompt({
38
- body: promptBody(plan),
39
- path: { id: sessionId },
40
- query: { directory: sessionDirectory(deps, plan) }
41
- }));
44
+ function driveSession(deps, plan, options) {
45
+ return Effect.gen(function* () {
46
+ const sessionId = yield* resolveSessionId(deps, plan);
47
+ recordSession(deps, plan.nodeId, sessionId);
48
+ const stream = yield* streamEventsToOutput(deps, sessionId, plan, options);
49
+ return yield* promptSessionResult(deps, plan, sessionId).pipe(Effect.ensuring(stopStream(stream)));
50
+ });
51
+ }
52
+ function promptSessionResult(deps, plan, sessionId) {
53
+ return Effect.gen(function* () {
54
+ const data = unwrap(yield* (yield* OpencodeSdkService).promptSession(deps.client, promptRequest(deps, plan, sessionId)));
42
55
  return {
43
56
  ...data.info ? { assistant: data.info } : {},
44
57
  parts: data.parts ?? [],
45
58
  sessionId
46
59
  };
47
- } finally {
48
- await stream.stop();
49
- }
60
+ });
61
+ }
62
+ function promptRequest(deps, plan, sessionId) {
63
+ return {
64
+ body: promptBody(plan),
65
+ path: { id: sessionId },
66
+ query: { directory: sessionDirectory(deps, plan) }
67
+ };
68
+ }
69
+ function stopStream(stream) {
70
+ return Effect.tryPromise(() => stream.stop()).pipe(Effect.asVoid, Effect.catchAll(() => Effect.void));
50
71
  }
51
- async function resolveSessionId(deps, plan) {
72
+ function recordSession(deps, nodeId, sessionId) {
73
+ deps.onSession?.(nodeId, sessionId);
74
+ }
75
+ function resolveSessionId(deps, plan) {
52
76
  const existing = deps.registry.sessions.get(plan.nodeId);
53
- if (existing) return existing;
54
- const session = unwrap(await deps.client.session.create({
55
- body: { title: `moka:${plan.nodeId}` },
56
- query: { directory: plan.cwd ?? deps.directory }
57
- }));
58
- deps.registry.sessions.set(plan.nodeId, session.id);
59
- return session.id;
77
+ if (existing) return Effect.succeed(existing);
78
+ return Effect.gen(function* () {
79
+ const session = unwrap(yield* (yield* OpencodeSdkService).createSession(deps.client, {
80
+ body: { title: `moka:${plan.nodeId}` },
81
+ query: { directory: plan.cwd ?? deps.directory }
82
+ }));
83
+ deps.registry.sessions.set(plan.nodeId, session.id);
84
+ return session.id;
85
+ });
60
86
  }
61
87
  function promptBody(plan) {
62
88
  const prompt = promptText(plan);
@@ -95,24 +121,55 @@ function parseModel(model) {
95
121
  providerID: model.slice(0, slash)
96
122
  };
97
123
  }
98
- async function streamEventsToOutput(deps, sessionId, plan, options) {
99
- if (!options.onOutput) return { stop: () => Promise.resolve() };
100
- const iterator = (await deps.client.event.subscribe()).stream;
101
- const pump = (async () => {
102
- try {
103
- for await (const event of iterator) forwardEvent(event, sessionId, plan, options);
104
- } catch (error) {
105
- options.onOutput?.({
106
- chunk: `opencode event stream dropped: ${errorMessage(error)}\n`,
107
- nodeId: plan.nodeId,
108
- stream: "stderr"
109
- });
124
+ function streamEventsToOutput(deps, sessionId, plan, options) {
125
+ if (!options.onOutput) return Effect.succeed({ stop: () => Promise.resolve() });
126
+ return Effect.gen(function* () {
127
+ const iterator = (yield* (yield* OpencodeSdkService).subscribeEvents(deps.client)).stream;
128
+ const pump = Effect.runPromise(pumpEvents(iterator, sessionId, plan, options));
129
+ return { stop: () => stopIterator(iterator, pump) };
130
+ });
131
+ }
132
+ function pumpEvents(iterator, sessionId, plan, options) {
133
+ return Effect.gen(function* () {
134
+ let done = false;
135
+ while (!done) {
136
+ const next = yield* readNextEvent(iterator);
137
+ done = next.done === true;
138
+ if (!done) forwardEvent(next.value, sessionId, plan, options);
110
139
  }
111
- })();
112
- return { stop: async () => {
113
- await iterator.return?.(void 0);
114
- await pump;
115
- } };
140
+ }).pipe(Effect.catchAll((error) => reportStreamDrop(error, plan, options)));
141
+ }
142
+ function readNextEvent(iterator) {
143
+ return Effect.tryPromise({
144
+ catch: (error) => error,
145
+ try: () => iterator.next()
146
+ });
147
+ }
148
+ function reportStreamDrop(error, plan, options) {
149
+ return Effect.sync(() => {
150
+ options.onOutput?.({
151
+ chunk: `opencode event stream dropped: ${errorMessage(error)}\n`,
152
+ nodeId: plan.nodeId,
153
+ stream: "stderr"
154
+ });
155
+ });
156
+ }
157
+ function stopIterator(iterator, pump) {
158
+ return Effect.runPromise(stopIteratorEffect(iterator, pump));
159
+ }
160
+ function stopIteratorEffect(iterator, pump) {
161
+ return Effect.gen(function* () {
162
+ yield* requestIteratorReturn(iterator);
163
+ yield* Effect.tryPromise(() => pump).pipe(Effect.catchAll(() => Effect.void));
164
+ });
165
+ }
166
+ function requestIteratorReturn(iterator) {
167
+ const returnIterator = iterator.return;
168
+ if (!returnIterator) return Effect.void;
169
+ return Effect.tryPromise({
170
+ catch: (error) => error,
171
+ try: () => returnIterator.call(iterator, void 0)
172
+ }).pipe(Effect.asVoid, Effect.catchAll(() => Effect.void));
116
173
  }
117
174
  function forwardEvent(event, sessionId, plan, options) {
118
175
  const forwarded = eventChunk(event, sessionId);
@@ -1,5 +1,6 @@
1
1
  import { childReporter } from "../events/events.js";
2
2
  import "../events/index.js";
3
+ import { configUsesOpencode, leaseOpencodeRuntime } from "../opencode-runtime.js";
3
4
  import { WorktreeService, WorktreeServiceLive } from "../services/worktree-service.js";
4
5
  import { Effect } from "effect";
5
6
  //#region src/runtime/parallel-node/parallel-node.ts
@@ -76,15 +77,33 @@ function gcStaleWorktrees(context) {
76
77
  * interruption); release retains dirty/unpushed work for downstream selection.
77
78
  */
78
79
  function runChildInWorktree(child, context, runtime) {
79
- if (!context.config.parallel_worktrees?.enabled) return Effect.promise(() => runtime.executeNode(child, context));
80
+ if (!context.config.parallel_worktrees?.enabled) return executeChild(child, context, runtime);
80
81
  return Effect.gen(function* () {
81
82
  const worktree = yield* WorktreeService;
82
- return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => Effect.promise(() => runtime.executeNode(child, {
83
- ...context,
84
- worktreePath: lease.path
85
- })), (lease) => Effect.sync(() => lease.release()));
83
+ return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => runChildWithWorktreeLease(child, context, runtime, lease.path), (lease) => Effect.sync(() => lease.release()));
86
84
  });
87
85
  }
86
+ function executeChild(child, context, runtime) {
87
+ return Effect.tryPromise(() => runtime.executeNode(child, context)).pipe(Effect.orDie);
88
+ }
89
+ function runChildWithWorktreeLease(child, context, runtime, worktreePath) {
90
+ const childContext = {
91
+ ...context,
92
+ worktreePath
93
+ };
94
+ if (!configUsesOpencode(context.config)) return executeChild(child, childContext, runtime);
95
+ return Effect.acquireUseRelease(leaseChildOpencodeRuntime(context, worktreePath), (childRuntime) => executeChild(child, {
96
+ ...childContext,
97
+ executor: childRuntime.executor
98
+ }, runtime), (childRuntime) => Effect.tryPromise(() => childRuntime.release()).pipe(Effect.orDie));
99
+ }
100
+ function leaseChildOpencodeRuntime(context, worktreePath) {
101
+ return Effect.tryPromise(() => leaseOpencodeRuntime({
102
+ config: context.config,
103
+ ...context.signal ? { signal: context.signal } : {},
104
+ worktreePath
105
+ })).pipe(Effect.orDie);
106
+ }
88
107
  function childLeaseOptions(child, context) {
89
108
  return {
90
109
  childNodeId: child.id,
@@ -2,7 +2,8 @@ import { createRunnerLaunchPlan } from "../../runner.js";
2
2
  import { normalizeRunnerOutput } from "../../runner-output.js";
3
3
  import { parseJsonObject } from "../json-validation/json-validation.js";
4
4
  import "../json-validation/index.js";
5
- import { promoteWorktreeChanges } from "../parallel-worktrees/parallel-worktrees.js";
5
+ import { SelectCandidateService, SelectCandidateServiceLive } from "../services/select-candidate-service.js";
6
+ import { Effect } from "effect";
6
7
  //#region src/runtime/select-candidate/select-candidate.ts
7
8
  const SCORE_RE = /-?\d+(?:\.\d+)?/;
8
9
  function selectBestCandidate(candidates) {
@@ -11,19 +12,25 @@ function selectBestCandidate(candidates) {
11
12
  return passing.reduce((best, candidate) => (candidate.judgeScore ?? 0) > (best.judgeScore ?? 0) ? candidate : best);
12
13
  }
13
14
  async function executeSelectCandidateBuiltin(context, node) {
14
- const candidates = await scoreCandidates(context, readCandidates(context, node?.needs.at(0) ?? null));
15
- const selected = selectBestCandidate(candidates);
16
- if (!selected) return {
17
- evidence: [`select-candidate: no passing candidate among ${candidates.length}`, ...candidates.map((candidate) => `- ${candidate.nodeId}: FAIL`)],
18
- exitCode: 1,
19
- output: ""
20
- };
21
- const promoted = promoteWinner(context, node, selected.nodeId);
22
- return {
23
- evidence: selectionEvidence(selected, candidates.length, promoted),
24
- exitCode: 0,
25
- output: selected.output
26
- };
15
+ const program = executeSelectCandidateBuiltinProgram(context, node);
16
+ return await Effect.runPromise(Effect.provide(program, SelectCandidateServiceLive));
17
+ }
18
+ function executeSelectCandidateBuiltinProgram(context, node) {
19
+ return Effect.gen(function* () {
20
+ const candidates = yield* scoreCandidates(context, readCandidates(context, firstNeed(node)));
21
+ const selected = selectBestCandidate(candidates);
22
+ if (!selected) return {
23
+ evidence: [`select-candidate: no passing candidate among ${candidates.length}`, ...candidates.map((candidate) => `- ${candidate.nodeId}: FAIL`)],
24
+ exitCode: 1,
25
+ output: ""
26
+ };
27
+ const promoted = yield* promoteWinner(context, node, selected.nodeId);
28
+ return {
29
+ evidence: selectionEvidence(selected, candidates.length, promoted),
30
+ exitCode: 0,
31
+ output: selected.output
32
+ };
33
+ });
27
34
  }
28
35
  function selectionEvidence(selected, candidateCount, promoted) {
29
36
  const lines = [`select-candidate: selected '${selected.nodeId}' (judge=${selected.judgeScore ?? "n/a"}) from ${candidateCount} candidates`];
@@ -31,24 +38,33 @@ function selectionEvidence(selected, candidateCount, promoted) {
31
38
  return lines;
32
39
  }
33
40
  function promoteWinner(context, node, winnerNodeId) {
34
- const parentNodeId = node?.needs.at(0);
35
- if (!(context.config.parallel_worktrees?.enabled && parentNodeId)) return [];
36
- return promoteWorktreeChanges(context.worktreePath, context.runId, parentNodeId, winnerNodeId);
41
+ const parentNodeId = firstNeed(node);
42
+ if (!shouldPromoteWinner(context, parentNodeId)) return Effect.succeed([]);
43
+ return SelectCandidateService.pipe(Effect.flatMap((service) => service.promoteWinner(context.worktreePath, context.runId, parentNodeId, winnerNodeId)));
37
44
  }
38
- async function scoreCandidates(context, candidates) {
45
+ function shouldPromoteWinner(context, parentNodeId) {
46
+ const parallelWorktrees = context.config.parallel_worktrees;
47
+ return Boolean(parallelWorktrees?.enabled) && parentNodeId !== null;
48
+ }
49
+ function firstNeed(node) {
50
+ return node?.needs.at(0) ?? null;
51
+ }
52
+ function scoreCandidates(context, candidates) {
39
53
  const model = context.config.best_of_n?.judge_model;
40
54
  const runner = Object.keys(context.config.runners).at(0);
41
- if (!(model && runner)) return candidates;
42
- return await Promise.all(candidates.map((candidate) => scoreCandidate(context, candidate, runner, model)));
43
- }
44
- async function scoreCandidate(context, candidate, runner, model) {
45
- const plan = judgePlan(context, candidate, runner, model);
46
- context.agentInvocations.push(plan);
47
- const judgeScore = parseScore(normalizeRunnerOutput(plan, (await context.executor(plan, { signal: context.signal })).stdout).output);
48
- return judgeScore === null ? candidate : {
49
- ...candidate,
50
- judgeScore
51
- };
55
+ if (!(model && runner)) return Effect.succeed(candidates);
56
+ return Effect.forEach(candidates, (candidate) => scoreCandidate(context, candidate, runner, model), { concurrency: "unbounded" });
57
+ }
58
+ function scoreCandidate(context, candidate, runner, model) {
59
+ return Effect.gen(function* () {
60
+ const plan = judgePlan(context, candidate, runner, model);
61
+ context.agentInvocations.push(plan);
62
+ const judgeScore = parseScore(normalizeRunnerOutput(plan, (yield* (yield* SelectCandidateService).executeRunner(context.executor, plan, { signal: context.signal })).stdout).output);
63
+ return judgeScore === null ? candidate : {
64
+ ...candidate,
65
+ judgeScore
66
+ };
67
+ });
52
68
  }
53
69
  function judgePlan(context, candidate, runner, model) {
54
70
  const profileId = `select-candidate:judge:${candidate.nodeId}`;
@@ -0,0 +1,15 @@
1
+ import { buildRepoMapContext } from "../../context/repo-map.js";
2
+ import { Context, Effect, Layer } from "effect";
3
+ import { readFileSync } from "node:fs";
4
+ //#region src/runtime/services/agent-node-runtime-service.ts
5
+ var AgentNodeRuntimeService = class extends Context.Tag("AgentNodeRuntimeService")() {};
6
+ const AgentNodeRuntimeServiceLive = Layer.succeed(AgentNodeRuntimeService, {
7
+ buildRepoMap: buildRepoMapContext,
8
+ executeRunner: (executor, plan, options) => Effect.tryPromise({
9
+ catch: (error) => error,
10
+ try: () => Promise.resolve(executor(plan, options))
11
+ }),
12
+ readText: (path) => Effect.sync(() => readFileSync(path, "utf8"))
13
+ });
14
+ //#endregion
15
+ export { AgentNodeRuntimeService, AgentNodeRuntimeServiceLive };
@@ -0,0 +1,8 @@
1
+ import { executeCommand } from "../command-executor/command-executor.js";
2
+ import "../command-executor/index.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+ //#region src/runtime/services/command-executor-service.ts
5
+ var CommandExecutor = class extends Context.Tag("CommandExecutor")() {};
6
+ const CommandExecutorLive = Layer.succeed(CommandExecutor, { execute: (command, context, options) => Effect.tryPromise(() => executeCommand(command, context, options)) });
7
+ //#endregion
8
+ export { CommandExecutor, CommandExecutorLive };
@@ -0,0 +1,42 @@
1
+ import { PipelineConfigError, configIssuesFromZodError, validationError } from "../../config/schemas.js";
2
+ import { parseDocument } from "yaml";
3
+ import { Cause, Context, Effect, Layer, Option } from "effect";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ //#region src/runtime/services/config-io-service.ts
6
+ function parseYamlSource(source, sourcePath) {
7
+ const document = parseDocument(source, {
8
+ prettyErrors: false,
9
+ uniqueKeys: true
10
+ });
11
+ if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
12
+ message: err.message,
13
+ path: sourcePath
14
+ })));
15
+ return document.toJS();
16
+ }
17
+ var ConfigIoService = class extends Context.Tag("ConfigIoService")() {};
18
+ const ConfigIoServiceLive = Layer.succeed(ConfigIoService, {
19
+ parseYaml: (source, sourcePath) => Effect.try({
20
+ try: () => parseYamlSource(source, sourcePath),
21
+ catch: (error) => error
22
+ }),
23
+ readOptionalText: (path) => Effect.sync(() => existsSync(path) ? readFileSync(path, "utf8") : null),
24
+ readText: (path) => Effect.sync(() => readFileSync(path, "utf8"))
25
+ });
26
+ function parseConfigYamlAs(source, sourcePath, schema) {
27
+ return Effect.gen(function* () {
28
+ const yaml = yield* (yield* ConfigIoService).parseYaml(source, sourcePath);
29
+ const parsed = schema.safeParse(yaml);
30
+ if (!parsed.success) return yield* Effect.fail(validationError(configIssuesFromZodError(parsed.error)));
31
+ return parsed.data;
32
+ });
33
+ }
34
+ function runConfigIoSync(program) {
35
+ const exit = Effect.runSyncExit(Effect.provide(program, ConfigIoServiceLive));
36
+ if (exit._tag === "Success") return exit.value;
37
+ const failure = Cause.failureOption(exit.cause);
38
+ if (Option.isSome(failure)) throw failure.value;
39
+ throw Cause.squash(exit.cause);
40
+ }
41
+ //#endregion
42
+ export { ConfigIoService, parseConfigYamlAs, runConfigIoSync };
@@ -0,0 +1,10 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import simpleGit$1 from "simple-git";
3
+ //#region src/runtime/services/drain-merge-git-service.ts
4
+ var DrainMergeGitService = class extends Context.Tag("DrainMergeGitService")() {};
5
+ const DrainMergeGitServiceLive = Layer.succeed(DrainMergeGitService, { create: (baseDir) => Effect.sync(() => {
6
+ const git = simpleGit$1({ baseDir });
7
+ return { raw: (args) => Effect.tryPromise(() => git.raw(args)) };
8
+ }) });
9
+ //#endregion
10
+ export { DrainMergeGitService, DrainMergeGitServiceLive };
@@ -0,0 +1,38 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { execFile, execFileSync } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ //#region src/runtime/services/git-porcelain-service.ts
5
+ const execGit = promisify(execFile);
6
+ var GitPorcelainService = class extends Context.Tag("GitPorcelainService")() {};
7
+ const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
8
+ run: (cwd, args, env) => Effect.tryPromise({
9
+ try: async () => {
10
+ const { stdout } = await execGit("git", args, {
11
+ cwd,
12
+ encoding: "utf8",
13
+ env
14
+ });
15
+ return stdout;
16
+ },
17
+ catch: (error) => error
18
+ }),
19
+ statusPorcelain: (cwd) => Effect.try({
20
+ try: () => execFileSync("git", [
21
+ "status",
22
+ "--porcelain=v1",
23
+ "--untracked-files=all",
24
+ "-z"
25
+ ], {
26
+ cwd,
27
+ encoding: "utf8",
28
+ stdio: [
29
+ "ignore",
30
+ "pipe",
31
+ "ignore"
32
+ ]
33
+ }),
34
+ catch: (error) => error
35
+ })
36
+ });
37
+ //#endregion
38
+ export { GitPorcelainService, GitPorcelainServiceLive };
@@ -0,0 +1,13 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
3
+
4
+ //#region src/runtime/services/kubernetes-argo-service.d.ts
5
+ type CoreApi = Pick<CoreV1Api, "createNamespacedConfigMap">;
6
+ type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject">;
7
+ interface KubernetesArgoIoDependencies {
8
+ coreApi?: CoreApi;
9
+ kubeConfig?: KubeConfig;
10
+ workflowApi?: WorkflowApi;
11
+ }
12
+ //#endregion
13
+ export { CoreApi, KubernetesArgoIoDependencies, WorkflowApi };
@@ -0,0 +1,81 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { execa } from "execa";
3
+ import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
4
+ //#region src/runtime/services/kubernetes-argo-service.ts
5
+ var KubernetesArgoService = class extends Context.Tag("KubernetesArgoService")() {};
6
+ const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
7
+ createConfigMap: ({ body, dependencies, namespace, options }) => Effect.try({
8
+ try: () => apiClients(options, dependencies),
9
+ catch: (error) => error
10
+ }).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
11
+ try: () => coreApi.createNamespacedConfigMap({
12
+ body,
13
+ namespace
14
+ }),
15
+ catch: (error) => error
16
+ }))),
17
+ createWorkflow: ({ body, dependencies, namespace, options }) => Effect.try({
18
+ try: () => apiClients(options, dependencies),
19
+ catch: (error) => error
20
+ }).pipe(Effect.flatMap(({ workflowApi }) => Effect.tryPromise({
21
+ try: () => workflowApi.createNamespacedCustomObject({
22
+ body,
23
+ group: "argoproj.io",
24
+ namespace,
25
+ plural: "workflows",
26
+ version: "v1alpha1"
27
+ }),
28
+ catch: (error) => error
29
+ }))),
30
+ kubectl: (args, options) => Effect.tryPromise({
31
+ try: () => execa("kubectl", kubectlArgs(args, options.kubeContext), {
32
+ env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
33
+ stdin: "ignore"
34
+ }),
35
+ catch: (error) => error
36
+ }).pipe(Effect.map((result) => ({
37
+ ok: true,
38
+ stderr: result.stderr,
39
+ stdout: result.stdout
40
+ })), Effect.catchAll((error) => Effect.succeed({
41
+ ok: false,
42
+ stderr: kubectlErrorStderr(error),
43
+ stdout: kubectlErrorStdout(error)
44
+ })))
45
+ });
46
+ function resolveKubeConfig(options, dependencies) {
47
+ if (dependencies.kubeConfig) return dependencies.kubeConfig;
48
+ const kubeConfig = new KubeConfig();
49
+ if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
50
+ else kubeConfig.loadFromDefault();
51
+ return kubeConfig;
52
+ }
53
+ function apiClients(options, dependencies) {
54
+ if (dependencies.coreApi && dependencies.workflowApi) return {
55
+ coreApi: dependencies.coreApi,
56
+ workflowApi: dependencies.workflowApi
57
+ };
58
+ return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
59
+ }
60
+ function buildApiClients(kubeConfig, dependencies) {
61
+ return {
62
+ coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
63
+ workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
64
+ };
65
+ }
66
+ function kubectlArgs(args, kubeContext) {
67
+ return kubeContext ? [
68
+ "--context",
69
+ kubeContext,
70
+ ...args
71
+ ] : [...args];
72
+ }
73
+ function kubectlErrorStderr(error) {
74
+ const parsed = error;
75
+ return (parsed.stderr || parsed.shortMessage || "kubectl failed").trim();
76
+ }
77
+ function kubectlErrorStdout(error) {
78
+ return (error.stdout || "").trim();
79
+ }
80
+ //#endregion
81
+ export { KubernetesArgoService, KubernetesArgoServiceLive };