From d9b1ba79239339a1b9e5185f787fbb4d32aa5f18 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:40:48 +0800 Subject: [PATCH 1/4] fix(hosts): harden managed integration lifecycle --- CHANGELOG.md | 4 ++++ src/hosts/amazon-q/index.ts | 3 +-- src/hosts/copilot-agent/index.ts | 3 +++ src/hosts/crush/index.ts | 4 +--- src/hosts/gemini-cli/index.ts | 15 +++++++++---- src/hosts/kimi/index.ts | 28 +++++++++++++++++++----- src/hosts/mcp-agent/index.ts | 16 +++++++++++--- test/hosts/copilot-agent.test.ts | 16 ++++++++++++++ test/hosts/crush.test.ts | 23 +++++++++++++++----- test/hosts/gemini-cli.test.ts | 29 +++++++++++++++++++++++++ test/hosts/kimi.test.ts | 18 ++++++++++++++++ test/hosts/mcp-agent.test.ts | 37 ++++++++++++++++++++++++++++++++ test/hosts/qodo.test.ts | 8 +++++++ 13 files changed, 182 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f38bdc..efbdcdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Harden ownership detection, restoration, and malformed marker handling for beta host integrations. + ## 0.8.0 - 2026-05-25 ### Features diff --git a/src/hosts/amazon-q/index.ts b/src/hosts/amazon-q/index.ts index 98ac06a3..98b09843 100644 --- a/src/hosts/amazon-q/index.ts +++ b/src/hosts/amazon-q/index.ts @@ -1,4 +1,4 @@ -import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises"; +import { mkdir, rename, stat, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { @@ -151,7 +151,6 @@ export async function uninstallAmazonQRule( const backupPath = `${resolvedRulePath}.bak`; const backup = await readInstructionFile(backupPath); if (existing.exists && backup.exists && !isTokenjuiceAmazonQRuleText(backup.text)) { - await rm(resolvedRulePath, { force: true }); await rename(backupPath, resolvedRulePath); return { rulePath: resolvedRulePath, removed: true }; } diff --git a/src/hosts/copilot-agent/index.ts b/src/hosts/copilot-agent/index.ts index 191c560c..6a6aadb2 100644 --- a/src/hosts/copilot-agent/index.ts +++ b/src/hosts/copilot-agent/index.ts @@ -343,6 +343,9 @@ export async function installCopilotAgentHook( removeTokenjuiceCopilotAgentHooks(config); const retained = getPostToolUseHooks(config); config.hooks.postToolUse = [...retained, createTokenjuiceCopilotAgentHook(command)]; + if (config.disableAllHooks === true) { + config.disableAllHooks = false; + } if (typeof config.version !== "number") { config.version = 1; } diff --git a/src/hosts/crush/index.ts b/src/hosts/crush/index.ts index 60ef6460..ccb6168b 100644 --- a/src/hosts/crush/index.ts +++ b/src/hosts/crush/index.ts @@ -67,9 +67,7 @@ async function writeCrushSkillWithoutBackup(skillPath: string): Promise { } function isTokenjuiceCrushSkillText(text: string): boolean { - return text.includes(TOKENJUICE_CRUSH_MARKER) - || text.includes(TOKENJUICE_WRAP_COMMAND) - || text.includes(TOKENJUICE_RAW_COMMAND); + return text.includes(TOKENJUICE_CRUSH_MARKER); } export async function installCrushSkill( diff --git a/src/hosts/gemini-cli/index.ts b/src/hosts/gemini-cli/index.ts index d84011d3..a662d9bd 100644 --- a/src/hosts/gemini-cli/index.ts +++ b/src/hosts/gemini-cli/index.ts @@ -1,4 +1,5 @@ -import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; @@ -99,13 +100,19 @@ async function loadGeminiSettingsWithBackup(settingsPath: string): Promise<{ con async function writeGeminiSettings(settingsPath: string, config: GeminiSettings): Promise { await mkdir(dirname(settingsPath), { recursive: true }); - const tempPath = `${settingsPath}.tmp`; - await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); - await rename(tempPath, settingsPath); + const tempPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`; + try { + await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", flag: "wx" }); + await rename(tempPath, settingsPath); + } finally { + await rm(tempPath, { force: true }); + } } function isTokenjuiceGeminiHook(entry: unknown): boolean { return isRecord(entry) + && entry.type === "command" + && entry.name === "tokenjuice" && typeof entry.command === "string" && entry.command.includes(TOKENJUICE_GEMINI_CLI_SUBCOMMAND); } diff --git a/src/hosts/kimi/index.ts b/src/hosts/kimi/index.ts index 6a52fa03..b9281f40 100644 --- a/src/hosts/kimi/index.ts +++ b/src/hosts/kimi/index.ts @@ -95,12 +95,30 @@ async function readConfig(configPath: string): Promise<{ text: string; exists: b } } -function countMarker(text: string, marker: string): number { - return text.split(marker).length - 1; -} - function hasMalformedMarkerStructure(text: string): boolean { - return countMarker(text, TOKENJUICE_KIMI_BEGIN) !== countMarker(text, TOKENJUICE_KIMI_END); + let offset = 0; + let hasOpenBlock = false; + while (offset < text.length) { + const beginIndex = text.indexOf(TOKENJUICE_KIMI_BEGIN, offset); + const endIndex = text.indexOf(TOKENJUICE_KIMI_END, offset); + if (beginIndex === -1 && endIndex === -1) { + break; + } + if (beginIndex !== -1 && (endIndex === -1 || beginIndex < endIndex)) { + if (hasOpenBlock) { + return true; + } + hasOpenBlock = true; + offset = beginIndex + TOKENJUICE_KIMI_BEGIN.length; + } else { + if (!hasOpenBlock) { + return true; + } + hasOpenBlock = false; + offset = endIndex + TOKENJUICE_KIMI_END.length; + } + } + return hasOpenBlock; } function removeKimiHookBlock(text: string): { text: string; removed: number } { diff --git a/src/hosts/mcp-agent/index.ts b/src/hosts/mcp-agent/index.ts index 686cb854..2e0a0f37 100644 --- a/src/hosts/mcp-agent/index.ts +++ b/src/hosts/mcp-agent/index.ts @@ -40,6 +40,7 @@ export type McpAgentDoctorReport = { }; const TOKENJUICE_MCP_AGENT_FIX_COMMAND = "tokenjuice install mcp-agent"; +const TOKENJUICE_MCP_AGENT_OWNERSHIP_MARKER = ""; const TOKENJUICE_MCP_AGENT_MARKER = "tokenjuice mcp-agent terminal output compaction"; const TOKENJUICE_MCP_AGENT_RESTORE_BACKUP_MARKER_PREFIX = "`, ""] : []), @@ -285,7 +296,6 @@ export async function uninstallMcpAgentDefinition( await rejectDefinitionSymlink(backupPath); const backup = await readInstructionFile(backupPath); if (backup.exists && !isTokenjuiceMcpAgentDefinitionText(backup.text)) { - await rm(resolvedAgentPath, { force: true }); await rename(backupPath, resolvedAgentPath); return { agentPath: resolvedAgentPath, removed: true }; } diff --git a/test/hosts/copilot-agent.test.ts b/test/hosts/copilot-agent.test.ts index 059c874f..419f69ca 100644 --- a/test/hosts/copilot-agent.test.ts +++ b/test/hosts/copilot-agent.test.ts @@ -131,6 +131,22 @@ describe("installCopilotAgentHook", () => { expect(parsed.hooks.postToolUse.find((entry) => entry.bash.includes("copilot-agent-post-tool-use"))).toBeTruthy(); }); + it("enables hooks when reinstalling into a disabled configuration", async () => { + const projectDir = await createTempDir(); + const hooksPath = join(projectDir, ".github", "hooks", "tokenjuice-agent.json"); + await mkdir(join(projectDir, ".github", "hooks"), { recursive: true }); + await writeFile( + hooksPath, + `${JSON.stringify({ version: 1, disableAllHooks: true, hooks: {} }, null, 2)}\n`, + "utf8", + ); + + await installCopilotAgentHook(undefined, { projectDir }); + const config = JSON.parse(await readFile(hooksPath, "utf8")) as { disableAllHooks?: boolean }; + + expect(config.disableAllHooks).toBe(false); + }); + it("removes stale tokenjuice entries from sibling repo hook files", async () => { const projectDir = await createTempDir(); const hooksDir = join(projectDir, ".github", "hooks"); diff --git a/test/hosts/crush.test.ts b/test/hosts/crush.test.ts index 4268165a..1374063f 100644 --- a/test/hosts/crush.test.ts +++ b/test/hosts/crush.test.ts @@ -105,18 +105,19 @@ describe("crush skill", () => { await expect(readFile(skillPath, "utf8")).resolves.toContain("tokenjuice wrap --raw -- "); }); - it("does not restore tokenjuice guidance with an edited marker on uninstall", async () => { + it("restores a markerless existing skill that mentions tokenjuice commands", async () => { const home = await createTempDir(); const skillPath = join(home, ".crush", "skills", "tokenjuice", "SKILL.md"); - await installCrushSkill(undefined, { projectDir: home }); - await writeFile(skillPath, "---\nname: tokenjuice\n---\n\nuse `tokenjuice wrap -- `\n", "utf8"); + const customSkill = "---\nname: tokenjuice\n---\n\nuse `tokenjuice wrap -- `\n"; + await mkdir(join(home, ".crush", "skills", "tokenjuice"), { recursive: true }); + await writeFile(skillPath, customSkill, "utf8"); await installCrushSkill(undefined, { projectDir: home }); const removed = await uninstallCrushSkill(skillPath); expect(removed.removed).toBe(true); - await expect(access(skillPath)).rejects.toMatchObject({ code: "ENOENT" }); - await expect(readFile(`${skillPath}.bak`, "utf8")).resolves.toContain("tokenjuice wrap -- "); + await expect(readFile(skillPath, "utf8")).resolves.toBe(customSkill); + await expect(access(`${skillPath}.bak`)).rejects.toMatchObject({ code: "ENOENT" }); }); it("restores a backed-up custom skill on uninstall", async () => { @@ -202,6 +203,18 @@ describe("crush skill", () => { await expect(readFile(skillPath, "utf8")).resolves.toContain("custom skill"); }); + it("does not remove a custom skill that only mentions tokenjuice commands", async () => { + const home = await createTempDir(); + const skillPath = join(home, ".crush", "skills", "tokenjuice", "SKILL.md"); + await mkdir(join(home, ".crush", "skills", "tokenjuice"), { recursive: true }); + await writeFile(skillPath, "---\nname: tokenjuice\n---\n\nrun tokenjuice wrap -- \n", "utf8"); + + const removed = await uninstallCrushSkill(skillPath); + + expect(removed.removed).toBe(false); + await expect(readFile(skillPath, "utf8")).resolves.toContain("run tokenjuice wrap -- "); + }); + it("uses CRUSH_PROJECT_DIR for the default skill file", async () => { const home = await createTempDir(); process.env.CRUSH_PROJECT_DIR = home; diff --git a/test/hosts/gemini-cli.test.ts b/test/hosts/gemini-cli.test.ts index d7e7612f..77b942bd 100644 --- a/test/hosts/gemini-cli.test.ts +++ b/test/hosts/gemini-cli.test.ts @@ -78,6 +78,35 @@ describe("gemini-cli hooks", () => { expect(parsed.hooks.AfterTool[1]?.hooks[0]?.command).toBe(`${launcherPath} gemini-cli-after-tool`); }); + it("preserves unrelated hooks that mention the tokenjuice subcommand", async () => { + const home = await createTempDir(); + const settingsPath = join(home, "settings.json"); + await writeFile(settingsPath, JSON.stringify({ + hooks: { + AfterTool: [ + { + matcher: "run_shell_command", + hooks: [{ type: "command", name: "custom", command: "echo gemini-cli-after-tool" }], + }, + ], + }, + })); + + await installGeminiCliHook(settingsPath); + const removed = await uninstallGeminiCliHook(settingsPath); + const parsed = JSON.parse(await readFile(settingsPath, "utf8")) as { + hooks: { AfterTool: Array<{ hooks: Array<{ name?: string; command: string }> }> }; + }; + + expect(removed.removed).toBe(1); + expect(parsed.hooks.AfterTool).toEqual([ + { + matcher: "run_shell_command", + hooks: [{ type: "command", name: "custom", command: "echo gemini-cli-after-tool" }], + }, + ]); + }); + it("reports installed and uninstalled hook health", async () => { const home = await createTempDir(); const settingsPath = join(home, "settings.json"); diff --git a/test/hosts/kimi.test.ts b/test/hosts/kimi.test.ts index 69567f94..61c4c28b 100644 --- a/test/hosts/kimi.test.ts +++ b/test/hosts/kimi.test.ts @@ -169,6 +169,24 @@ describe("kimi hook", () => { expect(disabled.status).toBe("disabled"); }); + it("refuses balanced but misordered tokenjuice markers", async () => { + const home = await createTempDir(); + const configPath = join(home, ".kimi", "config.toml"); + const malformed = [ + "# tokenjuice:kimi begin", + 'theme = "dark"', + "# tokenjuice:kimi begin", + "# tokenjuice:kimi end", + 'model = "keep"', + "# tokenjuice:kimi end", + ].join("\n"); + await mkdir(join(home, ".kimi"), { recursive: true }); + await writeFile(configPath, malformed, "utf8"); + + await expect(uninstallKimiHook(configPath)).rejects.toThrow(/malformed tokenjuice Kimi markers/u); + await expect(readFile(configPath, "utf8")).resolves.toBe(malformed); + }); + it("uses KIMI_HOME for the default config path", async () => { const home = await createTempDir(); process.env.KIMI_HOME = home; diff --git a/test/hosts/mcp-agent.test.ts b/test/hosts/mcp-agent.test.ts index b0aa5275..901b6306 100644 --- a/test/hosts/mcp-agent.test.ts +++ b/test/hosts/mcp-agent.test.ts @@ -117,6 +117,7 @@ describe("mcp-agent definition", () => { expect(result.agentPath).toBe(agentPath); expect(result.backupPath).toBeUndefined(); expect(definition).toContain("name: tokenjuice"); + expect(definition).toContain(""); expect(definition).toContain("tokenjuice mcp-agent terminal output compaction"); expect(definition).toContain("mcp-agent workflow"); expect(definition).toContain("tokenjuice wrap -- "); @@ -205,6 +206,20 @@ describe("mcp-agent definition", () => { await expect(readFile(`${agentPath}.bak`, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); }); + it("recognizes and removes an exact legacy tokenjuice definition", async () => { + const home = await createTempDir(); + const agentPath = join(home, ".mcp-agent", "agents", "tokenjuice.md"); + await installMcpAgentDefinition(agentPath, { projectDir: home }); + const currentDefinition = await readFile(agentPath, "utf8"); + const legacyDefinition = currentDefinition.replace("\n\n", ""); + await writeFile(agentPath, legacyDefinition, "utf8"); + + const removed = await uninstallMcpAgentDefinition(agentPath, { projectDir: home }); + + expect(removed.removed).toBe(true); + await expect(readFile(agentPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + }); + it("does not restore incidental backups when uninstalling a fresh tokenjuice definition", async () => { const home = await createTempDir(); const agentPath = join(home, ".mcp-agent", "agents", "tokenjuice.md"); @@ -249,6 +264,7 @@ describe("mcp-agent definition", () => { "name: tokenjuice", "description: stale", "---", + "", "# tokenjuice mcp-agent terminal output compaction", "- Prefer `tokenjuice wrap -- `.", "- If output looks wrong, rerun with `tokenjuice wrap --full -- `.", @@ -279,6 +295,27 @@ describe("mcp-agent definition", () => { expect(definition).toBe("custom mcp-agent definition\n"); }); + it("does not claim an unrelated definition that only mentions the tokenjuice marker", async () => { + const home = await createTempDir(); + const agentPath = join(home, ".mcp-agent", "agents", "tokenjuice.md"); + const customDefinition = [ + "---", + "name: tokenjuice", + "---", + "", + "# tokenjuice mcp-agent terminal output compaction", + "", + "Custom guidance.", + ].join("\n"); + await mkdir(join(home, ".mcp-agent", "agents"), { recursive: true }); + await writeFile(agentPath, customDefinition, "utf8"); + + const removed = await uninstallMcpAgentDefinition(agentPath, { projectDir: home }); + + expect(removed.removed).toBe(false); + await expect(readFile(agentPath, "utf8")).resolves.toBe(customDefinition); + }); + it("reports non-tokenjuice agent definitions as disabled", async () => { const home = await createTempDir(); const agentPath = join(home, ".mcp-agent", "agents", "tokenjuice.md"); diff --git a/test/hosts/qodo.test.ts b/test/hosts/qodo.test.ts index 6a4175b7..ed2b9828 100644 --- a/test/hosts/qodo.test.ts +++ b/test/hosts/qodo.test.ts @@ -210,6 +210,14 @@ describe("Qodo review config", () => { await expect(installQodoReviewConfig(configPath, { projectDir: home })).rejects.toThrow(/inline TOML table/u); }); + it("refuses quoted inline review_agent tables before adding a table section", async () => { + const home = await createTempDir(); + const configPath = join(home, ".pr_agent.toml"); + await writeFile(configPath, '"review_agent" = { issues_user_guidelines = "keep my review guidance" }\n', "utf8"); + + await expect(installQodoReviewConfig(configPath, { projectDir: home })).rejects.toThrow(/inline TOML table/u); + }); + it("ignores table-looking text inside review-agent multiline strings", async () => { const home = await createTempDir(); const configPath = join(home, ".pr_agent.toml"); From da72009a01f20c23be1a86ab328076752a2c395b Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:40:50 +0800 Subject: [PATCH 2/4] fix(copilot-cli): read live post-tool fields --- src/hosts/copilot-cli/index.ts | 24 ++++++++++++++++-------- test/hosts/copilot-cli.test.ts | 7 +++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/hosts/copilot-cli/index.ts b/src/hosts/copilot-cli/index.ts index 2d2fad22..f0231e68 100644 --- a/src/hosts/copilot-cli/index.ts +++ b/src/hosts/copilot-cli/index.ts @@ -63,6 +63,10 @@ type CopilotCliPostToolUsePayload = { toolArgs?: unknown; tool_result?: unknown; toolResult?: unknown; + result_type?: unknown; + resultType?: unknown; + text_result_for_llm?: unknown; + textResultForLlm?: unknown; }; const TOKENJUICE_COPILOT_CLI_FIX_COMMAND = "tokenjuice install copilot-cli"; @@ -492,14 +496,14 @@ export async function runCopilotCliPostToolUseHook(rawText: string): Promise { cwd: "/tmp", toolName: "bash", toolArgs: { command: "git ls-files" }, - toolResult: { - resultType: "success", - textResultForLlm: longOutput, - }, + toolResult: "raw tool result", + resultType: "success", + textResultForLlm: longOutput, }); const { code, output } = await captureStdout(() => runCopilotCliPostToolUseHook(payload)); From eb22a10de7d998ead97ad4999bb324ae1f10699f Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:40:56 +0800 Subject: [PATCH 3/4] fix(release): build from explicit tags --- .github/workflows/homebrew-tap.yml | 10 ++++++++++ .github/workflows/release.yml | 19 ++++++++++++++----- CHANGELOG.md | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/homebrew-tap.yml b/.github/workflows/homebrew-tap.yml index b71f0c0e..56d6a34d 100644 --- a/.github/workflows/homebrew-tap.yml +++ b/.github/workflows/homebrew-tap.yml @@ -17,6 +17,16 @@ jobs: if: startsWith(inputs.tag_name, 'v') steps: + - name: Validate release tag + env: + TAG: ${{ inputs.tag_name }} + run: | + set -euo pipefail + if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*$ ]]; then + echo "Invalid release tag: ${TAG}" + exit 1 + fi + - name: Validate tap configuration run: | set -euo pipefail diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e8a42cc..65a613f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Resolve release tag + id: release env: INPUT_TAG: ${{ inputs.tag_name }} run: | @@ -43,6 +39,19 @@ jobs: fi echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV" echo "RELEASE_VERSION=${TAG#v}" >> "$GITHUB_ENV" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + + - name: Checkout release tag + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: refs/tags/${{ steps.release.outputs.tag }} + + - name: Verify checked out release tag + run: | + set -euo pipefail + git rev-parse --verify "refs/tags/${RELEASE_TAG}^{commit}" >/dev/null + test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" - name: Setup Node uses: actions/setup-node@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index efbdcdc5..1b8997d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Harden ownership detection, restoration, and malformed marker handling for beta host integrations. +- Build release artifacts from explicit tags and validate Homebrew tap release-tag input. ## 0.8.0 - 2026-05-25 From 6c30ac156089c86d5823c4e862acc336891a0b00 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:41:00 +0800 Subject: [PATCH 4/4] fix(build): support cross-platform dist builds --- CHANGELOG.md | 1 + package.json | 2 +- scripts/clean-dist.mjs | 3 +++ scripts/copy-rules.mjs | 4 ++++ 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 scripts/clean-dist.mjs create mode 100644 scripts/copy-rules.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8997d2..132566aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Harden ownership detection, restoration, and malformed marker handling for beta host integrations. - Build release artifacts from explicit tags and validate Homebrew tap release-tag input. +- Make `pnpm build` portable across supported Node platforms. ## 0.8.0 - 2026-05-25 diff --git a/package.json b/package.json index b58dc80e..f59a712c 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "generate:builtin-rules": "node scripts/generate-builtin-rules.mjs", "prepare": "pnpm repair:pnpm-bin-shims", "repair:pnpm-bin-shims": "node scripts/repair-pnpm-bin-shims.mjs", - "build": "rm -rf dist && pnpm generate:builtin-rules && tsc -p tsconfig.json && node scripts/build-pi-runtime.mjs && node scripts/build-opencode-runtime.mjs && mkdir -p dist/rules && cp -R src/rules/. dist/rules/", + "build": "node scripts/clean-dist.mjs && pnpm generate:builtin-rules && tsc -p tsconfig.json && node scripts/build-pi-runtime.mjs && node scripts/build-opencode-runtime.mjs && node scripts/copy-rules.mjs", "lint": "oxlint --deny-warnings src test scripts", "lint:circular": "madge --circular --extensions ts,mjs --ts-config tsconfig.json src", "release:artifacts": "node scripts/build-release.mjs", diff --git a/scripts/clean-dist.mjs b/scripts/clean-dist.mjs new file mode 100644 index 00000000..d8975d02 --- /dev/null +++ b/scripts/clean-dist.mjs @@ -0,0 +1,3 @@ +import { rm } from "node:fs/promises"; + +await rm("dist", { recursive: true, force: true }); diff --git a/scripts/copy-rules.mjs b/scripts/copy-rules.mjs new file mode 100644 index 00000000..ab7c057f --- /dev/null +++ b/scripts/copy-rules.mjs @@ -0,0 +1,4 @@ +import { cp, mkdir } from "node:fs/promises"; + +await mkdir("dist/rules", { recursive: true }); +await cp("src/rules", "dist/rules", { recursive: true });