@oisincoveney/pipeline 3.20.0 → 3.21.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.
@@ -76,6 +76,7 @@ declare const submitDynamicRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
76
76
  }, z.core.$strict>;
77
77
  declare const commandScheduleOptionsSchema: z.ZodObject<{
78
78
  command: z.ZodArray<z.ZodString>;
79
+ deliverPullRequest: z.ZodDefault<z.ZodBoolean>;
79
80
  generatedAt: z.ZodDefault<z.ZodDate>;
80
81
  scheduleId: z.ZodOptional<z.ZodString>;
81
82
  task: z.ZodString;
@@ -1,6 +1,7 @@
1
1
  import { ArgoGraphCompilerError, compileArgoExecutionGraph } from "./argo-graph.js";
2
2
  import { dbAuthOptionSchema, mcpGatewayAuthOptionSchema } from "./remote/argo/model.js";
3
3
  import { brokerAuthOptionSchema } from "./credentials/broker.js";
4
+ import { appendPullRequestDelivery } from "./schedule/passes/open-pull-request.js";
4
5
  import { compileScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
5
6
  import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
6
7
  import { buildRunnerTaskDescriptor } from "./runner-command/task-descriptor.js";
@@ -62,6 +63,7 @@ const submitDynamicRunnerArgoWorkflowOptionsSchema = z.object({
62
63
  }).strict().refine(hasWorkflowName, { message: "Argo submit options must declare name or generateName" });
63
64
  const commandScheduleOptionsSchema = z.object({
64
65
  command: z.array(z.string().min(1)).min(1),
66
+ deliverPullRequest: z.boolean().default(false),
65
67
  generatedAt: z.date().default(() => /* @__PURE__ */ new Date()),
66
68
  scheduleId: scheduleIdSchema.optional(),
67
69
  task: z.string().min(1)
@@ -271,7 +273,7 @@ function workflowSubmitResult(response, workflow, base) {
271
273
  function buildCommandScheduleYaml(rawOptions) {
272
274
  const options = commandScheduleOptionsSchema.parse(rawOptions);
273
275
  const scheduleId = options.scheduleId ?? `custom-${randomBytes(8).toString("hex")}`;
274
- return stringify({
276
+ const artifact = {
275
277
  generated_at: options.generatedAt.toISOString(),
276
278
  kind: "pipeline-schedule",
277
279
  root_workflow: "root",
@@ -284,7 +286,8 @@ function buildCommandScheduleYaml(rawOptions) {
284
286
  id: "command",
285
287
  kind: "command"
286
288
  }] } }
287
- });
289
+ };
290
+ return stringify(appendPullRequestDelivery(options.deliverPullRequest, artifact));
288
291
  }
289
292
  function normalizeRunnerPayloadForSubmit(input) {
290
293
  const repository = normalizeRunnerRepositoryForSubmit(input.payload.repository);
@@ -10,7 +10,7 @@ function registerBootstrapCommands(program) {
10
10
  console.log(flags.json ? JSON.stringify(result) : formatDoctorResult(result));
11
11
  if (!result.passed) throw new Error("Doctor checks failed.");
12
12
  });
13
- program.command("init").description("Install or refresh package-owned pipeline support: per-machine harness (skills + slash-command adapters + agent hooks + global instruction files) installed globally to ~/.claude, ~/.config/opencode, ~/.codex with no repo-local config").option("--check", "verify the generated harness is current; fail if stale").option("--dry-run", "show planned changes without writing files").option("--force", "overwrite manually edited harness files").action(async (flags) => {
13
+ program.command("init").description("Install or refresh moka's slash-command adapters (/moka-execute, /moka-inspect, /moka-quick) plus the singleton MCP gateway host config, globally to ~/.claude, ~/.config/opencode, ~/.codex with no repo-local config. The agent harness (skills, hooks, instruction rules) is provisioned separately from oisin-ee/agent via chezmoi, not by moka.").option("--check", "verify the installed adapters are current; fail if stale").option("--dry-run", "show planned changes without writing files").option("--force", "overwrite manually edited command adapter files").action(async (flags) => {
14
14
  const result = await initPipelineProject({
15
15
  ...flags,
16
16
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd()
@@ -1,102 +1,21 @@
1
- import { claudeGlobalConfigDir, codexGlobalConfigDir, opencodeGlobalConfigDir } from "./install-commands/shared.js";
2
- import { AGENT_SKILL_SOURCE } from "./agent-assets.js";
3
1
  import { installCommands } from "./install-commands.js";
4
- import { installHooks } from "./install-hooks.js";
5
- import { installRules } from "./install-rules.js";
6
- import { execa } from "execa";
7
- import { homedir } from "node:os";
8
- import { join } from "node:path";
9
- import { rm } from "node:fs/promises";
10
2
  //#region src/pipeline-init.ts
11
- const DEFAULT_SKILL_INSTALL_SOURCE = AGENT_SKILL_SOURCE;
12
- const SKILL_INSTALL_AGENT_ARGS = [
13
- "--agent",
14
- "opencode",
15
- "--agent",
16
- "codex",
17
- "--agent",
18
- "claude-code",
19
- "--skill",
20
- "*",
21
- "--yes",
22
- "--global"
23
- ];
24
3
  /**
25
- * Every global location the `skills` CLI writes into for the three agents we
26
- * manage. The CLI copies each skill's real folder once into the shared master
27
- * store `~/.agents/skills` and points each agent's global skills dir at it via
28
- * symlinks, recording install state in `~/.agents/.skill-lock.json` (or
29
- * `$XDG_STATE_HOME/skills/.skill-lock.json`). Each entry below mirrors that
30
- * resolution from the skills CLI source (skills `dist/cli.mjs`): per-agent
31
- * config dirs honor `CLAUDE_CONFIG_DIR` / `CODEX_HOME` /
32
- * `OPENCODE_CONFIG_DIR`+`XDG_CONFIG_HOME` (reused from install-commands so the
33
- * test suite's env redirect isolates them), the master store and lock honor
34
- * the home dir and `XDG_STATE_HOME`.
4
+ * `moka init` installs only moka's own slash-command adapters
5
+ * (`/moka-execute|inspect|quick`) plus the singleton MCP gateway host config,
6
+ * globally for Claude Code, Codex, and OpenCode. The shared agent harness
7
+ * (skills, agent hooks, and global instruction rules) is no longer installed by
8
+ * moka — it is provisioned from `oisin-ee/agent` via chezmoi (the dotfiles'
9
+ * `.chezmoiexternal` clone + `run_onchange` harness installer). Keeping moka's
10
+ * command adapters here means the runner image (and local dev) still gets the
11
+ * `/moka-*` entrypoints after `chezmoi apply` lays down the harness.
35
12
  */
36
- function globalSkillCleanTargets() {
37
- const agentsHome = homedir();
38
- const skillLockPath = process.env.XDG_STATE_HOME ? join(process.env.XDG_STATE_HOME, "skills", ".skill-lock.json") : join(agentsHome, ".agents", ".skill-lock.json");
39
- return [
40
- join(claudeGlobalConfigDir(), "skills"),
41
- join(codexGlobalConfigDir(), "skills"),
42
- join(opencodeGlobalConfigDir(), "skills"),
43
- join(agentsHome, ".agents", "skills"),
44
- skillLockPath
45
- ];
46
- }
47
- /**
48
- * Clean-replace step run before the additive `skills add`. `npx skills add`
49
- * only ever adds, so without this a renamed, removed, or foreign global skill
50
- * accumulates forever across `moka init` runs. Removing the per-agent symlink
51
- * farms + shared master store + lock resets global skill state so the post-add
52
- * set equals exactly the canonical `oisin-ee/agent` source. Safe when absent
53
- * (rm force); only `skills` subdirs, the master store, and the lock are
54
- * touched — never a whole host config dir.
55
- */
56
- async function cleanGlobalSkills() {
57
- await Promise.all(globalSkillCleanTargets().map((target) => rm(target, {
58
- force: true,
59
- recursive: true
60
- })));
61
- }
62
- async function installDefaultSkills(cwd) {
63
- try {
64
- await cleanGlobalSkills();
65
- await execa("npx", [
66
- "--yes",
67
- "skills",
68
- "add",
69
- DEFAULT_SKILL_INSTALL_SOURCE,
70
- ...SKILL_INSTALL_AGENT_ARGS
71
- ], {
72
- cwd,
73
- stdio: "inherit"
74
- });
75
- } catch (error) {
76
- const cause = error instanceof Error ? `: ${error.message}` : "";
77
- throw new Error(`Failed to install default skills from ${DEFAULT_SKILL_INSTALL_SOURCE}${cause}. If this is a private repository, authenticate GitHub access for npx skills add and rerun \`moka init\`.`);
78
- }
79
- }
80
- function hookInstallerFiles(result) {
81
- return "items" in result ? result.items.map((item) => item.path) : result.files;
82
- }
83
13
  async function initPipelineProject(options = {}) {
84
- const cwd = options.cwd ?? process.cwd();
85
- const { check, dryRun } = options;
86
- const installerFlags = initInstallerFlags(options);
87
- if (!(check || dryRun)) await (options.skillInstaller ?? installDefaultSkills)(cwd);
88
- const result = await installCommands({
89
- cwd,
14
+ return { files: (await installCommands({
15
+ cwd: options.cwd ?? process.cwd(),
90
16
  host: "all",
91
- ...installerFlags
92
- });
93
- const hooks = await (options.hookInstaller ?? (() => installHooks(installerFlags)))(cwd);
94
- const rulesResult = await (options.rulesInstaller ?? (() => installRules(installerFlags)))(cwd);
95
- return { files: [
96
- ...result.items.map((item) => item.path),
97
- ...hookInstallerFiles(hooks),
98
- ...rulesResult.items.map((item) => item.path)
99
- ] };
17
+ ...initInstallerFlags(options)
18
+ })).items.map((item) => item.path) };
100
19
  }
101
20
  function initInstallerFlags(options) {
102
21
  const { check, dryRun } = options;
@@ -108,17 +27,17 @@ function initInstallerFlags(options) {
108
27
  }
109
28
  const INIT_RESULT_COPY = {
110
29
  install: {
111
- headline: "Initialized package-owned pipeline support:",
30
+ headline: "Initialized moka slash-command adapters:",
112
31
  fileVerb: "generated",
113
32
  footer: "no repo-local pipeline config files were created"
114
33
  },
115
34
  check: {
116
- headline: "Verified package-owned pipeline support is current:",
35
+ headline: "Verified moka slash-command adapters are current:",
117
36
  fileVerb: "current",
118
- footer: "harness verified; no changes written"
37
+ footer: "adapters verified; no changes written"
119
38
  },
120
39
  dryRun: {
121
- headline: "Planned package-owned pipeline support:",
40
+ headline: "Planned moka slash-command adapters:",
122
41
  fileVerb: "would generate",
123
42
  footer: "dry run; no changes written"
124
43
  }
@@ -132,7 +51,7 @@ function formatPipelineInitResult(result, mode = {}) {
132
51
  const copy = INIT_RESULT_COPY[initResultMode(mode)];
133
52
  return [
134
53
  copy.headline,
135
- "per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); global instruction files generated via rulesync from oisin-ee/agent/rules; inherited by every repo with no per-repo copy",
54
+ "per-machine slash-command adapters (/moka-execute|inspect|quick) installed globally (~/.claude, ~/.config/opencode, ~/.codex); the agent harness (skills, hooks, instruction rules) comes from oisin-ee/agent via chezmoi, not moka",
136
55
  ...result.files.map((path) => `${copy.fileVerb} ${path}`),
137
56
  copy.footer
138
57
  ].join("\n");
@@ -16,7 +16,7 @@ import { integrateParallelWriteFanout } from "../schedule/passes/drain-merge.js"
16
16
  import { canonicalizeGeneratedScheduleIds } from "../schedule/passes/ids.js";
17
17
  import { SCHEDULE_PASS_ORDER } from "../schedule/passes/index.js";
18
18
  import { applyNodeCatalogModelFallbacks } from "../schedule/passes/models.js";
19
- import { appendPullRequestDelivery } from "../schedule/passes/open-pull-request.js";
19
+ import { appendPullRequestDelivery, isPullRequestDeliveryEnabled } from "../schedule/passes/open-pull-request.js";
20
20
  import { namespaceScheduleWorkflows } from "../schedule/passes/references.js";
21
21
  import { plannerPrompt, plannerRepairPrompt } from "../schedule/prompts.js";
22
22
  import { parseDocument, stringify } from "yaml";
@@ -131,7 +131,7 @@ async function generateScheduleArtifactInMemory(options) {
131
131
  });
132
132
  const generatedArtifact = await planScheduleArtifact(baseline, policy.planner_profile, options, planningContext);
133
133
  assertSchedulePassOrder();
134
- const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog, appendPullRequestDelivery(options.config, integrateParallelWriteFanout(options.config, addGeneratedImplementationCoverage(options.config, generatedArtifact))))), planningContext);
134
+ const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog, appendPullRequestDelivery(isPullRequestDeliveryEnabled(options.config), integrateParallelWriteFanout(options.config, addGeneratedImplementationCoverage(options.config, generatedArtifact))))), planningContext);
135
135
  validateScheduleArtifact(options.config, artifact, planningContext);
136
136
  compileScheduleArtifact(options.config, artifact, options.worktreePath);
137
137
  return {
@@ -27,6 +27,7 @@ function compileMokaCommandSubmitPlan(options, runId) {
27
27
  const task = commandTask(options);
28
28
  const scheduleYaml = buildCommandScheduleYaml({
29
29
  command: options.commandArgv,
30
+ deliverPullRequest: options.delivery.pullRequest,
30
31
  scheduleId: runId,
31
32
  task: taskDescription(task)
32
33
  });
@@ -109,12 +109,17 @@ function pushHeadBranch(git, headBranch) {
109
109
  function submitPullRequest(prCtx, context) {
110
110
  if (prCtx.mode === "update-existing-pr") return handleExistingPr(prCtx.headBranch, prCtx.label, context);
111
111
  return Effect.gen(function* () {
112
- const createResult = yield* runGhPrCreate(yield* CommandExecutor, prCtx, extractPrTitle(prCtx.task), context);
113
- if (createResult.exitCode === 0) return openPrSuccess(extractPrUrl(createResult.output), "opened");
112
+ const executor = yield* CommandExecutor;
113
+ const createResult = yield* runGhPrCreate(executor, prCtx, extractPrTitle(prCtx.task), context);
114
+ if (createResult.exitCode === 0) return yield* labelCreatedPr(executor, prCtx, createResult, context);
114
115
  if (isPrAlreadyExistsError(createResult.output)) return yield* handleExistingPr(prCtx.headBranch, prCtx.label, context);
115
116
  return createResult;
116
117
  });
117
118
  }
119
+ function labelCreatedPr(executor, prCtx, createResult, context) {
120
+ const url = extractPrUrl(createResult.output);
121
+ return runGhPrEdit(executor, prCtx.headBranch, prCtx.label, context).pipe(Effect.map((editResult) => editResult.exitCode === 0 ? openPrSuccess(url, "opened") : openPrSuccess(url, "opened", [`open-pull-request: label '${prCtx.label}' not applied — ${editResult.output || `gh pr edit exited ${editResult.exitCode}`}`])));
122
+ }
118
123
  function runGhPrCreate(executor, prCtx, title, context) {
119
124
  return executor.execute(buildGhPrCreateArgs(prCtx, title), context).pipe(Effect.catch((e) => Effect.succeed(openPrFailure(errorMessage(e)))));
120
125
  }
@@ -143,9 +148,7 @@ function buildGhPrCreateArgs(prCtx, title) {
143
148
  "--title",
144
149
  title,
145
150
  "--body",
146
- `Opened by moka run ${prCtx.runId}`,
147
- "--label",
148
- prCtx.label
151
+ `Opened by moka run ${prCtx.runId}`
149
152
  ];
150
153
  }
151
154
  function buildGhPrEditArgs(headBranch, label) {
@@ -164,9 +167,9 @@ function isPrAlreadyExistsError(output) {
164
167
  function extractPrUrl(output) {
165
168
  return output.split(NEWLINE_RE).map((l) => l.trim()).find((l) => l.startsWith("https://")) ?? output.trim();
166
169
  }
167
- function openPrSuccess(url, action) {
170
+ function openPrSuccess(url, action, extraEvidence = []) {
168
171
  return {
169
- evidence: [`open-pull-request: PR ${action} — ${url}`],
172
+ evidence: [`open-pull-request: PR ${action} — ${url}`, ...extraEvidence],
170
173
  exitCode: 0,
171
174
  output: JSON.stringify({
172
175
  action,
@@ -25,8 +25,8 @@ function buildPrNode(terminalIds, usedIds) {
25
25
  };
26
26
  }
27
27
  /** Append a final open-pull-request node to the root workflow when enabled. */
28
- function appendPullRequestDelivery(config, artifact) {
29
- if (!isPullRequestDeliveryEnabled(config)) return artifact;
28
+ function appendPullRequestDelivery(enabled, artifact) {
29
+ if (!enabled) return artifact;
30
30
  const rootWorkflow = artifact.workflows[artifact.root_workflow];
31
31
  if (!rootWorkflow) return artifact;
32
32
  const nodes = rootWorkflow.nodes;
@@ -46,4 +46,4 @@ function appendPullRequestDelivery(config, artifact) {
46
46
  };
47
47
  }
48
48
  //#endregion
49
- export { appendPullRequestDelivery };
49
+ export { appendPullRequestDelivery, isPullRequestDeliveryEnabled };
package/package.json CHANGED
@@ -138,7 +138,7 @@
138
138
  "prepack": "nub run build:cli"
139
139
  },
140
140
  "type": "module",
141
- "version": "3.20.0",
141
+ "version": "3.21.0",
142
142
  "description": "Config-driven multi-agent pipeline runner for repository work",
143
143
  "main": "./dist/index.js",
144
144
  "types": "./dist/index.d.ts",
@@ -1,7 +0,0 @@
1
- //#region src/agent-assets.ts
2
- const AGENT_ASSET_SOURCE = "oisin-ee/agent";
3
- const AGENT_SKILL_SOURCE = "oisin-ee/agent/skills";
4
- const AGENT_HOOKS_DIR = "hooks";
5
- const AGENT_RULES_DIR = "rules";
6
- //#endregion
7
- export { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR, AGENT_RULES_DIR, AGENT_SKILL_SOURCE };
@@ -1,274 +0,0 @@
1
- import { applyJsonEdit, ensureTrailingNewline, parseJsonRecord } from "./json-config-merge.js";
2
- import { resolveHarnessTarget } from "./install-commands/shared.js";
3
- import { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR } from "./agent-assets.js";
4
- import { existsSync, readFileSync, statSync } from "node:fs";
5
- import { execa } from "execa";
6
- import { tmpdir } from "node:os";
7
- import { dirname, join, relative } from "node:path";
8
- import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
9
- import { createHash } from "node:crypto";
10
- //#region src/install-hooks.ts
11
- const DEFAULT_HOOK_INSTALL_SOURCE = AGENT_ASSET_SOURCE;
12
- const HOOK_HOSTS = [
13
- "claude-code",
14
- "codex",
15
- "opencode"
16
- ];
17
- const MANIFEST_FILE = ".moka-agent-hooks.json";
18
- const HOST_TARGET_ROOT = {
19
- "claude-code": ".claude",
20
- codex: ".codex",
21
- opencode: ".opencode"
22
- };
23
- const NON_HOOK_OWNED_TARGETS = /* @__PURE__ */ new Set([".opencode/opencode.json"]);
24
- function hashContent(content) {
25
- return createHash("sha256").update(content).digest("hex");
26
- }
27
- const MERGE_MANAGED = { ".claude/settings.json": [
28
- ["hooks"],
29
- ["skillListingBudgetFraction"],
30
- ["skillOverrides"]
31
- ] };
32
- function mergeKeysFor(path) {
33
- return MERGE_MANAGED[path];
34
- }
35
- function canonicalize(value) {
36
- if (Array.isArray(value)) return value.map(canonicalize);
37
- if (isRecord(value)) {
38
- const out = {};
39
- for (const key of Object.keys(value).sort()) out[key] = canonicalize(value[key]);
40
- return out;
41
- }
42
- return value;
43
- }
44
- function hashJson(value) {
45
- return hashContent(Buffer.from(JSON.stringify(canonicalize(value) ?? null)));
46
- }
47
- function managedSubtree(text, keyPath) {
48
- const parsed = parseJsonRecord(text);
49
- if (!parsed.ok) return;
50
- let cursor = parsed.value;
51
- for (const key of keyPath) {
52
- if (!isRecord(cursor)) return;
53
- cursor = cursor[key];
54
- }
55
- return cursor;
56
- }
57
- function targetIdentityHash(path, content) {
58
- const mergeKeys = mergeKeysFor(path);
59
- if (!mergeKeys) return hashContent(content);
60
- const text = content.toString("utf8");
61
- const subtrees = {};
62
- for (const keyPath of mergeKeys) subtrees[keyPath.join(".")] = managedSubtree(text, keyPath);
63
- return hashJson(subtrees);
64
- }
65
- async function cloneHookRepository(targetDir) {
66
- await execa("gh", [
67
- "repo",
68
- "clone",
69
- DEFAULT_HOOK_INSTALL_SOURCE,
70
- targetDir,
71
- "--",
72
- "--depth=1"
73
- ], { stdio: "inherit" });
74
- }
75
- async function withHookSource(useSource) {
76
- const parent = await mkdtemp(join(tmpdir(), "moka-agent-"));
77
- const source = join(parent, "agent");
78
- try {
79
- await cloneHookRepository(source);
80
- return await useSource(source);
81
- } finally {
82
- await rm(parent, {
83
- force: true,
84
- recursive: true
85
- });
86
- }
87
- }
88
- async function listFiles(root) {
89
- if (!existsSync(root)) return [];
90
- if (statSync(root).isFile()) return [root];
91
- const entries = await readdir(root, { withFileTypes: true });
92
- return (await Promise.all(entries.map((entry) => {
93
- const path = join(root, entry.name);
94
- return entry.isDirectory() ? listFiles(path) : [path];
95
- }))).flat();
96
- }
97
- async function sourceHookFiles(source) {
98
- return (await Promise.all(HOOK_HOSTS.map(async (host) => {
99
- const hostRoot = join(source, AGENT_HOOKS_DIR, host);
100
- return (await listFiles(hostRoot)).flatMap((file) => {
101
- const relativePath = relative(hostRoot, file).replaceAll("\\", "/");
102
- const content = readFileSync(file);
103
- const path = `${HOST_TARGET_ROOT[host]}/${relativePath}`;
104
- return isHookOwnedTarget(path) ? [{
105
- content,
106
- hash: targetIdentityHash(path, content),
107
- host,
108
- path
109
- }] : [];
110
- });
111
- }))).flat().sort((a, b) => a.path.localeCompare(b.path));
112
- }
113
- function manifestPath(host) {
114
- return resolveHarnessTarget(`${HOST_TARGET_ROOT[host]}/${MANIFEST_FILE}`);
115
- }
116
- function emptyManifest() {
117
- return {
118
- files: {},
119
- repository: DEFAULT_HOOK_INSTALL_SOURCE,
120
- version: 1
121
- };
122
- }
123
- function isHookOwnedTarget(path) {
124
- return !NON_HOOK_OWNED_TARGETS.has(path);
125
- }
126
- function readManifest(host) {
127
- const path = manifestPath(host);
128
- if (!existsSync(path)) return emptyManifest();
129
- try {
130
- return normalizeManifest(JSON.parse(readFileSync(path, "utf8")));
131
- } catch {
132
- return emptyManifest();
133
- }
134
- }
135
- function isRecord(value) {
136
- return typeof value === "object" && value !== null && !Array.isArray(value);
137
- }
138
- function normalizeManifest(value) {
139
- const files = {};
140
- const manifestFiles = isRecord(value) ? value.files : void 0;
141
- if (!isRecord(manifestFiles)) return emptyManifest();
142
- for (const [path, entry] of Object.entries(manifestFiles)) if (isRecord(entry) && typeof entry.hash === "string") files[path] = { hash: entry.hash };
143
- return {
144
- files,
145
- repository: DEFAULT_HOOK_INSTALL_SOURCE,
146
- version: 1
147
- };
148
- }
149
- function targetPath(path) {
150
- return resolveHarnessTarget(path);
151
- }
152
- function actionForFile(file, force, manifests) {
153
- const target = targetPath(file.path);
154
- if (!existsSync(target)) return "create";
155
- const currentHash = targetIdentityHash(file.path, readFileSync(target));
156
- if (currentHash === file.hash) return "unchanged";
157
- if (force) return "update";
158
- return (manifests.get(file.host)?.files[file.path])?.hash === currentHash ? "update" : "conflict";
159
- }
160
- function planFiles(files, force, manifests) {
161
- return files.map((file) => ({
162
- ...file,
163
- action: actionForFile(file, force, manifests)
164
- }));
165
- }
166
- function planObsoleteFiles(desiredPaths, force, manifests) {
167
- const obsolete = [];
168
- for (const [host, manifest] of manifests) for (const [path, entry] of Object.entries(manifest.files)) {
169
- const planned = planObsoleteFile(host, path, entry, force, desiredPaths);
170
- if (planned) obsolete.push(planned);
171
- }
172
- return obsolete.sort((a, b) => a.path.localeCompare(b.path));
173
- }
174
- function planObsoleteFile(host, path, entry, force, desiredPaths) {
175
- if (desiredPaths.has(path) || !isHookOwnedTarget(path)) return;
176
- const target = targetPath(path);
177
- if (!existsSync(target)) return;
178
- const currentHash = hashContent(readFileSync(target));
179
- return {
180
- action: force || currentHash === entry.hash ? "delete" : "conflict",
181
- host,
182
- path
183
- };
184
- }
185
- async function writePlannedFile(file) {
186
- if (file.action === "conflict" || file.action === "unchanged") return;
187
- const target = targetPath(file.path);
188
- await mkdir(dirname(target), { recursive: true });
189
- const mergeKeys = mergeKeysFor(file.path);
190
- if (mergeKeys && existsSync(target)) {
191
- const sourceText = file.content.toString("utf8");
192
- let merged = readFileSync(target, "utf8");
193
- for (const keyPath of mergeKeys) {
194
- const desired = managedSubtree(sourceText, keyPath);
195
- if (desired !== void 0) merged = applyJsonEdit(merged, keyPath, desired);
196
- }
197
- await writeFile(target, ensureTrailingNewline(merged));
198
- return;
199
- }
200
- await writeFile(target, file.content);
201
- }
202
- function itemFor(file) {
203
- return {
204
- action: file.action,
205
- host: file.host,
206
- path: file.path
207
- };
208
- }
209
- function itemForObsolete(file) {
210
- return {
211
- action: file.action,
212
- host: file.host,
213
- path: file.path
214
- };
215
- }
216
- async function removeObsoleteFile(file) {
217
- if (file.action !== "delete") return;
218
- await rm(targetPath(file.path), { force: true });
219
- }
220
- function assertNoConflicts(items, dryRun) {
221
- if (dryRun) return;
222
- const conflicts = items.filter((item) => item.action === "conflict");
223
- if (conflicts.length === 0) return;
224
- throw new Error([
225
- "Refusing to overwrite manually edited hook files.",
226
- ...conflicts.map((item) => `- ${item.path}`),
227
- "Re-run with --force to overwrite them."
228
- ].join("\n"));
229
- }
230
- function assertCheckCurrent(items, check) {
231
- if (!check) return;
232
- const changed = items.filter((item) => item.action !== "unchanged");
233
- if (changed.length === 0) return;
234
- throw new Error(["Installed hook files are not up to date.", ...changed.map((item) => `- ${item.path}: ${item.action}`)].join("\n"));
235
- }
236
- async function writeManifests(files) {
237
- const byHost = /* @__PURE__ */ new Map();
238
- for (const host of HOOK_HOSTS) byHost.set(host, emptyManifest());
239
- for (const file of files) {
240
- const manifest = byHost.get(file.host);
241
- if (manifest) manifest.files[file.path] = { hash: file.hash };
242
- }
243
- await Promise.all([...byHost.entries()].map(async ([host, manifest]) => {
244
- const path = manifestPath(host);
245
- if (Object.keys(manifest.files).length === 0) {
246
- await rm(path, { force: true });
247
- return;
248
- }
249
- await mkdir(dirname(path), { recursive: true });
250
- await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
251
- }));
252
- }
253
- function installHooks(options = {}) {
254
- return withHookSource(async (source) => {
255
- const files = await sourceHookFiles(source);
256
- const manifests = new Map(HOOK_HOSTS.map((host) => [host, readManifest(host)]));
257
- const planned = planFiles(files, Boolean(options.force), manifests);
258
- const obsolete = planObsoleteFiles(new Set(files.map((file) => file.path)), Boolean(options.force), manifests);
259
- const items = [...planned.map(itemFor), ...obsolete.map(itemForObsolete)];
260
- assertCheckCurrent(items, Boolean(options.check));
261
- assertNoConflicts(items, Boolean(options.dryRun));
262
- if (!(options.check || options.dryRun)) {
263
- for (const file of planned) await writePlannedFile(file);
264
- for (const file of obsolete) await removeObsoleteFile(file);
265
- await writeManifests(planned);
266
- }
267
- return {
268
- items,
269
- source: DEFAULT_HOOK_INSTALL_SOURCE
270
- };
271
- });
272
- }
273
- //#endregion
274
- export { installHooks };
@@ -1,200 +0,0 @@
1
- import { resolveHarnessTarget } from "./install-commands/shared.js";
2
- import { AGENT_ASSET_SOURCE, AGENT_RULES_DIR } from "./agent-assets.js";
3
- import { execa } from "execa";
4
- import { tmpdir } from "node:os";
5
- import { dirname, join } from "node:path";
6
- import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
7
- //#region src/install-rules.ts
8
- const DEFAULT_RULES_INSTALL_SOURCE = AGENT_ASSET_SOURCE;
9
- const RULESYNC_PACKAGE = "rulesync@8.30.1";
10
- const RULESYNC_TARGETS = [
11
- "claudecode",
12
- "codexcli",
13
- "geminicli",
14
- "opencode"
15
- ];
16
- const RULE_OUTPUTS = [
17
- {
18
- generatedPath: ".claude/CLAUDE.md",
19
- targetPath: ".claude/CLAUDE.md"
20
- },
21
- {
22
- generatedPath: ".codex/AGENTS.md",
23
- targetPath: ".codex/AGENTS.md"
24
- },
25
- {
26
- generatedPath: ".gemini/GEMINI.md",
27
- targetPath: ".gemini/GEMINI.md"
28
- },
29
- {
30
- generatedPath: ".config/opencode/AGENTS.md",
31
- targetPath: ".opencode/AGENTS.md"
32
- }
33
- ];
34
- async function cloneRulesRepository(targetDir) {
35
- await execa("gh", [
36
- "repo",
37
- "clone",
38
- DEFAULT_RULES_INSTALL_SOURCE,
39
- targetDir,
40
- "--",
41
- "--depth=1"
42
- ], { stdio: "inherit" });
43
- }
44
- async function withRulesSource(sourceOverride, useSource) {
45
- if (sourceOverride !== void 0) return useSource(sourceOverride);
46
- const parent = await mkdtemp(join(tmpdir(), "moka-agent-rules-"));
47
- const source = join(parent, "agent");
48
- try {
49
- await cloneRulesRepository(source);
50
- return await useSource(source);
51
- } finally {
52
- await rm(parent, {
53
- force: true,
54
- recursive: true
55
- });
56
- }
57
- }
58
- async function defaultRulesyncRunner(args, opts) {
59
- try {
60
- await execa("npx", [
61
- "--yes",
62
- RULESYNC_PACKAGE,
63
- ...args
64
- ], {
65
- cwd: opts.cwd,
66
- env: opts.env,
67
- stdio: "inherit"
68
- });
69
- } catch (error) {
70
- const cause = error instanceof Error ? `: ${error.message}` : "";
71
- throw new Error(`Failed to generate global rules from ${DEFAULT_RULES_INSTALL_SOURCE}${cause}. If this is a private repository, authenticate GitHub access with \`gh auth login\` and rerun \`moka init\`.`);
72
- }
73
- }
74
- async function buildRootRule(source) {
75
- const rulesDir = join(source, AGENT_RULES_DIR);
76
- let entries = [];
77
- try {
78
- entries = (await readdir(rulesDir, { withFileTypes: true })).filter((d) => d.isFile() && d.name.endsWith(".md")).map((d) => d.name).sort((a, b) => a.localeCompare(b));
79
- } catch {}
80
- const rootContent = `---
81
- root: true
82
- targets:
83
- - "*"
84
- ---
85
-
86
- ${(await Promise.all(entries.map(async (name) => {
87
- return (await readFile(join(rulesDir, name), "utf8")).trimEnd();
88
- }))).join("\n\n")}\n`;
89
- const rulesyncRulesDir = join(source, ".rulesync", "rules");
90
- await mkdir(rulesyncRulesDir, { recursive: true });
91
- await writeFile(join(rulesyncRulesDir, "_root.md"), rootContent);
92
- }
93
- async function withRulesyncHome(useHome) {
94
- const home = await mkdtemp(join(tmpdir(), "moka-rules-home-"));
95
- try {
96
- return await useHome(home);
97
- } finally {
98
- await rm(home, {
99
- force: true,
100
- recursive: true
101
- });
102
- }
103
- }
104
- function rulesyncArgs(options) {
105
- const args = [
106
- "generate",
107
- "-t",
108
- RULESYNC_TARGETS.join(","),
109
- "-f",
110
- "rules",
111
- "--delete",
112
- "--global"
113
- ];
114
- if (options.dryRun) args.push("--dry-run");
115
- if (options.silent) args.push("--silent");
116
- return args;
117
- }
118
- async function runRulesyncGenerate(input) {
119
- await input.runner(rulesyncArgs(input), {
120
- cwd: input.source,
121
- env: {
122
- ...process.env,
123
- HOME_DIR: input.home
124
- }
125
- });
126
- }
127
- function ruleItems(action) {
128
- return RULE_OUTPUTS.map((output) => ({
129
- action,
130
- path: resolveHarnessTarget(output.targetPath)
131
- }));
132
- }
133
- function readGeneratedRuleFiles(home) {
134
- return Promise.all(RULE_OUTPUTS.map(async (output) => ({
135
- content: await readFile(join(home, output.generatedPath), "utf8"),
136
- path: resolveHarnessTarget(output.targetPath)
137
- })));
138
- }
139
- async function writeGeneratedRuleFiles(files) {
140
- for (const file of files) {
141
- await mkdir(dirname(file.path), { recursive: true });
142
- await writeFile(file.path, file.content);
143
- }
144
- }
145
- async function installedRuleAction(file) {
146
- try {
147
- return await readFile(file.path, "utf8") === file.content ? "unchanged" : "update";
148
- } catch {
149
- return "create";
150
- }
151
- }
152
- async function assertInstalledRulesCurrent(files) {
153
- const changed = (await Promise.all(files.map(async (file) => ({
154
- action: await installedRuleAction(file),
155
- path: file.path
156
- })))).filter((item) => item.action !== "unchanged");
157
- if (changed.length === 0) return;
158
- throw new Error(["Installed rule files are not up to date.", ...changed.map((item) => `- ${item.path}: ${item.action}`)].join("\n"));
159
- }
160
- function installRules(options = {}) {
161
- const runner = options.rulesyncRunner ?? defaultRulesyncRunner;
162
- return withRulesSource(options.sourceOverride, async (source) => {
163
- await buildRootRule(source);
164
- return withRulesyncHome(async (home) => {
165
- if (options.dryRun) {
166
- await runRulesyncGenerate({
167
- dryRun: true,
168
- home,
169
- runner,
170
- source
171
- });
172
- return {
173
- items: ruleItems("skip"),
174
- source: DEFAULT_RULES_INSTALL_SOURCE
175
- };
176
- }
177
- await runRulesyncGenerate({
178
- home,
179
- runner,
180
- silent: options.check,
181
- source
182
- });
183
- const files = await readGeneratedRuleFiles(home);
184
- if (options.check) {
185
- await assertInstalledRulesCurrent(files);
186
- return {
187
- items: ruleItems("skip"),
188
- source: DEFAULT_RULES_INSTALL_SOURCE
189
- };
190
- }
191
- await writeGeneratedRuleFiles(files);
192
- return {
193
- items: ruleItems("generate"),
194
- source: DEFAULT_RULES_INSTALL_SOURCE
195
- };
196
- });
197
- });
198
- }
199
- //#endregion
200
- export { installRules };