capix-code 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +203 -0
- package/README.md +131 -0
- package/bin/capix-code.cjs +57 -0
- package/dist/customer/config/capix-defaults.json +22 -0
- package/dist/customer/config/defaults.json +248 -0
- package/package.json +79 -0
- package/scripts/assert-artifact.sh +30 -0
- package/scripts/assert-customer-brand.sh +54 -0
- package/scripts/assert-upstream-brand.sh +46 -0
- package/scripts/bootstrap.sh +38 -0
- package/scripts/build-manifest.mjs +219 -0
- package/scripts/build.sh +74 -0
- package/scripts/checksum.sh +8 -0
- package/scripts/dev-install.sh +30 -0
- package/scripts/dev.sh +35 -0
- package/scripts/install-config.sh +66 -0
- package/scripts/install.sh +115 -0
- package/scripts/package-customer.sh +28 -0
- package/scripts/package.sh +84 -0
- package/scripts/postinstall.js +33 -0
- package/scripts/prepare-npm-meta.mjs +12 -0
- package/scripts/prepare-npm-platform.mjs +59 -0
- package/scripts/rebrand.sh +159 -0
- package/scripts/resolve-version.mjs +126 -0
- package/scripts/validate-manifest.mjs +167 -0
- package/scripts/verify-runtime-provider.ts +9 -0
- package/scripts/write-release-entry.mjs +38 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a requested version ("latest", empty, or an explicit tag) to an
|
|
3
|
+
* immutable vMAJOR.MINOR.PATCH tag.
|
|
4
|
+
*
|
|
5
|
+
* "latest" is permitted ONLY by resolving it to a concrete immutable version
|
|
6
|
+
* before any download — never by blindly trusting mutable content. Resolution
|
|
7
|
+
* sources, in priority order:
|
|
8
|
+
*
|
|
9
|
+
* 1. CAPIX_STABLE_VERSION env (deterministic, offline, release-pinned)
|
|
10
|
+
* 2. a materialized manifest's stableVersion (single source of truth)
|
|
11
|
+
* 3. the GitHub releases API /releases/latest tag_name (--allow-network only)
|
|
12
|
+
*
|
|
13
|
+
* If none resolves, the script fails closed (exit 2) so the installer never
|
|
14
|
+
* proceeds with an unbounded/mutable version.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/resolve-version.mjs [latest|<tag>] [--manifest manifest/release-manifest.json] [--allow-network]
|
|
18
|
+
*
|
|
19
|
+
* Prints the resolved immutable tag to stdout.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { resolve } from 'node:path';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const SEMVER_TAG = /^v\d+\.\d+\.\d+$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a version request. Pure (no IO except the optional fetch passed in)
|
|
29
|
+
* so it is unit-testable without a network.
|
|
30
|
+
*
|
|
31
|
+
* @param {string|null} request "latest", "" or null
|
|
32
|
+
* @param {object} ctx
|
|
33
|
+
* @param {string|undefined} ctx.envStable CAPIX_STABLE_VERSION value
|
|
34
|
+
* @param {object|undefined} ctx.manifest parsed manifest with stableVersion
|
|
35
|
+
* @param {(() => Promise<string|null>)|undefined} ctx.fetchLatestTag network resolver
|
|
36
|
+
* @returns {{tag:string, source:string}}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveVersion(request, ctx) {
|
|
39
|
+
const requested = (request ?? '').trim();
|
|
40
|
+
// An explicit immutable tag is always accepted as-is.
|
|
41
|
+
if (requested && requested !== 'latest') {
|
|
42
|
+
if (!SEMVER_TAG.test(requested)) {
|
|
43
|
+
throw new Error(`invalid version '${requested}' (expected vMAJOR.MINOR.PATCH)`);
|
|
44
|
+
}
|
|
45
|
+
return { tag: requested, source: 'explicit' };
|
|
46
|
+
}
|
|
47
|
+
// latest / empty → resolve to an immutable tag.
|
|
48
|
+
if (ctx.envStable) {
|
|
49
|
+
if (!SEMVER_TAG.test(ctx.envStable)) {
|
|
50
|
+
throw new Error(`CAPIX_STABLE_VERSION is not a vMAJOR.MINOR.PATCH tag: '${ctx.envStable}'`);
|
|
51
|
+
}
|
|
52
|
+
return { tag: ctx.envStable, source: 'env' };
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
ctx.manifest &&
|
|
56
|
+
typeof ctx.manifest.stableVersion === 'string' &&
|
|
57
|
+
SEMVER_TAG.test(ctx.manifest.stableVersion)
|
|
58
|
+
) {
|
|
59
|
+
return { tag: ctx.manifest.stableVersion, source: 'manifest' };
|
|
60
|
+
}
|
|
61
|
+
// Network resolver is invoked lazily by the caller (kept out of the pure
|
|
62
|
+
// path so tests never touch the network).
|
|
63
|
+
return null; // signals "caller may try network, else fail closed"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read a manifest file and return its parsed object, or null if unreadable. */
|
|
67
|
+
export function loadManifest(path) {
|
|
68
|
+
if (!path) return null;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** GitHub releases API resolver. Returns the latest tag_name or null. */
|
|
77
|
+
async function githubLatest(repo) {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
80
|
+
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'capix-code-installer' },
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) return null;
|
|
83
|
+
const body = await res.json();
|
|
84
|
+
const tag = body?.tag_name;
|
|
85
|
+
return typeof tag === 'string' && SEMVER_TAG.test(tag) ? tag : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const argv = process.argv.slice(2);
|
|
93
|
+
const request = argv.find((a) => !a.startsWith('--')) ?? '';
|
|
94
|
+
const manifestPath = argv[argv.indexOf('--manifest') + 1];
|
|
95
|
+
const allowNetwork = argv.includes('--allow-network');
|
|
96
|
+
const repo = process.env.CAPIX_RELEASE_REPO ?? 'CapIX-Protocol/Capix-Code';
|
|
97
|
+
|
|
98
|
+
const ctx = {
|
|
99
|
+
envStable: process.env.CAPIX_STABLE_VERSION?.trim() || undefined,
|
|
100
|
+
manifest: loadManifest(manifestPath),
|
|
101
|
+
};
|
|
102
|
+
const resolved = resolveVersion(request, ctx);
|
|
103
|
+
if (resolved) {
|
|
104
|
+
process.stdout.write(resolved.tag);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
if (allowNetwork) {
|
|
108
|
+
const tag = await githubLatest(repo);
|
|
109
|
+
if (tag) {
|
|
110
|
+
process.stdout.write(tag);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.error(
|
|
115
|
+
`ERROR: cannot resolve '${request || 'latest'}' to an immutable version. ` +
|
|
116
|
+
`Set CAPIX_STABLE_VERSION (e.g. v1.2.3) or pass --manifest <path> [--allow-network].`
|
|
117
|
+
);
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.error(err?.message ? `ERROR: ${err.message}` : String(err));
|
|
124
|
+
process.exit(2);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release manifest validator.
|
|
3
|
+
*
|
|
4
|
+
* Guarantees that a production release-manifest.json cannot ship with
|
|
5
|
+
* unmaterialized TEMPLATE placeholders, zero-size artifacts, non-HTTPS URLs,
|
|
6
|
+
* or missing/invalid checksums. A manifest that fails any check is rejected
|
|
7
|
+
* with a concrete, actionable error — it must never be silently published.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/validate-manifest.mjs <manifest.json> [--strict]
|
|
11
|
+
*
|
|
12
|
+
* Exit 0 = valid, 1 = invalid (message on stderr).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const SEMVER_TAG = /^v\d+\.\d+\.\d+$/;
|
|
19
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
20
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
21
|
+
const SHA1 = /^[0-9a-f]{40}$/;
|
|
22
|
+
const ISO8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
23
|
+
const REQUIRED_PLATFORMS = ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64'];
|
|
24
|
+
|
|
25
|
+
/** Deep scan: any string field equal to "TEMPLATE" is a release blocker. */
|
|
26
|
+
export function findTemplatePlaceholders(value, path = '$') {
|
|
27
|
+
const hits = [];
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
if (value === 'TEMPLATE') hits.push(path);
|
|
30
|
+
} else if (Array.isArray(value)) {
|
|
31
|
+
for (let i = 0; i < value.length; i++)
|
|
32
|
+
hits.push(...findTemplatePlaceholders(value[i], `${path}[${i}]`));
|
|
33
|
+
} else if (value && typeof value === 'object') {
|
|
34
|
+
for (const [k, v] of Object.entries(value))
|
|
35
|
+
hits.push(...findTemplatePlaceholders(v, `${path}.${k}`));
|
|
36
|
+
}
|
|
37
|
+
return hits;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate a parsed manifest object. Returns an array of error strings
|
|
42
|
+
* (empty when valid). Pure function — safe to unit-test without IO.
|
|
43
|
+
*/
|
|
44
|
+
export function validateManifest(manifest, { strict = false } = {}) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
48
|
+
return ['manifest must be a JSON object'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 1. No unmaterialized TEMPLATE placeholders anywhere.
|
|
52
|
+
const placeholders = findTemplatePlaceholders(manifest);
|
|
53
|
+
for (const p of placeholders) errors.push(`${p} is an unmaterialized TEMPLATE placeholder`);
|
|
54
|
+
|
|
55
|
+
// 2. Top-level identity.
|
|
56
|
+
if (typeof manifest.id !== 'string' || !manifest.id)
|
|
57
|
+
errors.push('$.id must be a non-empty string');
|
|
58
|
+
if (manifest.immutable !== true)
|
|
59
|
+
errors.push('$.immutable must be true for a publishable manifest');
|
|
60
|
+
if (typeof manifest.createdAt !== 'string' || !ISO8601.test(manifest.createdAt)) {
|
|
61
|
+
errors.push('$.createdAt must be an ISO-8601 timestamp');
|
|
62
|
+
}
|
|
63
|
+
if (typeof manifest.stableVersion !== 'string' || !SEMVER_TAG.test(manifest.stableVersion)) {
|
|
64
|
+
errors.push(
|
|
65
|
+
'$.stableVersion must be an immutable vMAJOR.MINOR.PATCH tag (single source of truth)'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Component provenance (launcher/opencode/plugin/provider).
|
|
70
|
+
for (const comp of ['launcher', 'opencode', 'plugin', 'provider']) {
|
|
71
|
+
const node = manifest[comp];
|
|
72
|
+
if (!node || typeof node !== 'object') {
|
|
73
|
+
errors.push(`$.${comp} must be an object`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (typeof node.sourceSha !== 'string' || !SHA1.test(node.sourceSha)) {
|
|
77
|
+
errors.push(`$.${comp}.sourceSha must be a 40-character lowercase hex commit SHA`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof node.version !== 'string' || !SEMVER.test(node.version)) {
|
|
80
|
+
errors.push(`$.${comp}.version must be MAJOR.MINOR.PATCH`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 4. Platforms — every required platform present with a real artifact.
|
|
85
|
+
if (!manifest.platforms || typeof manifest.platforms !== 'object') {
|
|
86
|
+
errors.push('$.platforms must be an object');
|
|
87
|
+
} else {
|
|
88
|
+
for (const plat of REQUIRED_PLATFORMS) {
|
|
89
|
+
const entry = manifest.platforms[plat];
|
|
90
|
+
if (!entry || typeof entry !== 'object') {
|
|
91
|
+
errors.push(`$.platforms.${plat} is missing`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (typeof entry.url !== 'string' || !/^https:\/\//.test(entry.url)) {
|
|
95
|
+
errors.push(`$.platforms.${plat}.url must be an https URL`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof entry.sha256 !== 'string' || !SHA256.test(entry.sha256)) {
|
|
98
|
+
errors.push(`$.platforms.${plat}.sha256 must be a 64-character lowercase hex SHA-256`);
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
typeof entry.sizeBytes !== 'number' ||
|
|
102
|
+
!Number.isFinite(entry.sizeBytes) ||
|
|
103
|
+
entry.sizeBytes <= 0
|
|
104
|
+
) {
|
|
105
|
+
errors.push(`$.platforms.${plat}.sizeBytes must be a positive number`);
|
|
106
|
+
}
|
|
107
|
+
// signatureUrl may be null for unsigned artifacts, but must not be TEMPLATE.
|
|
108
|
+
if (
|
|
109
|
+
entry.signatureUrl !== null &&
|
|
110
|
+
(typeof entry.signatureUrl !== 'string' || !/^https:\/\//.test(entry.signatureUrl))
|
|
111
|
+
) {
|
|
112
|
+
errors.push(`$.platforms.${plat}.signatureUrl must be an https URL or null`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Reject unknown platforms that snuck in (keeps the manifest honest).
|
|
116
|
+
for (const plat of Object.keys(manifest.platforms)) {
|
|
117
|
+
if (!REQUIRED_PLATFORMS.includes(plat)) {
|
|
118
|
+
errors.push(`$.platforms.${plat} is not a recognized platform`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5. Supply-chain refs — must be present and materialized (or explicitly null
|
|
124
|
+
// with a documented reason, in non-strict mode).
|
|
125
|
+
for (const ref of ['sbomRef', 'provenanceRef', 'thirdPartyNoticesRef']) {
|
|
126
|
+
const v = manifest[ref];
|
|
127
|
+
if (typeof v !== 'string' || !v) errors.push(`$.${ref} must be a non-empty reference`);
|
|
128
|
+
}
|
|
129
|
+
if (strict) {
|
|
130
|
+
if (typeof manifest.signatureRef !== 'string' || !manifest.signatureRef) {
|
|
131
|
+
errors.push('$.signatureRef must be present in strict mode (signed releases)');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return errors;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function main() {
|
|
139
|
+
const [file, ...rest] = process.argv.slice(2);
|
|
140
|
+
if (!file) {
|
|
141
|
+
console.error('usage: node scripts/validate-manifest.mjs <manifest.json> [--strict]');
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
const strict = rest.includes('--strict');
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`cannot read/parse manifest ${file}: ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const errors = validateManifest(parsed, { strict });
|
|
153
|
+
if (errors.length) {
|
|
154
|
+
for (const e of errors) console.error(`✗ ${e}`);
|
|
155
|
+
console.error(
|
|
156
|
+
`manifest ${file} is not publishable (${errors.length} blocker${errors.length === 1 ? '' : 's'})`
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
console.log(`✓ manifest ${file} is publishable`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Run as CLI only when invoked directly, not when imported by tests.
|
|
165
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
166
|
+
main();
|
|
167
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const runtime = await import('@capix/runtime-provider');
|
|
2
|
+
if (typeof runtime.capix !== 'function') {
|
|
3
|
+
throw new Error('@capix/runtime-provider does not export the capix provider factory');
|
|
4
|
+
}
|
|
5
|
+
const model = runtime.capix('auto');
|
|
6
|
+
if (model.specificationVersion !== 'v2' || model.provider !== 'capix') {
|
|
7
|
+
throw new Error('@capix/runtime-provider does not implement the pinned LanguageModelV2 contract');
|
|
8
|
+
}
|
|
9
|
+
console.log('@capix/runtime-provider: bundled provider contract verified');
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const [version, platform, arch, archive] = process.argv.slice(2);
|
|
7
|
+
if (!version || !platform || !arch || !archive) process.exit(2);
|
|
8
|
+
|
|
9
|
+
const shaPattern = /^[0-9a-f]{40}$/i;
|
|
10
|
+
const githubSha = process.env.GITHUB_SHA?.trim();
|
|
11
|
+
let sourceSha;
|
|
12
|
+
if (githubSha) {
|
|
13
|
+
if (!shaPattern.test(githubSha)) throw new Error('GITHUB_SHA must be a 40-character hexadecimal commit SHA');
|
|
14
|
+
sourceSha = githubSha.toLowerCase();
|
|
15
|
+
} else {
|
|
16
|
+
if (process.env.CI) throw new Error('GITHUB_SHA is required when generating release metadata in CI');
|
|
17
|
+
sourceSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
18
|
+
cwd: new URL('..', import.meta.url).pathname,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
}).trim();
|
|
21
|
+
if (!shaPattern.test(sourceSha)) throw new Error('git rev-parse returned an invalid source SHA');
|
|
22
|
+
}
|
|
23
|
+
const entry = {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
product: 'capix-code',
|
|
26
|
+
version,
|
|
27
|
+
platform: `${platform}-${arch}`,
|
|
28
|
+
artifact: basename(archive),
|
|
29
|
+
sha256: createHash('sha256').update(readFileSync(archive)).digest('hex'),
|
|
30
|
+
sizeBytes: statSync(archive).size,
|
|
31
|
+
sourceSha,
|
|
32
|
+
signed: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(dirname(archive), `capix-code-${version}-${platform}-${arch}.release.json`),
|
|
37
|
+
`${JSON.stringify(entry, null, 2)}\n`
|
|
38
|
+
);
|