open-plan-annotator 1.0.9 → 1.0.12

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Interactive plan annotation plugin for Claude Code",
8
- "version": "1.0.0"
8
+ "version": "1.0.12"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "open-plan-annotator",
13
13
  "source": "./",
14
14
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
15
- "version": "1.0.9",
15
+ "version": "1.0.12",
16
16
  "author": {
17
17
  "name": "ndom91"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
3
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
4
- "version": "1.0.9",
4
+ "version": "1.0.12",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
package/README.md CHANGED
@@ -117,6 +117,12 @@ bun run lint:fix # auto-fix
117
117
  bun run format # format
118
118
  ```
119
119
 
120
+ ## Maintainer Docs
121
+
122
+ - Architecture: [`docs/architecture.md`](docs/architecture.md)
123
+ - Operations: [`docs/operations.md`](docs/operations.md)
124
+ - Release process: [`docs/release.md`](docs/release.md)
125
+
120
126
  ## License
121
127
 
122
128
  MIT
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFileSync, spawn } = require("child_process");
4
- const path = require("path");
5
- const fs = require("fs");
3
+ import { execFileSync, spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
 
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
  const binaryPath = path.join(__dirname, "open-plan-annotator-binary");
8
- const installScript = path.join(__dirname, "..", "install.cjs");
9
- const VERSION = require("../package.json").version;
10
+ const installScript = path.join(__dirname, "..", "install.mjs");
11
+ const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
10
12
 
11
13
  const arg = process.argv[2];
12
14
 
package/install.mjs ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import https from "node:https";
6
+ import path from "node:path";
7
+ import {
8
+ PLATFORM_ASSET_BASENAME_MAP,
9
+ REPO,
10
+ getPlatformAssetArchiveName,
11
+ getPlatformKey,
12
+ parseChecksumManifest,
13
+ selectChecksumAsset,
14
+ } from "./shared/releaseAssets.mjs";
15
+ import { fileURLToPath } from "node:url";
16
+ import zlib from "node:zlib";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const VERSION = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
20
+
21
+ function getReleaseApiUrl() {
22
+ return `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
23
+ }
24
+
25
+ function fetch(url, redirects) {
26
+ if (redirects === undefined) redirects = 5;
27
+ return new Promise((resolve, reject) => {
28
+ https
29
+ .get(url, { headers: { "User-Agent": "open-plan-annotator-install" } }, (res) => {
30
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
31
+ if (redirects <= 0) return reject(new Error("Too many redirects"));
32
+ return fetch(res.headers.location, redirects - 1).then(resolve, reject);
33
+ }
34
+ if (res.statusCode < 200 || res.statusCode >= 300) {
35
+ return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
36
+ }
37
+ const chunks = [];
38
+ res.on("data", (c) => chunks.push(c));
39
+ res.on("end", () => resolve(Buffer.concat(chunks)));
40
+ })
41
+ .on("error", reject);
42
+ });
43
+ }
44
+
45
+ async function fetchJson(url) {
46
+ const buffer = await fetch(url);
47
+ return JSON.parse(buffer.toString("utf8"));
48
+ }
49
+
50
+ function sha256Hex(buffer) {
51
+ return crypto.createHash("sha256").update(buffer).digest("hex");
52
+ }
53
+
54
+ async function resolveReleaseAssetAndChecksum(options) {
55
+ const opts = options || {};
56
+ const fetchJsonImpl = opts.fetchJson || fetchJson;
57
+ const fetchBuffer = opts.fetch || fetch;
58
+ const releaseApiUrl = opts.releaseApiUrl || getReleaseApiUrl();
59
+ const version = opts.version || VERSION;
60
+
61
+ const release = await fetchJsonImpl(releaseApiUrl);
62
+ const releaseAssets = Array.isArray(release.assets) ? release.assets : [];
63
+ const key = opts.platformKey || getPlatformKey();
64
+ const assetName = getPlatformAssetArchiveName(key);
65
+ if (!assetName) {
66
+ throw new Error(`Unsupported platform ${key}`);
67
+ }
68
+
69
+ const asset = releaseAssets.find((entry) => entry.name === assetName);
70
+ if (!asset) {
71
+ throw new Error(`Release v${version} is missing asset ${assetName}`);
72
+ }
73
+
74
+ const checksumAsset = selectChecksumAsset(releaseAssets);
75
+ if (!checksumAsset) {
76
+ throw new Error(`Release v${version} does not contain a checksum manifest asset`);
77
+ }
78
+
79
+ const checksumManifest = (await fetchBuffer(checksumAsset.browser_download_url)).toString("utf8");
80
+ const checksums = parseChecksumManifest(checksumManifest);
81
+ const expectedSha256 = checksums.get(assetName);
82
+ if (!expectedSha256) {
83
+ throw new Error(`Checksum manifest does not contain ${assetName}`);
84
+ }
85
+
86
+ return {
87
+ assetName,
88
+ assetUrl: asset.browser_download_url,
89
+ expectedSha256,
90
+ };
91
+ }
92
+
93
+ function errorMessage(err) {
94
+ return err && err.message ? err.message : String(err);
95
+ }
96
+
97
+ async function downloadVerifiedArchive(options) {
98
+ const opts = options || {};
99
+ const resolveRelease = opts.resolveReleaseAssetAndChecksum || resolveReleaseAssetAndChecksum;
100
+ const fetchBuffer = opts.fetch || fetch;
101
+ const checksumRequirement =
102
+ "open-plan-annotator requires release checksum/sha256sum availability and will not install without verification.";
103
+
104
+ let releaseInfo;
105
+
106
+ try {
107
+ releaseInfo = await resolveRelease();
108
+ } catch (err) {
109
+ throw new Error(`Unable to verify release checksums: ${errorMessage(err)} ${checksumRequirement}`);
110
+ }
111
+
112
+ const { assetName, assetUrl, expectedSha256 } = releaseInfo;
113
+ const archiveBuffer = await fetchBuffer(assetUrl);
114
+ const actualSha256 = sha256Hex(archiveBuffer);
115
+
116
+ if (actualSha256 !== expectedSha256) {
117
+ throw new Error(
118
+ `Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256}). ${checksumRequirement}`,
119
+ );
120
+ }
121
+
122
+ return archiveBuffer;
123
+ }
124
+
125
+ function extractBinaryFromTarGz(buffer) {
126
+ const tarBuffer = zlib.gunzipSync(buffer);
127
+ let offset = 0;
128
+
129
+ while (offset < tarBuffer.length) {
130
+ const header = tarBuffer.subarray(offset, offset + 512);
131
+ offset += 512;
132
+
133
+ const name = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
134
+ const sizeStr = header.toString("utf-8", 124, 136).replace(/\0.*/g, "").trim();
135
+ const size = parseInt(sizeStr, 8);
136
+
137
+ if (!name || isNaN(size)) break;
138
+
139
+ if (name === "open-plan-annotator" || name.endsWith("/open-plan-annotator")) {
140
+ return tarBuffer.subarray(offset, offset + size);
141
+ }
142
+
143
+ offset += Math.ceil(size / 512) * 512;
144
+ }
145
+
146
+ throw new Error("Binary 'open-plan-annotator' not found in archive");
147
+ }
148
+
149
+ async function main() {
150
+ const destDir = path.join(__dirname, "bin");
151
+ const destPath = path.join(destDir, "open-plan-annotator-binary");
152
+ const tempPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
153
+
154
+ // Skip if binary already exists
155
+ if (fs.existsSync(destPath)) {
156
+ return;
157
+ }
158
+
159
+ console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
160
+ const archiveBuffer = await downloadVerifiedArchive();
161
+
162
+ try {
163
+ const binaryBuffer = extractBinaryFromTarGz(archiveBuffer);
164
+
165
+ if (!fs.existsSync(destDir)) {
166
+ fs.mkdirSync(destDir, { recursive: true });
167
+ }
168
+
169
+ fs.writeFileSync(tempPath, binaryBuffer, { mode: 0o755 });
170
+ fs.renameSync(tempPath, destPath);
171
+ fs.chmodSync(destPath, 0o755);
172
+ console.error(`Installed open-plan-annotator to ${destPath}`);
173
+ } catch (err) {
174
+ try {
175
+ fs.unlinkSync(tempPath);
176
+ } catch {
177
+ // Temp file may not exist
178
+ }
179
+ throw err;
180
+ }
181
+ }
182
+
183
+ function shouldSkipInstall() {
184
+ return Boolean(process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL || process.env.npm_config_dev);
185
+ }
186
+
187
+ function runCli() {
188
+ if (shouldSkipInstall()) {
189
+ process.exit(0);
190
+ }
191
+
192
+ main().catch((err) => {
193
+ console.error("Failed to install open-plan-annotator binary:", err.message);
194
+ console.error("You can try manually running: node", path.join(__dirname, "install.mjs"));
195
+ process.exit(1);
196
+ });
197
+ }
198
+
199
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
200
+ runCli();
201
+ }
202
+
203
+ export {
204
+ VERSION,
205
+ PLATFORM_ASSET_BASENAME_MAP as PLATFORM_MAP,
206
+ getPlatformKey,
207
+ getReleaseApiUrl,
208
+ fetch,
209
+ fetchJson,
210
+ sha256Hex,
211
+ parseChecksumManifest,
212
+ selectChecksumAsset,
213
+ resolveReleaseAssetAndChecksum,
214
+ extractBinaryFromTarGz,
215
+ downloadVerifiedArchive,
216
+ shouldSkipInstall,
217
+ main,
218
+ };
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
 
