@textcortex/zenocode 0.1.2 → 0.1.4

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.
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
3
4
  import fs from "node:fs/promises";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import process from "node:process";
7
8
  import { fileURLToPath } from "node:url";
9
+ import { patchZenocodeBinaryText } from "./branding-patch.mjs";
8
10
 
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
12
+ const __dirname = path.dirname(currentFilePath);
10
13
  const appRoot = path.resolve(__dirname, "..");
11
14
  const defaultOutputDir = path.join(appRoot, ".zenocode", "brand-build");
12
15
 
@@ -50,6 +53,94 @@ function _command(name) {
50
53
  return name;
51
54
  }
52
55
 
56
+ function _packageParts(packageName) {
57
+ if (packageName.startsWith("@")) {
58
+ const [scope, baseName] = packageName.split("/");
59
+ return { scope, baseName };
60
+ }
61
+ return { scope: "", baseName: packageName };
62
+ }
63
+
64
+ function _runtimeBinaryPrefix(packageName) {
65
+ const { baseName } = _packageParts(packageName);
66
+ return baseName.replace(/-ai$/, "");
67
+ }
68
+
69
+ export function mapBrandedBinaryPackageName(originalPackageName, runtimePackageName = wrapperPackageName) {
70
+ if (!originalPackageName.startsWith("opencode-")) {
71
+ throw new Error(`Unsupported OpenCode binary package name: ${originalPackageName}`);
72
+ }
73
+ const { scope } = _packageParts(runtimePackageName);
74
+ const prefix = _runtimeBinaryPrefix(runtimePackageName);
75
+ const suffix = originalPackageName.slice("opencode".length);
76
+ const unscopedName = `${prefix}${suffix}`;
77
+ return scope ? `${scope}/${unscopedName}` : unscopedName;
78
+ }
79
+
80
+ export function buildWrapperBinMap(packageName, binName) {
81
+ const { baseName } = _packageParts(packageName);
82
+ const binMap = {
83
+ [baseName]: "./bin/opencode",
84
+ opencode: "./bin/opencode",
85
+ };
86
+ if (binName && !(binName in binMap)) {
87
+ binMap[binName] = "./bin/opencode";
88
+ }
89
+ return binMap;
90
+ }
91
+
92
+ function compareSemver(left, right) {
93
+ const parse = (value) => {
94
+ const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
95
+ if (!match) return null;
96
+ return {
97
+ major: Number(match[1]),
98
+ minor: Number(match[2]),
99
+ patch: Number(match[3]),
100
+ prerelease: match[4] ?? null,
101
+ };
102
+ };
103
+
104
+ const lhs = parse(left);
105
+ const rhs = parse(right);
106
+ if (!lhs || !rhs) {
107
+ throw new Error(`Invalid semver comparison: ${left} vs ${right}`);
108
+ }
109
+
110
+ for (const key of ["major", "minor", "patch"]) {
111
+ if (lhs[key] > rhs[key]) return 1;
112
+ if (lhs[key] < rhs[key]) return -1;
113
+ }
114
+
115
+ if (lhs.prerelease === rhs.prerelease) return 0;
116
+ if (lhs.prerelease === null) return 1;
117
+ if (rhs.prerelease === null) return -1;
118
+
119
+ const leftParts = lhs.prerelease.split(".");
120
+ const rightParts = rhs.prerelease.split(".");
121
+ const length = Math.max(leftParts.length, rightParts.length);
122
+ for (let index = 0; index < length; index += 1) {
123
+ const leftPart = leftParts[index];
124
+ const rightPart = rightParts[index];
125
+ if (leftPart === undefined) return -1;
126
+ if (rightPart === undefined) return 1;
127
+ const leftNumeric = /^\d+$/.test(leftPart);
128
+ const rightNumeric = /^\d+$/.test(rightPart);
129
+ if (leftNumeric && rightNumeric) {
130
+ const leftValue = Number(leftPart);
131
+ const rightValue = Number(rightPart);
132
+ if (leftValue > rightValue) return 1;
133
+ if (leftValue < rightValue) return -1;
134
+ continue;
135
+ }
136
+ if (leftNumeric && !rightNumeric) return -1;
137
+ if (!leftNumeric && rightNumeric) return 1;
138
+ if (leftPart > rightPart) return 1;
139
+ if (leftPart < rightPart) return -1;
140
+ }
141
+ return 0;
142
+ }
143
+
53
144
  async function run(command, args, options = {}) {
54
145
  return new Promise((resolve, reject) => {
55
146
  const child = spawn(command, args, {
@@ -68,6 +159,35 @@ async function run(command, args, options = {}) {
68
159
  });
69
160
  }
70
161
 
162
+ async function runAndCapture(command, args, options = {}) {
163
+ return new Promise((resolve, reject) => {
164
+ const child = spawn(command, args, {
165
+ stdio: ["ignore", "pipe", "pipe"],
166
+ ...options,
167
+ });
168
+
169
+ let stdout = "";
170
+ let stderr = "";
171
+ child.stdout?.on("data", (chunk) => {
172
+ stdout += chunk.toString("utf-8");
173
+ });
174
+ child.stderr?.on("data", (chunk) => {
175
+ stderr += chunk.toString("utf-8");
176
+ });
177
+ child.on("error", reject);
178
+ child.on("exit", (code) => {
179
+ if (code === 0) {
180
+ resolve({ stdout, stderr });
181
+ return;
182
+ }
183
+ const error = new Error(`${command} ${args.join(" ")} failed with exit code ${String(code ?? 1)}`);
184
+ error.stdout = stdout;
185
+ error.stderr = stderr;
186
+ reject(error);
187
+ });
188
+ });
189
+ }
190
+
71
191
  async function requireCommand(command, args = ["--version"]) {
72
192
  try {
73
193
  await run(command, args, { stdio: "ignore" });
@@ -83,90 +203,375 @@ async function readJson(filePath) {
83
203
  return JSON.parse(await fs.readFile(filePath, "utf-8"));
84
204
  }
85
205
 
86
- async function buildWrapperPackage({
87
- checkoutDir,
88
- distDir,
89
- artifactDir,
90
- packageName,
91
- binName,
92
- }) {
93
- const distEntries = await fs.readdir(distDir, { withFileTypes: true });
94
- const binaries = {};
95
- const versions = new Set();
206
+ async function writeJson(filePath, payload) {
207
+ await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
208
+ }
96
209
 
97
- for (const entry of distEntries) {
98
- if (!entry.isDirectory()) continue;
99
- const packageJsonPath = path.join(distDir, entry.name, "package.json");
210
+ async function maybePublishedVersion(packageName) {
211
+ const npmCommand = _command("npm");
212
+ try {
213
+ const result = await runAndCapture(
214
+ npmCommand,
215
+ ["view", packageName, "version", "--json"],
216
+ { env: { ...process.env, NODE_AUTH_TOKEN: process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || "" } },
217
+ );
218
+ const value = JSON.parse(result.stdout.trim() || "null");
219
+ if (typeof value === "string" && value) return value;
220
+ return null;
221
+ } catch (error) {
222
+ const stderr = String(error?.stderr || "");
223
+ if (stderr.includes("E404") || stderr.includes("404")) {
224
+ return null;
225
+ }
226
+ throw error;
227
+ }
228
+ }
229
+
230
+ async function packPackage(packageDir) {
231
+ const npmCommand = _command("npm");
232
+ await run(npmCommand, ["pack"], { cwd: packageDir });
233
+ const tarballs = (await fs.readdir(packageDir)).filter((entry) => entry.endsWith(".tgz")).sort();
234
+ if (!tarballs.length) {
235
+ throw new Error(`npm pack did not produce a tarball in ${packageDir}`);
236
+ }
237
+ return path.join(packageDir, tarballs[tarballs.length - 1]);
238
+ }
239
+
240
+ async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
241
+ if (!publishEnabled) return { published: false, skipped: false };
242
+
243
+ const publishedVersion = await maybePublishedVersion(packageName);
244
+ if (publishedVersion) {
245
+ const comparison = compareSemver(version, publishedVersion);
246
+ if (comparison < 0) {
247
+ throw new Error(
248
+ `Refusing to publish ${packageName}@${version}; npm already has newer version ${publishedVersion}.`,
249
+ );
250
+ }
251
+ if (comparison === 0) {
252
+ return { published: false, skipped: true };
253
+ }
254
+ }
255
+
256
+ const npmCommand = _command("npm");
257
+ await run(
258
+ npmCommand,
259
+ ["publish", tarballPath, "--access", "public", "--tag", publishTag, "--provenance"],
260
+ { cwd },
261
+ );
262
+ return { published: true, skipped: false };
263
+ }
264
+
265
+ function _binaryFilename() {
266
+ return process.platform === "win32" ? "opencode.exe" : "opencode";
267
+ }
268
+
269
+ async function patchBinaryAtPath(binaryPath) {
270
+ const buffer = await fs.readFile(binaryPath);
271
+ const originalLength = buffer.length;
272
+ const patch = patchZenocodeBinaryText(buffer.toString("latin1"));
273
+ if (!patch.patched) {
274
+ throw new Error(`Branding patch did not match binary ${binaryPath}`);
275
+ }
276
+ const nextBuffer = Buffer.from(patch.text, "latin1");
277
+ if (nextBuffer.length !== originalLength) {
278
+ throw new Error(`Branding patch changed binary length for ${binaryPath}`);
279
+ }
280
+ await fs.writeFile(binaryPath, nextBuffer);
281
+ if (process.platform !== "win32") {
282
+ await fs.chmod(binaryPath, 0o755);
283
+ }
284
+ }
285
+
286
+ function _buildWrapperExecutable({ runtimePackageName, binName }) {
287
+ const { scope, baseName } = _packageParts(runtimePackageName);
288
+ const scopeLiteral = JSON.stringify(scope);
289
+ const packagePrefixLiteral = JSON.stringify(_runtimeBinaryPrefix(runtimePackageName));
290
+ const binNameLiteral = JSON.stringify(binName || "");
291
+ const packageBaseLiteral = JSON.stringify(baseName);
292
+
293
+ return `#!/usr/bin/env node
294
+
295
+ const childProcess = require("child_process")
296
+ const fs = require("fs")
297
+ const path = require("path")
298
+ const os = require("os")
299
+
300
+ function run(target) {
301
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
302
+ stdio: "inherit",
303
+ })
304
+ if (result.error) {
305
+ console.error(result.error.message)
306
+ process.exit(1)
307
+ }
308
+ const code = typeof result.status === "number" ? result.status : 0
309
+ process.exit(code)
310
+ }
311
+
312
+ const envPath = process.env.OPENCODE_BIN_PATH
313
+ if (envPath) {
314
+ run(envPath)
315
+ }
316
+
317
+ const scriptPath = fs.realpathSync(__filename)
318
+ const scriptDir = path.dirname(scriptPath)
319
+ const packageScope = ${scopeLiteral}
320
+ const packagePrefix = ${packagePrefixLiteral}
321
+ const packageBaseName = ${packageBaseLiteral}
322
+ const brandedBinName = ${binNameLiteral}
323
+
324
+ const platformMap = {
325
+ darwin: "darwin",
326
+ linux: "linux",
327
+ win32: "windows",
328
+ }
329
+ const archMap = {
330
+ x64: "x64",
331
+ arm64: "arm64",
332
+ arm: "arm",
333
+ }
334
+
335
+ let platform = platformMap[os.platform()]
336
+ if (!platform) {
337
+ platform = os.platform()
338
+ }
339
+ let arch = archMap[os.arch()]
340
+ if (!arch) {
341
+ arch = os.arch()
342
+ }
343
+ const base = packagePrefix + "-" + platform + "-" + arch
344
+ const binary = platform === "windows" ? "opencode.exe" : "opencode"
345
+
346
+ function supportsAvx2() {
347
+ if (arch !== "x64") return false
348
+
349
+ if (platform === "linux") {
100
350
  try {
101
- const pkg = await readJson(packageJsonPath);
102
- if (typeof pkg?.name === "string" && typeof pkg?.version === "string") {
103
- binaries[pkg.name] = pkg.version;
104
- versions.add(pkg.version);
105
- }
351
+ return /(^|\\s)avx2(\\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
106
352
  } catch {
107
- continue;
353
+ return false
108
354
  }
109
355
  }
110
356
 
111
- if (!Object.keys(binaries).length) {
112
- throw new Error(`No binary packages found under ${distDir}`);
357
+ if (platform === "darwin") {
358
+ try {
359
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
360
+ encoding: "utf8",
361
+ timeout: 1500,
362
+ })
363
+ if (result.status !== 0) return false
364
+ return (result.stdout || "").trim() === "1"
365
+ } catch {
366
+ return false
367
+ }
113
368
  }
114
369
 
115
- const version = versions.values().next().value;
116
- if (!version) {
117
- throw new Error("Unable to determine branded package version");
370
+ if (platform === "windows") {
371
+ const cmd =
372
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
373
+
374
+ for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
375
+ try {
376
+ const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], {
377
+ encoding: "utf8",
378
+ timeout: 3000,
379
+ windowsHide: true,
380
+ })
381
+ if (result.status !== 0) continue
382
+ const out = (result.stdout || "").trim().toLowerCase()
383
+ if (out === "true" || out === "1") return true
384
+ if (out === "false" || out === "0") return false
385
+ } catch {
386
+ continue
387
+ }
388
+ }
389
+
390
+ return false
118
391
  }
119
392
 
120
- const wrapperDir = path.join(artifactDir, "npm-package");
121
- await fs.rm(wrapperDir, { recursive: true, force: true });
122
- await fs.mkdir(path.join(wrapperDir, "bin"), { recursive: true });
393
+ return false
394
+ }
123
395
 
124
- const opencodePkgPath = path.join(checkoutDir, "packages", "opencode", "package.json");
125
- const opencodePkg = await readJson(opencodePkgPath);
126
- const license = typeof opencodePkg?.license === "string" ? opencodePkg.license : "MIT";
396
+ const names = (() => {
397
+ const avx2 = supportsAvx2()
398
+ const baseline = arch === "x64" && !avx2
127
399
 
128
- const binMap = { opencode: "./bin/opencode" };
129
- if (binName && binName !== "opencode") {
130
- binMap[binName] = "./bin/opencode";
400
+ if (platform === "linux") {
401
+ const musl = (() => {
402
+ try {
403
+ if (fs.existsSync("/etc/alpine-release")) return true
404
+ } catch {
405
+ }
406
+
407
+ try {
408
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
409
+ const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
410
+ if (text.includes("musl")) return true
411
+ } catch {
412
+ }
413
+
414
+ return false
415
+ })()
416
+
417
+ if (musl) {
418
+ if (arch === "x64") {
419
+ if (baseline) return [base + "-baseline-musl", base + "-musl", base + "-baseline", base]
420
+ return [base + "-musl", base + "-baseline-musl", base, base + "-baseline"]
421
+ }
422
+ return [base + "-musl", base]
423
+ }
424
+
425
+ if (arch === "x64") {
426
+ if (baseline) return [base + "-baseline", base, base + "-baseline-musl", base + "-musl"]
427
+ return [base, base + "-baseline", base + "-musl", base + "-baseline-musl"]
428
+ }
429
+ return [base, base + "-musl"]
131
430
  }
132
431
 
432
+ if (arch === "x64") {
433
+ if (baseline) return [base + "-baseline", base]
434
+ return [base, base + "-baseline"]
435
+ }
436
+ return [base]
437
+ })()
438
+
439
+ function candidatePackageNames(name) {
440
+ const scopedName = packageScope ? packageScope + "/" + name : name
441
+ const legacyName = name.replace(new RegExp("^" + packagePrefix), "opencode")
442
+ return packageScope ? [scopedName, legacyName] : [scopedName]
443
+ }
444
+
445
+ function resolveFromPackage(packageName) {
446
+ try {
447
+ const packageJsonPath = require.resolve(packageName + "/package.json", {
448
+ paths: [scriptDir],
449
+ })
450
+ const packageDir = path.dirname(packageJsonPath)
451
+ const candidate = path.join(packageDir, "bin", binary)
452
+ if (fs.existsSync(candidate)) return candidate
453
+ } catch {
454
+ }
455
+ }
456
+
457
+ function findBinary() {
458
+ for (const name of names) {
459
+ for (const packageName of candidatePackageNames(name)) {
460
+ const candidate = resolveFromPackage(packageName)
461
+ if (candidate) return candidate
462
+ }
463
+ }
464
+ }
465
+
466
+ const resolved = findBinary()
467
+ if (!resolved) {
468
+ const installTargets = names.flatMap((name) => candidatePackageNames(name))
469
+ console.error(
470
+ "It seems that your package manager failed to install the right version of the " +
471
+ packageBaseName +
472
+ " CLI for your platform. You can try manually installing " +
473
+ installTargets.map((name) => "\\""+name+"\\"").join(" or ") +
474
+ " package",
475
+ )
476
+ process.exit(1)
477
+ }
478
+
479
+ run(resolved)
480
+ `;
481
+ }
482
+
483
+ async function buildWrapperPackage({
484
+ artifactDir,
485
+ licensePath,
486
+ packageName,
487
+ binName,
488
+ version,
489
+ binaryPackages,
490
+ }) {
491
+ const wrapperDir = path.join(artifactDir, "npm-package");
492
+ await fs.rm(wrapperDir, { recursive: true, force: true });
493
+ await fs.mkdir(path.join(wrapperDir, "bin"), { recursive: true });
494
+
133
495
  const wrapperPkg = {
134
496
  name: packageName,
135
497
  version,
136
- license,
498
+ license: "MIT",
137
499
  type: "commonjs",
138
- bin: binMap,
139
- scripts: { postinstall: "node ./postinstall.mjs" },
140
- optionalDependencies: binaries,
500
+ bin: buildWrapperBinMap(packageName, binName),
501
+ optionalDependencies: Object.fromEntries(
502
+ binaryPackages.map((pkg) => [pkg.brandedName, pkg.version]),
503
+ ),
504
+ files: [
505
+ "bin",
506
+ "LICENSE",
507
+ ],
141
508
  };
142
509
 
143
- await fs.copyFile(
144
- path.join(checkoutDir, "packages", "opencode", "bin", "opencode"),
510
+ await fs.copyFile(licensePath, path.join(wrapperDir, "LICENSE"));
511
+ await fs.writeFile(
145
512
  path.join(wrapperDir, "bin", "opencode"),
513
+ _buildWrapperExecutable({ runtimePackageName: packageName, binName }),
514
+ { encoding: "utf-8", mode: 0o755 },
146
515
  );
147
- await fs.copyFile(
148
- path.join(checkoutDir, "packages", "opencode", "script", "postinstall.mjs"),
149
- path.join(wrapperDir, "postinstall.mjs"),
150
- );
151
- await fs.copyFile(path.join(checkoutDir, "LICENSE"), path.join(wrapperDir, "LICENSE"));
152
- await fs.writeFile(path.join(wrapperDir, "package.json"), `${JSON.stringify(wrapperPkg, null, 2)}\n`, "utf-8");
516
+ if (process.platform !== "win32") {
517
+ await fs.chmod(path.join(wrapperDir, "bin", "opencode"), 0o755);
518
+ }
519
+ await writeJson(path.join(wrapperDir, "package.json"), wrapperPkg);
153
520
 
154
- const packCommand = _command("npm");
155
- await run(packCommand, ["pack"], { cwd: wrapperDir });
521
+ const tarballPath = await packPackage(wrapperDir);
522
+ const publishResult = await publishTarballIfNeeded(packageName, version, tarballPath, wrapperDir);
523
+ return { tarballPath, publishResult };
524
+ }
156
525
 
157
- const tarballs = (await fs.readdir(wrapperDir)).filter((entry) => entry.endsWith(".tgz")).sort();
158
- if (!tarballs.length) {
159
- throw new Error(`npm pack did not produce a tarball in ${wrapperDir}`);
160
- }
526
+ async function prepareBinaryPackages({ distDir, artifactDir, runtimePackageName }) {
527
+ const packageRoot = path.join(artifactDir, "npm-binaries");
528
+ await fs.rm(packageRoot, { recursive: true, force: true });
529
+ await fs.mkdir(packageRoot, { recursive: true });
530
+
531
+ const entries = await fs.readdir(distDir, { withFileTypes: true });
532
+ const packages = [];
533
+ for (const entry of entries) {
534
+ if (!entry.isDirectory()) continue;
535
+ const sourceDir = path.join(distDir, entry.name);
536
+ const packageJsonPath = path.join(sourceDir, "package.json");
537
+ let pkg;
538
+ try {
539
+ pkg = await readJson(packageJsonPath);
540
+ } catch {
541
+ continue;
542
+ }
543
+ if (typeof pkg?.name !== "string" || typeof pkg?.version !== "string") {
544
+ continue;
545
+ }
546
+ if (!pkg.name.startsWith("opencode-")) {
547
+ continue;
548
+ }
161
549
 
162
- const latestTarball = path.join(wrapperDir, tarballs[tarballs.length - 1]);
163
- if (publishEnabled) {
164
- await run(packCommand, ["publish", latestTarball, "--access", "public", "--tag", publishTag], {
165
- cwd: wrapperDir,
550
+ const brandedName = mapBrandedBinaryPackageName(pkg.name, runtimePackageName);
551
+ const targetDir = path.join(packageRoot, brandedName.replace("/", "__"));
552
+ await fs.cp(sourceDir, targetDir, { recursive: true });
553
+ pkg.name = brandedName;
554
+ await writeJson(path.join(targetDir, "package.json"), pkg);
555
+ await patchBinaryAtPath(path.join(targetDir, "bin", _binaryFilename()));
556
+
557
+ const tarballPath = await packPackage(targetDir);
558
+ const publishResult = await publishTarballIfNeeded(brandedName, pkg.version, tarballPath, targetDir);
559
+ packages.push({
560
+ dirName: entry.name,
561
+ originalName: entry.name,
562
+ brandedName,
563
+ version: pkg.version,
564
+ packageDir: targetDir,
565
+ tarballPath,
566
+ publishResult,
166
567
  });
167
568
  }
168
569
 
169
- return latestTarball;
570
+ if (!packages.length) {
571
+ throw new Error(`No OpenCode binary packages found under ${distDir}`);
572
+ }
573
+
574
+ return packages.sort((left, right) => left.originalName.localeCompare(right.originalName));
170
575
  }
171
576
 
172
577
  function pickCurrentPlatformBinary(distPackages) {
@@ -174,7 +579,8 @@ function pickCurrentPlatformBinary(distPackages) {
174
579
  const archMap = { x64: "x64", arm64: "arm64" };
175
580
  const expectedPlatform = platformMap[process.platform] || process.platform;
176
581
  const expectedArch = archMap[process.arch] || process.arch;
177
- const expectedPrefix = `opencode-${expectedPlatform}-${expectedArch}`;
582
+ const prefix = _runtimeBinaryPrefix(wrapperPackageName);
583
+ const expectedPrefix = `${prefix}-${expectedPlatform}-${expectedArch}`;
178
584
 
179
585
  const exact = distPackages.find((name) => name === expectedPrefix);
180
586
  if (exact) return exact;
@@ -187,7 +593,7 @@ function pickCurrentPlatformBinary(distPackages) {
187
593
 
188
594
  async function main() {
189
595
  if (cliArgs.includes("-h") || cliArgs.includes("--help")) {
190
- console.log("Build a branded Zenocode opencode binary/package from a fork.");
596
+ console.log("Build branded Zenocode OpenCode runtime packages from upstream.");
191
597
  console.log("");
192
598
  console.log("Environment variables:");
193
599
  console.log(` ZENOCODE_OPENCODE_FORK_URL Fork URL (default: ${forkUrl})`);
@@ -196,7 +602,7 @@ async function main() {
196
602
  console.log(` ZENOCODE_BRANDED_PACKAGE Wrapper package name (default: ${wrapperPackageName})`);
197
603
  console.log(` ZENOCODE_BRANDED_BIN_NAME Extra bin name in wrapper (default: ${wrapperBinName})`);
198
604
  console.log(" ZENOCODE_OPENCODE_BUILD_ARGS Additional args for upstream build script");
199
- console.log(" ZENOCODE_PUBLISH=1 Publish tarball to npm");
605
+ console.log(" ZENOCODE_PUBLISH=1 Publish packages to npm");
200
606
  console.log(` ZENOCODE_PUBLISH_TAG npm tag (default: ${publishTag})`);
201
607
  console.log(" Legacy CODECORTEX_* env names are still accepted for compatibility.");
202
608
  return;
@@ -230,38 +636,64 @@ async function main() {
230
636
  );
231
637
 
232
638
  await fs.mkdir(artifactDir, { recursive: true });
233
- const npmTarball = await buildWrapperPackage({
234
- checkoutDir,
639
+ const binaryPackages = await prepareBinaryPackages({
235
640
  distDir,
236
641
  artifactDir,
237
- packageName: wrapperPackageName,
238
- binName: wrapperBinName,
642
+ runtimePackageName: wrapperPackageName,
239
643
  });
240
644
 
241
- const distEntries = await fs.readdir(distDir, { withFileTypes: true });
242
- const distPackages = distEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
243
- const selectedPackage = pickCurrentPlatformBinary(distPackages);
244
- if (!selectedPackage) {
245
- throw new Error(`No build artifacts found under ${distDir}`);
645
+ const version = binaryPackages[0]?.version;
646
+ if (!version) {
647
+ throw new Error("Unable to determine branded runtime version");
246
648
  }
247
649
 
248
- const builtBinaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
249
- const builtBinaryPath = path.join(distDir, selectedPackage, "bin", builtBinaryName);
650
+ const wrapperResult = await buildWrapperPackage({
651
+ artifactDir,
652
+ licensePath: path.join(checkoutDir, "LICENSE"),
653
+ packageName: wrapperPackageName,
654
+ binName: wrapperBinName,
655
+ version,
656
+ binaryPackages,
657
+ });
658
+
659
+ const selectedPackage = pickCurrentPlatformBinary(
660
+ binaryPackages.map((pkg) => pkg.brandedName.split("/").at(-1)),
661
+ );
662
+ const localBinaryPackage = binaryPackages.find((pkg) => pkg.brandedName.endsWith(`/${selectedPackage}`))
663
+ || binaryPackages[0];
250
664
  const localBinaryDir = path.join(artifactDir, "bin");
251
665
  const localBinaryName = process.platform === "win32" ? "zenocode-opencode.exe" : "zenocode-opencode";
252
666
  const localBinaryPath = path.join(localBinaryDir, localBinaryName);
253
667
 
254
668
  await fs.mkdir(localBinaryDir, { recursive: true });
255
- await fs.copyFile(builtBinaryPath, localBinaryPath);
669
+ await fs.copyFile(
670
+ path.join(localBinaryPackage.packageDir, "bin", _binaryFilename()),
671
+ localBinaryPath,
672
+ );
256
673
  if (process.platform !== "win32") {
257
674
  await fs.chmod(localBinaryPath, 0o755);
258
675
  }
259
676
 
260
677
  console.log("\nZenocode branded build complete.");
261
678
  console.log(`Branded binary: ${localBinaryPath}`);
262
- console.log(`Branded package tarball: ${npmTarball}`);
679
+ console.log(`Wrapper package tarball: ${wrapperResult.tarballPath}`);
680
+ for (const pkg of binaryPackages) {
681
+ console.log(`Binary package tarball: ${pkg.tarballPath}`);
682
+ }
263
683
  if (publishEnabled) {
264
- console.log(`Published to npm as ${wrapperPackageName} (tag: ${publishTag}).`);
684
+ const publishedBinaries = binaryPackages.filter((pkg) => pkg.publishResult.published).map((pkg) => pkg.brandedName);
685
+ const skippedBinaries = binaryPackages.filter((pkg) => pkg.publishResult.skipped).map((pkg) => pkg.brandedName);
686
+ if (publishedBinaries.length) {
687
+ console.log(`Published binary packages: ${publishedBinaries.join(", ")}`);
688
+ }
689
+ if (skippedBinaries.length) {
690
+ console.log(`Skipped already-published binary packages: ${skippedBinaries.join(", ")}`);
691
+ }
692
+ if (wrapperResult.publishResult.published) {
693
+ console.log(`Published wrapper package: ${wrapperPackageName} (tag: ${publishTag}).`);
694
+ } else if (wrapperResult.publishResult.skipped) {
695
+ console.log(`Skipped already-published wrapper package: ${wrapperPackageName}.`);
696
+ }
265
697
  }
266
698
  console.log(`\nRun with local binary:`);
267
699
  console.log(` export ZENOCODE_OPENCODE_BIN_PATH="${localBinaryPath}"`);
@@ -272,7 +704,17 @@ async function main() {
272
704
  }
273
705
  }
274
706
 
275
- main().catch((error) => {
276
- console.error(error instanceof Error ? error.message : String(error));
277
- process.exit(1);
278
- });
707
+ function resolveExecutablePath(value) {
708
+ try {
709
+ return realpathSync(value);
710
+ } catch {
711
+ return path.resolve(value);
712
+ }
713
+ }
714
+
715
+ if (process.argv[1] && resolveExecutablePath(process.argv[1]) === currentFilePath) {
716
+ main().catch((error) => {
717
+ console.error(error instanceof Error ? error.message : String(error));
718
+ process.exit(1);
719
+ });
720
+ }