capix-code 2.2.3 → 2.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capix-code",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "Capix Code — decentralized AI coding agent with GPU marketplace",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "bin/",
12
- "scripts/",
12
+ "scripts/postinstall.cjs",
13
13
  "dist/customer/config/",
14
14
  "README.md",
15
15
  "LICENSE"
@@ -22,12 +22,14 @@
22
22
  "lint": "eslint src/",
23
23
  "format": "prettier --write .",
24
24
  "format:check": "prettier --check .",
25
+ "pretest": "node scripts/prepare-native-addons.mjs node_modules",
25
26
  "test": "vitest run",
26
27
  "test:watch": "vitest",
27
28
  "test:coverage": "vitest run --coverage",
28
29
  "manifest:validate": "node scripts/validate-manifest.mjs manifest/release-manifest.json",
29
30
  "manifest:build": "node scripts/build-manifest.mjs",
30
31
  "resolve-version": "node scripts/resolve-version.mjs",
32
+ "release:check": "node scripts/check-release-consistency.mjs",
31
33
  "prepack": "node scripts/prepare-npm-meta.mjs",
32
34
  "pack:npm-platform": "node scripts/prepare-npm-platform.mjs",
33
35
  "postinstall": "node scripts/postinstall.cjs"
@@ -35,12 +37,15 @@
35
37
  "devDependencies": {
36
38
  "@ai-sdk/provider": "3.0.8",
37
39
  "@capix/runtime-provider": "file:packages/runtime-provider",
40
+ "@capix/agent-runtime": "file:packages/agent-runtime",
38
41
  "@commitlint/config-conventional": "^21.2.0",
39
42
  "@eslint/js": "^9.0.0",
40
43
  "@opencode-ai/plugin": "1.17.18",
41
44
  "@opencode-ai/sdk": "1.17.18",
45
+ "@types/better-sqlite3": "^7.6.13",
42
46
  "@types/node": "^22.0.0",
43
47
  "@vitest/coverage-v8": "^2.1.0",
48
+ "better-sqlite3": "^12.11.1",
44
49
  "bun": "^1.3.14",
45
50
  "commitlint": "^21.2.1",
46
51
  "eslint": "^9.0.0",
@@ -1,63 +1,119 @@
1
1
  #!/usr/bin/env node
2
- const { execSync } = require('child_process');
2
+ const { execFileSync } = require('child_process');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const crypto = require('crypto');
7
- if (process.env.CI || process.env.GITHUB_ACTIONS) { process.exit(0); }
7
+ if (process.env.CI || process.env.GITHUB_ACTIONS) {
8
+ process.exit(0);
9
+ }
8
10
  const VERSION = require('../package.json').version;
9
11
  const plat = process.platform === 'win32' ? 'win32' : process.platform;
10
12
  const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
11
13
  const ext = process.platform === 'win32' ? 'zip' : 'tar.gz';
12
14
  const NAME = `capix-code-${VERSION}-${plat}-${arch}-unsigned`;
13
- const URL = `https://github.com/CapIX-Protocol/Capix-Code/releases/download/v${VERSION}/${NAME}.${ext}`;
15
+ const URL = `https://github.com/CapIX-Protocol/CapIX-Code/releases/download/v${VERSION}/${NAME}.${ext}`;
14
16
  const CHECKSUM_URL = `${URL}.sha256`;
15
17
  const ROOT = path.join(os.homedir(), '.capix-code');
16
- const TMP = path.join(os.tmpdir(), `capix-install-${VERSION}`);
18
+ const TMP = path.join(os.tmpdir(), `capix-install-${VERSION}-${process.pid}`);
17
19
  const archivePath = path.join(TMP, `${NAME}.${ext}`);
18
- fs.mkdirSync(ROOT, { recursive: true });
20
+ fs.rmSync(TMP, { recursive: true, force: true });
19
21
  fs.mkdirSync(TMP, { recursive: true });
20
22
  console.log(`Capix Code v${VERSION} (${plat}-${arch})`);
21
- execSync(`curl -fsSL -o "${archivePath}" "${URL}"`, { stdio: 'inherit' });
23
+ execFileSync('curl', ['-fsSL', '-o', archivePath, URL], { stdio: 'inherit' });
22
24
  const checksumPath = path.join(TMP, 'checksum.sha256');
23
- execSync(`curl -fsSL -o "${checksumPath}" "${CHECKSUM_URL}"`, { stdio: 'inherit' });
25
+ execFileSync('curl', ['-fsSL', '-o', checksumPath, CHECKSUM_URL], { stdio: 'inherit' });
24
26
  const expected = fs.readFileSync(checksumPath, 'utf8').trim().split(/\s+/)[0];
25
27
  const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
26
- if (expected !== actual) { console.error(`Checksum mismatch!`); process.exit(1); }
27
- if (process.platform === 'win32') { execSync(`tar -xf "${archivePath}" -C "${TMP}"`, { stdio: 'inherit' }); }
28
- else { execSync(`tar -xzf "${archivePath}" -C "${TMP}"`, { stdio: 'inherit' }); }
28
+ if (expected !== actual) {
29
+ console.error(`Checksum mismatch!`);
30
+ process.exit(1);
31
+ }
32
+ if (process.platform === 'win32') {
33
+ execFileSync('tar', ['-xf', archivePath, '-C', TMP], { stdio: 'inherit' });
34
+ } else {
35
+ execFileSync('tar', ['-xzf', archivePath, '-C', TMP], { stdio: 'inherit' });
36
+ }
29
37
  const src = path.join(TMP, 'customer');
30
- if (!fs.existsSync(src)) { console.error('Missing customer/'); process.exit(1); }
38
+ if (!fs.existsSync(src)) {
39
+ console.error('Missing customer/');
40
+ process.exit(1);
41
+ }
31
42
  const backup = ROOT + '.bak';
32
- if (fs.existsSync(backup)) fs.rmSync(backup, { recursive: true });
33
- if (fs.existsSync(ROOT)) fs.renameSync(ROOT, backup);
34
- fs.mkdirSync(ROOT, { recursive: true });
35
- for (const dir of ['bin', 'engine', 'runtime', 'config', 'mcp']) {
43
+ const next = `${ROOT}.next-${process.pid}`;
44
+ fs.rmSync(next, { recursive: true, force: true });
45
+ fs.mkdirSync(next, { recursive: true });
46
+ for (const dir of ['bin', 'engine', 'runtime', 'config', 'mcp', 'commands']) {
36
47
  const s = path.join(src, dir);
37
- if (fs.existsSync(s)) execSync(`cp -a "${s}" "${path.join(ROOT, dir)}"`, { stdio: 'inherit' });
48
+ if (fs.existsSync(s)) fs.cpSync(s, path.join(next, dir), { recursive: true });
49
+ }
50
+ const executableSuffix = process.platform === 'win32' ? '.exe' : '';
51
+ const nextLauncher = path.join(next, 'bin', `capix-code${executableSuffix}`);
52
+ const nextEngine = path.join(next, 'engine', `capix-engine${executableSuffix}`);
53
+ if (!fs.existsSync(nextLauncher) || !fs.existsSync(nextEngine)) {
54
+ console.error('The Capix Code release is missing its native launcher or engine.');
55
+ fs.rmSync(next, { recursive: true, force: true });
56
+ process.exit(1);
57
+ }
58
+ if (process.platform !== 'win32') {
59
+ fs.chmodSync(nextLauncher, 0o755);
60
+ fs.chmodSync(nextEngine, 0o755);
38
61
  }
39
- fs.chmodSync(path.join(ROOT, 'bin', 'capix-code'), 0o755);
40
- console.log('Installing runtime deps...');
41
- execSync('npm install --omit=dev --ignore-scripts', { cwd: path.join(ROOT, 'runtime'), stdio: 'inherit' });
42
- const mcpDir = path.join(ROOT, 'mcp');
62
+ const mcpDir = path.join(next, 'mcp');
43
63
  if (!fs.existsSync(path.join(mcpDir, 'node_modules', '@modelcontextprotocol'))) {
44
- console.log('Installing MCP deps...');
45
- execSync('npm install capix-mcp@2.1.0', { cwd: mcpDir, stdio: 'inherit' });
46
- fs.writeFileSync(path.join(mcpDir, 'capix-mcp.js'), '#!/usr/bin/env node\nconst{join}=require("node:path");const{homedir}=require("node:os");require(join(homedir(),".capix-code","mcp","node_modules","capix-mcp","dist","index.js"));\n');
47
- fs.chmodSync(path.join(mcpDir, 'capix-mcp.js'), 0o755);
64
+ console.error('The Capix Code release is missing its bundled MCP runtime.');
65
+ fs.rmSync(next, { recursive: true, force: true });
66
+ process.exit(1);
48
67
  }
49
- const cfgDir = path.join(os.homedir(), '.config', 'opencode');
68
+ fs.rmSync(backup, { recursive: true, force: true });
69
+ if (fs.existsSync(ROOT)) fs.renameSync(ROOT, backup);
70
+ try {
71
+ fs.renameSync(next, ROOT);
72
+ } catch (error) {
73
+ if (fs.existsSync(backup) && !fs.existsSync(ROOT)) fs.renameSync(backup, ROOT);
74
+ throw error;
75
+ }
76
+ const launcher = path.join(ROOT, 'bin', `capix-code${executableSuffix}`);
77
+ const engine = path.join(ROOT, 'engine', `capix-engine${executableSuffix}`);
78
+ const cfgDir = path.join(os.homedir(), '.config', 'capix-code');
50
79
  fs.mkdirSync(cfgDir, { recursive: true });
51
80
  const rt = path.join(ROOT, 'runtime');
52
81
  const pu = `file://${rt}/packages/runtime-provider/src/index.ts`;
53
- fs.writeFileSync(path.join(cfgDir, 'capix-code.json'), JSON.stringify({
54
- model: 'capix/auto', enabled_providers: ['capix'],
55
- plugin: [path.join(rt, 'src', 'native-bridge.ts'), path.join(rt, 'src', 'plugin.ts')],
56
- provider: { capix: { npm: pu, name: 'Capix', models: { auto: { name: 'Capix Auto', limit: { context: 128000, output: 64000 }, api: { url: 'https://www.capix.network/api/v1', npm: pu } } } } }
57
- }, null, 2));
82
+ const configPath = path.join(cfgDir, 'capix-code.json');
83
+ if (!fs.existsSync(configPath)) {
84
+ fs.writeFileSync(
85
+ configPath,
86
+ JSON.stringify(
87
+ {
88
+ model: 'capix/auto',
89
+ enabled_providers: ['capix'],
90
+ plugin: [path.join(rt, 'src', 'native-bridge.ts'), path.join(rt, 'src', 'plugin.ts')],
91
+ provider: {
92
+ capix: {
93
+ npm: pu,
94
+ name: 'Capix',
95
+ models: {
96
+ auto: {
97
+ name: 'Capix Auto',
98
+ limit: { context: 128000, output: 64000 },
99
+ api: { url: 'https://www.capix.network/api/v1', npm: pu },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ null,
106
+ 2
107
+ )
108
+ );
109
+ }
58
110
  if (process.platform === 'darwin') {
59
- try { execSync(`codesign --force --sign - "${path.join(ROOT, 'bin', 'capix-code')}"`, { stdio: 'ignore' }); } catch {}
60
- try { execSync(`codesign --force --sign - "${path.join(ROOT, 'engine', 'capix-engine')}"`, { stdio: 'ignore' }); } catch {}
111
+ try {
112
+ execFileSync('codesign', ['--force', '--sign', '-', launcher], { stdio: 'ignore' });
113
+ } catch {}
114
+ try {
115
+ execFileSync('codesign', ['--force', '--sign', '-', engine], { stdio: 'ignore' });
116
+ } catch {}
61
117
  }
62
118
  fs.rmSync(TMP, { recursive: true, force: true });
63
119
  if (fs.existsSync(backup)) fs.rmSync(backup, { recursive: true });
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- ROOT="${1:?artifact directory required}"
4
- SUFFIX=""
5
- test -f "$ROOT/bin/capix-code.exe" && SUFFIX=".exe"
6
- required=(
7
- "bin/capix-code$SUFFIX"
8
- "engine/capix-engine$SUFFIX"
9
- runtime/src/plugin.ts
10
- runtime/src/native-bridge.ts
11
- runtime/src/broker.ts
12
- runtime/src/sandbox.ts
13
- runtime/src/capix-provider.ts
14
- runtime/src/ai-sdk-provider.ts
15
- runtime/packages/runtime-provider/package.json
16
- runtime/node_modules/@capix/runtime-provider/package.json
17
- config/capix-defaults.json
18
- config/defaults.json
19
- mcp/capix-mcp.js
20
- )
21
- for path in "${required[@]}"; do
22
- test -e "$ROOT/$path" || { echo "✗ artifact missing $path"; exit 1; }
23
- done
24
- test -x "$ROOT/engine/capix-engine$SUFFIX" || { echo "✗ bundled engine is not executable"; exit 1; }
25
- test -x "$ROOT/bin/capix-code$SUFFIX" || { echo "✗ native launcher is not executable"; exit 1; }
26
- grep -q '"name": "@capix/runtime-provider"' "$ROOT/runtime/packages/runtime-provider/package.json"
27
- grep -q '"model": "{env:CAPIX_MODEL:capix/auto}"' "$ROOT/config/defaults.json"
28
- grep -q 'SuperGemma' "$ROOT/config/defaults.json"
29
- grep -q '/oauth/authorize' "$ROOT/runtime/src/broker.ts"
30
- grep -q '/oauth/token' "$ROOT/runtime/src/broker.ts"
31
- echo "✓ artifact contains pinned engine, plugin, broker, sandbox and runtime provider"
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- ROOT="${1:?artifact directory required}"
4
- FORBIDDEN='opencode|vast|hetzner|void|vscode'
5
-
6
- if find "$ROOT" -path "$ROOT/runtime/node_modules" -prune -o -type f -print | sed "s|$ROOT/||" | grep -Eiq "$FORBIDDEN"; then
7
- echo "✗ customer artifact contains a forbidden filename"
8
- exit 1
9
- fi
10
-
11
- for command in "--help" "--version" "doctor"; do
12
- output="$($ROOT/bin/capix-code $command 2>&1)"
13
- if printf '%s' "$output" | grep -Eiq "$FORBIDDEN"; then
14
- echo "✗ forbidden customer-visible brand in: capix-code $command"
15
- exit 1
16
- fi
17
- done
18
-
19
- # The embedded engine is not invoked directly by customers, but every help
20
- # surface it can render through the launcher must still be Capix-only.
21
- for args in "--help" "run --help"; do
22
- output="$($ROOT/engine/capix-engine $args 2>&1)"
23
- if printf '%s' "$output" | grep -Eiq "$FORBIDDEN"; then
24
- echo "✗ forbidden predecessor brand in embedded engine output: $args"
25
- exit 1
26
- fi
27
- done
28
-
29
- # Native account commands must never fall through to the bundled engine's
30
- # command parser. An unsigned CI artifact has no user keychain credential, so
31
- # the expected result is a clean Capix-only sign-in instruction.
32
- set +e
33
- status_output="$($ROOT/bin/capix-code status 2>&1)"
34
- status_code=$?
35
- set -e
36
- if [ "$status_code" -eq 0 ]; then
37
- # A developer machine may already have a valid native keychain session.
38
- printf '%s' "$status_output" | grep -Fq '"accountId"' || {
39
- echo "✗ authenticated status did not return canonical Capix account data"; exit 1;
40
- }
41
- else
42
- printf '%s' "$status_output" | grep -Fq 'capix-code login' || {
43
- echo "✗ status did not return the native Capix authentication instruction"; exit 1;
44
- }
45
- fi
46
- if printf '%s' "$status_output" | grep -Eiq "$FORBIDDEN"; then
47
- echo "✗ status leaked a forbidden inherited brand"; exit 1
48
- fi
49
-
50
- if grep -ERiq "$FORBIDDEN" "$ROOT/config"; then
51
- echo "✗ forbidden customer-visible brand in configuration"
52
- exit 1
53
- fi
54
- echo "✓ customer-visible artifact surfaces use Capix branding only"
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- ROOT="${1:?prepared engine source required}"
4
- # Internal import/package symbols intentionally retain their upstream ABI.
5
- # This expression targets customer-readable identity and command prose only.
6
- FORBIDDEN='OpenCode|OC \||run opencode|close opencode|Starting opencode|opencode -s|opencode (models|auth|does|version|server|with|to|upgrade|mcp|session)|Thank you[^\n]*opencode'
7
- SURFACES=(
8
- packages/tui/src/logo.ts
9
- packages/tui/src/app.tsx
10
- packages/tui/src/util/presentation.ts
11
- packages/capix-code/src/cli/ui.ts
12
- packages/capix-code/src/index.ts
13
- packages/capix-code/src/cli/error.ts
14
- packages/capix-code/src/cli/cmd/run.ts
15
- packages/capix-code/src/cli/cmd/run/splash.ts
16
- packages/capix-code/src/cli/cmd/run/footer.permission.tsx
17
- packages/capix-code/src/cli/cmd/run/footer.prompt.tsx
18
- packages/capix-code/src/cli/cmd/run/permission.shared.ts
19
- packages/capix-code/src/provider/error.ts
20
- packages/capix-code/src/cli/cmd/attach.ts
21
- packages/capix-code/src/cli/cmd/upgrade.ts
22
- packages/capix-code/src/cli/cmd/uninstall.ts
23
- packages/capix-code/src/cli/cmd/serve.ts
24
- packages/capix-code/src/cli/cmd/web.ts
25
- packages/capix-code/src/cli/cmd/pr.ts
26
- packages/capix-code/src/cli/cmd/tui.ts
27
- packages/capix-code/src/cli/network.ts
28
- )
29
- for file in "${SURFACES[@]}"; do
30
- test -f "$ROOT/$file" || { echo "✗ missing customer presentation source: $file"; exit 1; }
31
- if grep -Eq "$FORBIDDEN" "$ROOT/$file"; then
32
- echo "✗ predecessor branding remains in customer presentation source: $file"
33
- grep -En "$FORBIDDEN" "$ROOT/$file" | head -n 10
34
- exit 1
35
- fi
36
- done
37
- grep -Fq 'CAPIX CODE' "$ROOT/packages/capix-code/src/cli/ui.ts" || {
38
- echo "✗ Capix Code startup wordmark is missing"
39
- exit 1
40
- }
41
- grep -Fq 'export const identity' "$ROOT/packages/tui/src/logo.ts" &&
42
- grep -Fq 'CAPIX CODE' "$ROOT/packages/tui/src/logo.ts" || {
43
- echo "✗ Capix Code TUI identity is missing"
44
- exit 1
45
- }
46
- echo "✓ prepared engine customer presentation sources are Capix-only"
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env bash
2
- # bootstrap.sh — clone upstream source and prepare capix-code.
3
- set -euo pipefail
4
-
5
- DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
- CAPIX_CODE_DIR="${CAPIX_CODE_DIR:-$DIR/upstream}"
7
-
8
- # TODO: Update this SHA when upgrading upstream
9
- CAPIX_CODE_REF="${CAPIX_CODE_REF:-9976269ab1accfc9f9dc98a4a688c516934de422}"
10
-
11
- if [ -d "$CAPIX_CODE_DIR/.git" ]; then
12
- echo "✓ $CAPIX_CODE_DIR already cloned."
13
- else
14
- echo "▸ Cloning upstream source into $CAPIX_CODE_DIR (SHA: $CAPIX_CODE_REF)…"
15
- git clone https://github.com/anomalyco/opencode.git "$CAPIX_CODE_DIR"
16
- git -C "$CAPIX_CODE_DIR" checkout "$CAPIX_CODE_REF"
17
- fi
18
-
19
- ACTUAL_REF="$(git -C "$CAPIX_CODE_DIR" rev-parse HEAD)"
20
- if [ "$ACTUAL_REF" != "$CAPIX_CODE_REF" ]; then
21
- echo "✗ Source checkout is at $ACTUAL_REF; expected pinned ref $CAPIX_CODE_REF"
22
- echo " Use a new CAPIX_CODE_DIR or restore the pinned checkout before continuing."
23
- exit 1
24
- fi
25
-
26
- # A prepared checkout is intentionally changed by scripts/rebrand.sh. Treat the
27
- # expected renamed package as an idempotent success, while still rejecting an
28
- # unrelated dirty checkout before the first rebrand.
29
- if [ -d "$CAPIX_CODE_DIR/packages/capix-code" ]; then
30
- echo "✓ Pinned Capix Code source already prepared: $ACTUAL_REF"
31
- exit 0
32
- fi
33
-
34
- git -C "$CAPIX_CODE_DIR" diff --quiet || {
35
- echo "✗ Source tree has unstaged changes before Capix preparation"
36
- exit 1
37
- }
38
- echo "✓ Pinned source ready: $ACTUAL_REF"
@@ -1,219 +0,0 @@
1
- /**
2
- * Build (materialize) manifest/release-manifest.json from real per-platform
3
- * release entries produced by write-release-entry.mjs.
4
- *
5
- * A publishable manifest is assembled ONLY from real artifact metadata — it
6
- * never copies through TEMPLATE placeholders. If a required platform entry is
7
- * missing or carries invalid metadata, the build fails closed.
8
- *
9
- * Usage:
10
- * node scripts/build-manifest.mjs \
11
- * --entries release-artifacts \
12
- * --base-url https://github.com/CapIX-Protocol/Capix-Code/releases/download \
13
- * --stable-version v1.2.4 \
14
- * --source-sha <40-hex> \
15
- * --launcher-version 1.2.4 --opencode-version 1.17.18 \
16
- * --plugin-version 1.2.4 --provider-version 1.2.4 \
17
- * [--out manifest/release-manifest.json] [--require-all]
18
- *
19
- * The stable version is the single source of truth consumed by the installer's
20
- * `latest` resolver and by validate-manifest.
21
- */
22
- import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
23
- import { basename, join, resolve } from 'node:path';
24
- import { pathToFileURL } from 'node:url';
25
-
26
- const REQUIRED_PLATFORMS = ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64'];
27
- const SEMVER_TAG = /^v\d+\.\d+\.\d+$/;
28
- const SHA1 = /^[0-9a-f]{40}$/;
29
- const SHA256 = /^[0-9a-f]{64}$/;
30
- const SEMVER = /^\d+\.\d+\.\d+$/;
31
-
32
- function parseArgs(argv) {
33
- const args = {
34
- entries: 'release-artifacts',
35
- out: 'manifest/release-manifest.json',
36
- requireAll: false,
37
- };
38
- for (let i = 0; i < argv.length; i++) {
39
- const a = argv[i];
40
- switch (a) {
41
- case '--entries':
42
- args.entries = argv[++i];
43
- break;
44
- case '--base-url':
45
- args.baseUrl = argv[++i];
46
- break;
47
- case '--stable-version':
48
- args.stableVersion = argv[++i];
49
- break;
50
- case '--source-sha':
51
- args.sourceSha = argv[++i];
52
- break;
53
- case '--launcher-version':
54
- args.launcherVersion = argv[++i];
55
- break;
56
- case '--opencode-version':
57
- args.opencodeVersion = argv[++i];
58
- break;
59
- case '--plugin-version':
60
- args.pluginVersion = argv[++i];
61
- break;
62
- case '--provider-version':
63
- args.providerVersion = argv[++i];
64
- break;
65
- case '--out':
66
- args.out = argv[++i];
67
- break;
68
- case '--require-all':
69
- args.requireAll = true;
70
- break;
71
- default:
72
- throw new Error(`unknown argument: ${a}`);
73
- }
74
- }
75
- return args;
76
- }
77
-
78
- function requireArg(args, name) {
79
- const v = args[name];
80
- if (!v)
81
- throw new Error(
82
- `${name} is required (pass --${name.replace(/([A-Z])/g, '-$1').toLowerCase()})`
83
- );
84
- return v;
85
- }
86
-
87
- /** Read every capix-code-*-{platform}-{arch}.release.json in the entries dir. */
88
- export function readReleaseEntries(entriesDir) {
89
- const files = readdirSync(entriesDir).filter((f) => /\.release\.json$/i.test(f));
90
- const entries = new Map();
91
- for (const f of files) {
92
- let parsed;
93
- try {
94
- parsed = JSON.parse(readFileSync(join(entriesDir, f), 'utf8'));
95
- } catch (err) {
96
- throw new Error(`cannot parse ${f}: ${err.message}`);
97
- }
98
- if (parsed.product !== 'capix-code') throw new Error(`${f}: product is not capix-code`);
99
- const platform = parsed.platform;
100
- if (!platform || !REQUIRED_PLATFORMS.includes(platform)) {
101
- throw new Error(`${f}: unrecognized platform "${platform}"`);
102
- }
103
- if (entries.has(platform))
104
- throw new Error(`duplicate release entry for platform ${platform} (${f})`);
105
- entries.set(platform, parsed);
106
- }
107
- return entries;
108
- }
109
-
110
- /**
111
- * Build the manifest object from a map of platform -> release entry.
112
- * Pure function — testable without touching the filesystem.
113
- */
114
- export function buildManifestFromEntries(entries, opts) {
115
- const {
116
- baseUrl,
117
- stableVersion,
118
- sourceSha,
119
- launcherVersion,
120
- opencodeVersion,
121
- pluginVersion,
122
- providerVersion,
123
- } = opts;
124
- const errors = [];
125
- if (!SEMVER_TAG.test(stableVersion ?? ''))
126
- errors.push('--stable-version must be vMAJOR.MINOR.PATCH');
127
- if (!SHA1.test(sourceSha ?? ''))
128
- errors.push('--source-sha must be a 40-character lowercase hex commit SHA');
129
- for (const [name, v] of [
130
- ['launcher', launcherVersion],
131
- ['opencode', opencodeVersion],
132
- ['plugin', pluginVersion],
133
- ['provider', providerVersion],
134
- ]) {
135
- if (!SEMVER.test(v ?? '')) errors.push(`--${name}-version must be MAJOR.MINOR.PATCH`);
136
- }
137
- if (!baseUrl || !/^https:\/\//.test(baseUrl)) errors.push('--base-url must be an https URL');
138
-
139
- const present = [...entries.keys()];
140
- if (opts.requireAll) {
141
- for (const p of REQUIRED_PLATFORMS)
142
- if (!entries.has(p)) errors.push(`missing required platform entry: ${p}`);
143
- }
144
- if (errors.length) throw new Error(errors.join('; '));
145
-
146
- const platforms = {};
147
- for (const p of REQUIRED_PLATFORMS) {
148
- const e = entries.get(p);
149
- if (!e) continue; // when not --require-all, only publish what exists
150
- if (!SHA256.test(e.sha256 ?? ''))
151
- throw new Error(`${p}: release entry sha256 is not a valid 64-hex digest`);
152
- if (typeof e.sizeBytes !== 'number' || e.sizeBytes <= 0)
153
- throw new Error(`${p}: release entry sizeBytes must be positive`);
154
- const tag = stableVersion;
155
- platforms[p] = {
156
- url: `${baseUrl}/${tag}/${e.artifact}`,
157
- sha256: e.sha256,
158
- // Unsigned artifacts ship a null signatureUrl; strict signing is a
159
- // separate, later gate. Never TEMPLATE.
160
- signatureUrl: e.signed ? `${baseUrl}/${tag}/${e.artifact}.sig` : null,
161
- sizeBytes: e.sizeBytes,
162
- };
163
- }
164
-
165
- const createdAt = process.env.SOURCE_DATE_EPOCH
166
- ? new Date(Number(process.env.SOURCE_DATE_EPOCH) * 1000).toISOString()
167
- : new Date().toISOString();
168
-
169
- return {
170
- id: `rel_capix_code_${stableVersion.slice(1).replace(/\./g, '_')}`,
171
- schemaVersion: 1,
172
- stableVersion,
173
- createdAt,
174
- createdBy: 'release-engineering',
175
- immutable: true,
176
- launcher: { sourceSha, version: launcherVersion },
177
- opencode: { sourceSha, version: opencodeVersion },
178
- plugin: { sourceSha, version: pluginVersion },
179
- provider: { sourceSha, version: providerVersion },
180
- acpVersion: '1',
181
- ipcVersion: '1',
182
- apiRange: { min: 'v1', max: 'v1' },
183
- platforms,
184
- sbomRef: `${baseUrl}/${stableVersion}/sbom.spdx.json`,
185
- provenanceRef: `${baseUrl}/${stableVersion}/provenance.json`,
186
- signatureRef: null,
187
- thirdPartyNoticesRef: `${baseUrl}/${stableVersion}/NOTICE`,
188
- rollbackConstraints: ['retain-n-1'],
189
- publishedPlatforms: present,
190
- };
191
- }
192
-
193
- function main() {
194
- const args = parseArgs(process.argv.slice(2));
195
- const entries = readReleaseEntries(args.entries);
196
- const manifest = buildManifestFromEntries(entries, {
197
- baseUrl: requireArg(args, 'baseUrl'),
198
- stableVersion: requireArg(args, 'stableVersion'),
199
- sourceSha: requireArg(args, 'sourceSha'),
200
- launcherVersion: requireArg(args, 'launcherVersion'),
201
- opencodeVersion: requireArg(args, 'opencodeVersion'),
202
- pluginVersion: requireArg(args, 'pluginVersion'),
203
- providerVersion: requireArg(args, 'providerVersion'),
204
- requireAll: args.requireAll,
205
- });
206
- writeFileSync(args.out, `${JSON.stringify(manifest, null, 2)}\n`);
207
- console.log(
208
- `✓ wrote ${args.out} (stable=${manifest.stableVersion}, platforms=${Object.keys(manifest.platforms).join(',')})`
209
- );
210
- }
211
-
212
- if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
213
- try {
214
- main();
215
- } catch (err) {
216
- console.error(`✗ ${err.message}`);
217
- process.exit(1);
218
- }
219
- }