7
7
  const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
8
8
  const LOCAL_BINARY_PATH = join(PKG_ROOT, "bin", "open-plan-annotator-binary");
9
- const INSTALL_SCRIPT = join(PKG_ROOT, "install.cjs");
9
+ const INSTALL_SCRIPT = join(PKG_ROOT, "install.mjs");
10
10
 
11
11
  /** Resolved path to the binary (may differ from LOCAL_BINARY_PATH if found on PATH). */
12
12
  let BINARY_PATH = LOCAL_BINARY_PATH;
package/opencode/index.js CHANGED
@@ -4,15 +4,44 @@ import { resolveImplementationHandoff } from "./config.js";
4
4
 
5
5
  const PLAN_REVIEW_INSTRUCTIONS = `## Plan Review Workflow
6
6
 
7
- For non-trivial implementation work, create a plan first and call the \`submit_plan\` tool.
8
- The user will review the plan in a browser UI and either approve it or request changes.
9
-
10
- - If the tool returns that the plan was **approved**: immediately begin writing code. Do NOT call \`submit_plan\` again — the plan phase is complete.
11
- - If the tool returns **revision feedback**: revise the plan based on the feedback, then call \`submit_plan\` again with the updated plan.
12
-
13
- Only call \`submit_plan\` once per plan version. After approval, your sole job is to implement what was approved.`;
14
-
15
- const IMPLEMENTATION_PROMPT = "The plan has been approved by the user. Begin implementing it now — write code, create files, and make changes as described in the plan. Do not re-submit or re-review the plan.";
7
+ Track planning/execution using this state enum:
8
+ - \`DISCOVERY\`, \`PLAN_DRAFT\`, \`AWAITING_PLAN_DECISION\`, \`EXECUTION\`, \`DONE\`
9
+
10
+ State transitions:
11
+ - Start in \`DISCOVERY\`.
12
+ - Move to \`PLAN_DRAFT\` only when a plan is required.
13
+ - From \`PLAN_DRAFT\`, call \`submit_plan\` exactly once, then move to \`AWAITING_PLAN_DECISION\`.
14
+ - If user approves plan, set \`plan_status=approved\` and move to \`EXECUTION\`.
15
+ - If user rejects or requests plan changes, set \`plan_status=rejected\` and return to \`PLAN_DRAFT\`.
16
+ - When work is complete, move to \`DONE\`.
17
+
18
+ Required flags:
19
+ - \`plan_status\` in \`{none, submitted, approved, rejected}\`
20
+ - \`explicit_replan\` in \`{true,false}\` (default \`false\`)
21
+ - Set \`explicit_replan=true\` only when user clearly asks to replan (for example: revise/change/new/update plan).
22
+
23
+ Hard rules:
24
+ 1) \`submit_plan\` is allowed only in \`PLAN_DRAFT\`.
25
+ 2) If \`plan_status=approved\`, \`submit_plan\` is forbidden unless \`explicit_replan=true\`.
26
+ 3) Call \`submit_plan\` at most once per plan draft/version. If rejected, revise and submit once for the new draft.
27
+ 4) After approval, treat follow-up user messages as execution refinements by default, not planning triggers.
28
+ 5) On conflict, prioritize the approved plan and execute immediately.
29
+ 6) Do not ask permission to proceed after approval; execute and report progress/results.
30
+ 7) When delegating to subagents, always pass current \`plan_status\` and \`explicit_replan\` values.
31
+ 8) If \`plan_status=approved\` and \`explicit_replan=false\`, subagents must execute and must not call \`submit_plan\`.
32
+
33
+ Tool guard before calling \`submit_plan\`:
34
+ - assert \`state == PLAN_DRAFT\`
35
+ - assert \`plan_status != approved || explicit_replan == true\`
36
+ - if an assertion fails, continue execution without submitting a new plan.`;
37
+
38
+ const IMPLEMENTATION_PROMPT = [
39
+ "Plan review status: plan_status=approved.",
40
+ "State transition: next_state=EXECUTION.",
41
+ "Replan intent: explicit_replan=false unless the user explicitly asks to revise the plan.",
42
+ "Execute the approved plan directly now — write code, create files, and make changes.",
43
+ "Do not call `submit_plan` again unless the user explicitly requests re-planning.",
44
+ ].join(" ");
16
45
 
