open-plan-annotator 1.0.11 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/bin/{open-plan-annotator.cjs → open-plan-annotator.mjs} +7 -5
- package/install.mjs +218 -0
- package/opencode/bridge.js +1 -1
- package/package.json +15 -13
- package/shared/releaseAssets.d.ts +12 -0
- package/shared/releaseAssets.mjs +65 -0
- package/install.cjs +0 -231
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Interactive plan annotation plugin for Claude Code",
|
|
8
|
-
"version": "1.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.
|
|
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.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
9
|
-
const 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
|
+
};
|
package/opencode/bridge.js
CHANGED
|
@@ -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.
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-plan-annotator",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
22
|
+
"open-plan-annotator": "bin/open-plan-annotator.mjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"bin/open-plan-annotator.
|
|
26
|
-
"install.
|
|
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.
|
|
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.
|
|
39
|
-
"build": "bun run build:ui && node scripts/build-platforms.
|
|
40
|
-
"tarball": "node scripts/tarball.
|
|
41
|
-
"release": "bun run build && node scripts/tarball.
|
|
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": "
|
|
50
|
-
"postpack": "
|
|
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
|
-
});
|