17
46
  function getErrorMessage(error) {
18
47
  if (error instanceof Error && error.message) {
@@ -133,7 +162,7 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
133
162
  tool: {
134
163
  submit_plan: tool({
135
164
  description:
136
- "Submit a markdown plan for interactive user review. Returns approval status or structured revision feedback.",
165
+ "Submit a markdown plan for interactive user review. Returns a structured result with plan_status, next_state, approved, and feedback fields.",
137
166
 
138
167
  args: {
139
168
  plan: tool.schema.string().describe("The complete implementation plan in markdown format"),
@@ -147,6 +176,13 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
147
176
  cwd: ctx.directory,
148
177
  });
149
178
 
179
+ const basePayload = {
180
+ plan_status: result.approved ? "approved" : "rejected",
181
+ next_state: result.approved ? "EXECUTION" : "PLAN_DRAFT",
182
+ approved: result.approved,
183
+ feedback: result.approved ? null : (result.feedback ?? "Plan changes requested."),
184
+ };
185
+
150
186
  if (result.approved) {
151
187
  const lines = [
152
188
  "Plan approved by the user.",
@@ -167,18 +203,24 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
167
203
  }
168
204
 
169
205
  lines.push("Begin implementing the approved plan now — write code and make changes.");
170
- return lines.join("\n\n");
206
+ return {
207
+ ...basePayload,
208
+ guidance: lines.join("\n\n"),
209
+ };
171
210
  }
172
211
 
173
- return [
174
- "Plan needs revision.",
175
- "",
176
- "## User feedback",
177
- "",
178
- result.feedback ?? "Plan changes requested.",
179
- "",
180
- "Revise the plan using this feedback, then call `submit_plan` again.",
181
- ].join("\n");
212
+ return {
213
+ ...basePayload,
214
+ guidance: [
215
+ "Plan needs revision.",
216
+ "",
217
+ "## User feedback",
218
+ "",
219
+ basePayload.feedback,
220
+ "",
221
+ "Revise the plan using this feedback, then submit the revised draft once via `submit_plan`.",
222
+ ].join("\n"),
223
+ };
182
224
  },
183
225
  }),
184
226
  },
@@ -0,0 +1,56 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ afterEach(() => {
4
+ mock.restore();
5
+ });
6
+
7
+ function createPluginContext() {
8
+ return {
9
+ directory: process.cwd(),
10
+ client: {
11
+ session: {
12
+ messages: async () => ({ data: [] }),
13
+ prompt: async () => ({ data: null }),
14
+ },
15
+ app: {
16
+ agents: async () => ({ data: [] }),
17
+ },
18
+ },
19
+ };
20
+ }
21
+
22
+ describe("submit_plan return schema contract", () => {
23
+ test("approved decision maps to approved execution payload", async () => {
24
+ mock.module("./bridge.js", () => ({
25
+ runPlanReview: async () => ({ approved: true }),
26
+ }));
27
+
28
+ const { OpenPlanAnnotatorPlugin } = await import(`./index.js?approved-${Date.now()}`);
29
+ const plugin = await OpenPlanAnnotatorPlugin(createPluginContext());
30
+
31
+ const result = await plugin.tool.submit_plan.execute({ plan: "# Plan" }, { sessionID: "session-1" });
32
+
33
+ expect(result.plan_status).toBe("approved");
34
+ expect(result.next_state).toBe("EXECUTION");
35
+ expect(result.approved).toBe(true);
36
+ expect(result.feedback).toBeNull();
37
+ });
38
+
39
+ test("rejected decision maps to plan redraft payload with bridge feedback", async () => {
40
+ const bridgeFeedback = "Need to add rollback steps.";
41
+
42
+ mock.module("./bridge.js", () => ({
43
+ runPlanReview: async () => ({ approved: false, feedback: bridgeFeedback }),
44
+ }));
45
+
46
+ const { OpenPlanAnnotatorPlugin } = await import(`./index.js?rejected-${Date.now()}`);
47
+ const plugin = await OpenPlanAnnotatorPlugin(createPluginContext());
48
+
49
+ const result = await plugin.tool.submit_plan.execute({ plan: "# Plan" }, { sessionID: "session-2" });
50
+
51
+ expect(result.plan_status).toBe("rejected");
52
+ expect(result.next_state).toBe("PLAN_DRAFT");
53
+ expect(result.approved).toBe(false);
54
+ expect(result.feedback).toBe(bridgeFeedback);
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",
@@ -19,11 +19,12 @@
19
19
  ],
20
20
  "main": "opencode/index.js",
21
21
  "bin": {
22
- "open-plan-annotator": "bin/open-plan-annotator.cjs"
22
+ "open-plan-annotator": "bin/open-plan-annotator.mjs"
23
23
  },
24
24
  "files": [
25
- "bin/open-plan-annotator.cjs",
26
- "install.cjs",
25
+ "bin/open-plan-annotator.mjs",
26
+ "install.mjs",
27
+ "shared/",
27
28
  ".claude-plugin/",
28
29
  "opencode/",
29
30
  "hooks/",
@@ -31,14 +32,15 @@
31
32
  "README.md"
32
33
  ],
33
34
  "scripts": {
34
- "postinstall": "node install.cjs",
35
+ "postinstall": "node install.mjs",
35
36
  "test": "bun test",
36
37
  "typecheck": "tsgo --noEmit --project ui/tsconfig.json && tsgo --noEmit --project server/tsconfig.json",
37
38
  "build:ui": "cd ui && bun run vite build",
38
- "build:platforms": "node scripts/build-platforms.cjs",
39
- "build": "bun run build:ui && node scripts/build-platforms.cjs",
40
- "tarball": "node scripts/tarball.cjs",
41
- "release": "bun run build && node scripts/tarball.cjs",
39
+ "build:platforms": "node scripts/build-platforms.mjs",
40
+ "build": "bun run build:ui && node scripts/build-platforms.mjs",
41
+ "tarball": "node scripts/tarball.mjs",
42
+ "release": "bun run build && node scripts/tarball.mjs",
43
+ "pack:check": "node scripts/check-package-files.mjs",
42
44
  "dev:ui": "cd ui && bun run vite --port 5173",
43
45
  "dev:server": "NODE_ENV=development bun run server/index.ts",
44
46
  "dev": "NODE_ENV=development bun run server/index.ts & cd ui && bun run vite --port 5173",
@@ -46,16 +48,16 @@
46
48
  "lint:fix": "biome check --write .",
47
49
  "format": "biome format --write .",
48
50
  "do-release": "./scripts/release.sh",
49
- "prepack": "mv CLAUDE.md CLAUDE.dev.md.bak && cp CLAUDE.plugin.md CLAUDE.md",
50
- "postpack": "mv CLAUDE.dev.md.bak CLAUDE.md"
51
+ "prepack": "node scripts/claude-pack-docs.mjs prepack",
52
+ "postpack": "node scripts/claude-pack-docs.mjs postpack"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@biomejs/biome": "^2.4.4",
56
+ "@types/node": "^25.3.0",
54
57
  "@types/bun": "^1.3.9",
55
58
  "@typescript/native-preview": "^7.0.0-dev.20260224.1"
56
59
  },
57
60
  "dependencies": {
58
- "@opencode-ai/plugin": "^1.2.14",
59
- "@types/node": "^25.3.0"
61
+ "@opencode-ai/plugin": "^1.2.14"
60
62
  }
61
63
  }
@@ -0,0 +1,12 @@
1
+ export interface ReleaseAsset {
2
+ name: string;
3
+ browser_download_url: string;
4
+ }
5
+
6
+ export declare const REPO: string;
7
+ export declare const PLATFORM_ASSET_BASENAME_MAP: Record<string, string>;
8
+
9
+ export declare function getPlatformKey(platform?: string, arch?: string): string;
10
+ export declare function getPlatformAssetArchiveName(platformKey?: string): string | null;
11
+ export declare function parseChecksumManifest(manifestText: string): Map<string, string>;
12
+ export declare function selectChecksumAsset(assets: ReleaseAsset[]): ReleaseAsset | null;
@@ -0,0 +1,65 @@
1
+ const REPO = "ndom91/open-plan-annotator";
2
+
3
+ const PLATFORM_ASSET_BASENAME_MAP = {
4
+ "darwin-arm64": "open-plan-annotator-darwin-arm64",
5
+ "darwin-x64": "open-plan-annotator-darwin-x64",
6
+ "linux-x64": "open-plan-annotator-linux-x64",
7
+ "linux-arm64": "open-plan-annotator-linux-arm64",
8
+ };
9
+
10
+ function getPlatformKey(platform = process.platform, arch = process.arch) {
11
+ return `${platform}-${arch}`;
12
+ }
13
+
14
+ function getPlatformAssetArchiveName(platformKey = getPlatformKey()) {
15
+ const assetBaseName = PLATFORM_ASSET_BASENAME_MAP[platformKey];
16
+ if (!assetBaseName) {
17
+ return null;
18
+ }
19
+ return `${assetBaseName}.tar.gz`;
20
+ }
21
+
22
+ function parseChecksumManifest(manifestText) {
23
+ const checksums = new Map();
24
+
25
+ for (const rawLine of manifestText.split(/\r?\n/)) {
26
+ const line = rawLine.trim();
27
+ if (!line || line.startsWith("#")) continue;
28
+
29
+ const bsdStyle = line.match(/^SHA256\s*\(([^)]+)\)\s*=\s*([a-fA-F0-9]{64})$/);
30
+ if (bsdStyle) {
31
+ checksums.set(bsdStyle[1].trim(), bsdStyle[2].toLowerCase());
32
+ continue;
33
+ }
34
+
35
+ const gnuStyle = line.match(/^([a-fA-F0-9]{64})\s+[* ]?(.+)$/);
36
+ if (gnuStyle) {
37
+ checksums.set(gnuStyle[2].trim(), gnuStyle[1].toLowerCase());
38
+ }
39
+ }
40
+
41
+ return checksums;
42
+ }
43
+
44
+ function selectChecksumAsset(assets) {
45
+ const checksumAssets = assets
46
+ .filter((asset) => {
47
+ const lower = asset.name.toLowerCase();
48
+ return (
49
+ (lower.includes("sha256") || lower.includes("checksum")) &&
50
+ (lower.endsWith(".txt") || lower.endsWith(".sha256") || lower.endsWith(".sha256sum") || lower.endsWith(".sha256sums"))
51
+ );
52
+ })
53
+ .sort((a, b) => a.name.localeCompare(b.name));
54
+
55
+ return checksumAssets[0] ?? null;
56
+ }
57
+
58
+ export {
59
+ REPO,
60
+ PLATFORM_ASSET_BASENAME_MAP,
61
+ getPlatformKey,
62
+ getPlatformAssetArchiveName,
63
+ parseChecksumManifest,
64
+ selectChecksumAsset,
65
+ };
package/install.cjs DELETED
@@ -1,231 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Skip postinstall during local development
4
- if (process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL || process.env.npm_config_dev) {
5
- process.exit(0);
6
- }
7
-
8
- const fs = require("fs");
9
- const path = require("path");
10
- const zlib = require("zlib");
11
- const https = require("https");
12
- const crypto = require("crypto");
13
-
14
- const VERSION = require("./package.json").version;
15
- const REPO = "ndom91/open-plan-annotator";
16
-
17
- const PLATFORM_MAP = {
18
- "darwin-arm64": "open-plan-annotator-darwin-arm64",
19
- "darwin-x64": "open-plan-annotator-darwin-x64",
20
- "linux-x64": "open-plan-annotator-linux-x64",
21
- "linux-arm64": "open-plan-annotator-linux-arm64",
22
- };
23
-
24
- function getPlatformKey() {
25
- return `${process.platform}-${process.arch}`;
26
- }
27
-
28
- function getDownloadUrl() {
29
- const key = getPlatformKey();
30
- const asset = PLATFORM_MAP[key];
31
- if (!asset) {
32
- console.error(`open-plan-annotator: unsupported platform ${key}`);
33
- console.error(`Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`);
34
- process.exit(1);
35
- }
36
- return `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}.tar.gz`;
37
- }
38
-
39
- function getReleaseApiUrl() {
40
- return `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
41
- }
42
-
43
- function fetch(url, redirects) {
44
- if (redirects === undefined) redirects = 5;
45
- return new Promise((resolve, reject) => {
46
- https
47
- .get(url, { headers: { "User-Agent": "open-plan-annotator-install" } }, (res) => {
48
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
- if (redirects <= 0) return reject(new Error("Too many redirects"));
50
- return fetch(res.headers.location, redirects - 1).then(resolve, reject);
51
- }
52
- if (res.statusCode < 200 || res.statusCode >= 300) {
53
- return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
54
- }
55
- const chunks = [];
56
- res.on("data", (c) => chunks.push(c));
57
- res.on("end", () => resolve(Buffer.concat(chunks)));
58
- })
59
- .on("error", reject);
60
- });
61
- }
62
-
63
- async function fetchJson(url) {
64
- const buffer = await fetch(url);
65
- return JSON.parse(buffer.toString("utf8"));
66
- }
67
-
68
- function sha256Hex(buffer) {
69
- return crypto.createHash("sha256").update(buffer).digest("hex");
70
- }
71
-
72
- function parseChecksumManifest(manifestText) {
73
- const checksums = new Map();
74
-
75
- for (const rawLine of manifestText.split(/\r?\n/)) {
76
- const line = rawLine.trim();
77
- if (!line || line.startsWith("#")) continue;
78
-
79
- const bsdStyle = line.match(/^SHA256\s*\(([^)]+)\)\s*=\s*([a-fA-F0-9]{64})$/);
80
- if (bsdStyle) {
81
- checksums.set(bsdStyle[1].trim(), bsdStyle[2].toLowerCase());
82
- continue;
83
- }
84
-
85
- const gnuStyle = line.match(/^([a-fA-F0-9]{64})\s+[* ]?(.+)$/);
86
- if (gnuStyle) {
87
- checksums.set(gnuStyle[2].trim(), gnuStyle[1].toLowerCase());
88
- }
89
- }
90
-
91
- return checksums;
92
- }
93
-
94
- function selectChecksumAsset(assets) {
95
- const checksumAssets = assets
96
- .filter((asset) => {
97
- const lower = asset.name.toLowerCase();
98
- return (
99
- (lower.includes("sha256") || lower.includes("checksum")) &&
100
- (lower.endsWith(".txt") || lower.endsWith(".sha256") || lower.endsWith(".sha256sum") || lower.endsWith(".sha256sums"))
101
- );
102
- })
103
- .sort((a, b) => a.name.localeCompare(b.name));
104
-
105
- return checksumAssets[0] || null;
106
- }
107
-
108
- async function resolveReleaseAssetAndChecksum() {
109
- const release = await fetchJson(getReleaseApiUrl());
110
- const releaseAssets = Array.isArray(release.assets) ? release.assets : [];
111
- const key = getPlatformKey();
112
- const assetBaseName = PLATFORM_MAP[key];
113
- if (!assetBaseName) {
114
- throw new Error(`Unsupported platform ${key}`);
115
- }
116
-
117
- const assetName = `${assetBaseName}.tar.gz`;
118
- const asset = releaseAssets.find((entry) => entry.name === assetName);
119
- if (!asset) {
120
- throw new Error(`Release v${VERSION} is missing asset ${assetName}`);
121
- }
122
-
123
- const checksumAsset = selectChecksumAsset(releaseAssets);
124
- if (!checksumAsset) {
125
- throw new Error(`Release v${VERSION} does not contain a checksum manifest asset`);
126
- }
127
-
128
- const checksumManifest = (await fetch(checksumAsset.browser_download_url)).toString("utf8");
129
- const checksums = parseChecksumManifest(checksumManifest);
130
- const expectedSha256 = checksums.get(assetName);
131
- if (!expectedSha256) {
132
- throw new Error(`Checksum manifest does not contain ${assetName}`);
133
- }
134
-
135
- return {
136
- assetName,
137
- assetUrl: asset.browser_download_url,
138
- expectedSha256,
139
- };
140
- }
141
-
142
- function extractBinaryFromTarGz(buffer) {
143
- const tarBuffer = zlib.gunzipSync(buffer);
144
- let offset = 0;
145
-
146
- while (offset < tarBuffer.length) {
147
- const header = tarBuffer.subarray(offset, offset + 512);
148
- offset += 512;
149
-
150
- const name = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
151
- const sizeStr = header.toString("utf-8", 124, 136).replace(/\0.*/g, "").trim();
152
- const size = parseInt(sizeStr, 8);
153
-
154
- if (!name || isNaN(size)) break;
155
-
156
- if (name === "open-plan-annotator" || name.endsWith("/open-plan-annotator")) {
157
- return tarBuffer.subarray(offset, offset + size);
158
- }
159
-
160
- offset += Math.ceil(size / 512) * 512;
161
- }
162
-
163
- throw new Error("Binary 'open-plan-annotator' not found in archive");
164
- }
165
-
166
- async function main() {
167
- const destDir = path.join(__dirname, "bin");
168
- const destPath = path.join(destDir, "open-plan-annotator-binary");
169
- const tempPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
170
-
171
- // Skip if binary already exists
172
- if (fs.existsSync(destPath)) {
173
- return;
174
- }
175
-
176
- const fallbackUrl = getDownloadUrl();
177
- console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
178
-
179
- let archiveBuffer;
180
-
181
- // Try checksum-verified download via GitHub API first, fall back to direct URL
182
- try {
183
- const { assetName, assetUrl, expectedSha256 } = await resolveReleaseAssetAndChecksum();
184
- archiveBuffer = await fetch(assetUrl);
185
- const actualSha256 = sha256Hex(archiveBuffer);
186
-
187
- if (actualSha256 !== expectedSha256) {
188
- throw new Error(
189
- `Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256})`,
190
- );
191
- }
192
- } catch (verifiedErr) {
193
- const message = verifiedErr && verifiedErr.message ? verifiedErr.message : String(verifiedErr);
194
- console.error(`open-plan-annotator: checksum-verified install failed: ${message}`);
195
- console.error(`Falling back to direct download (without checksum verification)...`);
196
-
197
- try {
198
- archiveBuffer = await fetch(fallbackUrl);
199
- } catch (fallbackErr) {
200
- const fbMsg = fallbackErr && fallbackErr.message ? fallbackErr.message : String(fallbackErr);
201
- console.error(`open-plan-annotator: fallback download also failed: ${fbMsg}`);
202
- throw verifiedErr;
203
- }
204
- }
205
-
206
- try {
207
- const binaryBuffer = extractBinaryFromTarGz(archiveBuffer);
208
-
209
- if (!fs.existsSync(destDir)) {
210
- fs.mkdirSync(destDir, { recursive: true });
211
- }
212
-
213
- fs.writeFileSync(tempPath, binaryBuffer, { mode: 0o755 });
214
- fs.renameSync(tempPath, destPath);
215
- fs.chmodSync(destPath, 0o755);
216
- console.error(`Installed open-plan-annotator to ${destPath}`);
217
- } catch (err) {
218
- try {
219
- fs.unlinkSync(tempPath);
220
- } catch {
221
- // Temp file may not exist
222
- }
223
- throw err;
224
- }
225
- }
226
-
227
- main().catch((err) => {
228
- console.error("Failed to install open-plan-annotator binary:", err.message);
229
- console.error("You can try manually running: node", path.join(__dirname, "install.cjs"));
230
- process.exit(1);
231
- });