-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbuild-server.ts
More file actions
893 lines (761 loc) · 28.7 KB
/
Copy pathbuild-server.ts
File metadata and controls
893 lines (761 loc) · 28.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
#!/usr/bin/env bun
/**
* Build script for standalone Qwen Code server.
*
* Assembles a self-contained distribution directory with all runtime
* dependencies, resources, and platform-specific binaries.
*
* Usage:
* bun run scripts/build-server.ts
* bun run scripts/build-server.ts --platform=linux --arch=x64
* bun run scripts/build-server.ts --platform=linux --arch=arm64 --compress
*
* Options:
* --platform Target platform: darwin, linux (default: current)
* --arch Target architecture: x64, arm64 (default: current)
* --output Output directory (default: dist/server)
* --compress Create .tar.gz archive after assembly
* --skip-download Skip Bun/uv downloads (use existing if present)
* --help Show help
*/
process.on('uncaughtException', (error) => {
console.error('\n Build failed (uncaught):', error);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('\n Build failed (unhandled):', reason);
process.exit(1);
});
import { parseArgs } from 'util';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import {
existsSync,
mkdirSync,
rmSync,
copyFileSync,
cpSync,
lstatSync,
readdirSync,
readFileSync,
writeFileSync,
chmodSync,
symlinkSync,
} from 'fs';
import { $ } from 'bun';
import {
type Platform,
type Arch,
type BuildConfig,
BUN_VERSION,
UV_VERSION,
downloadBun,
downloadUv,
buildMcpServers,
getPlatformKey,
} from './build/common';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type ServerPlatform = 'darwin' | 'linux';
interface ServerBuildConfig {
platform: ServerPlatform;
arch: Arch;
rootDir: string;
electronDir: string; // Source of resources
outputDir: string;
compress: boolean;
skipDownload: boolean;
version: string;
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
function showHelp(): void {
console.log(`
Standalone server build script for Qwen Code
Usage:
bun run scripts/build-server.ts [options]
Options:
--platform=<platform> Target platform: darwin, linux
(default: ${process.platform})
--arch=<arch> Target architecture: x64, arm64
(default: ${process.arch === 'arm64' ? 'arm64' : 'x64'})
--output=<path> Output directory (default: dist/server)
--compress Create .tar.gz after assembly
--skip-download Reuse existing Bun/uv binaries
--help Show this help message
Examples:
# Build for current platform
bun run scripts/build-server.ts
# Build Linux x64 tarball
bun run scripts/build-server.ts --platform=linux --arch=x64 --compress
# Build Linux ARM64 tarball
bun run scripts/build-server.ts --platform=linux --arch=arm64 --compress
`);
}
// ---------------------------------------------------------------------------
// Resource assembly
// ---------------------------------------------------------------------------
function assembleResources(config: ServerBuildConfig): void {
const { electronDir, outputDir, platform, arch } = config;
const srcResources = join(electronDir, 'resources');
const destResources = join(outputDir, 'resources');
console.log(' Copying docs, themes, permissions, tool-icons...');
for (const dir of ['docs', 'themes', 'permissions', 'tool-icons']) {
const src = join(srcResources, dir);
if (existsSync(src)) {
cpSync(src, join(destResources, dir), { recursive: true });
}
}
// Config defaults
const configDefaults = join(srcResources, 'config-defaults.json');
if (existsSync(configDefaults)) {
copyFileSync(configDefaults, join(destResources, 'config-defaults.json'));
}
// Python scripts (skip tests/)
console.log(' Copying Python scripts...');
const scriptsDir = join(srcResources, 'scripts');
const destScripts = join(destResources, 'scripts');
mkdirSync(destScripts, { recursive: true });
if (existsSync(scriptsDir)) {
for (const entry of readdirSync(scriptsDir)) {
if (entry === 'tests') continue;
const src = join(scriptsDir, entry);
const stat = lstatSync(src);
if (stat.isFile()) {
copyFileSync(src, join(destScripts, entry));
}
}
}
// Shell wrappers for doc tools (not .cmd files — those are Windows-only)
console.log(' Copying doc tool wrappers...');
const binDir = join(destResources, 'bin');
mkdirSync(binDir, { recursive: true });
const srcBin = join(srcResources, 'bin');
if (existsSync(srcBin)) {
for (const entry of readdirSync(srcBin)) {
const src = join(srcBin, entry);
const stat = lstatSync(src);
// Only copy files (not platform directories), skip .cmd
if (stat.isFile() && !entry.endsWith('.cmd')) {
copyFileSync(src, join(binDir, entry));
}
}
}
// MCP servers
console.log(' Copying MCP servers...');
for (const server of ['session-mcp-server', 'bridge-mcp-server']) {
const src = join(srcResources, server);
if (existsSync(src)) {
cpSync(src, join(destResources, server), { recursive: true });
}
}
// Also copy session-mcp-server from packages/ build output (dev path fallback)
const sessionServerDist = join(config.rootDir, 'packages', 'session-mcp-server', 'dist', 'index.js');
if (existsSync(sessionServerDist)) {
const destSessionServer = join(destResources, 'session-mcp-server');
mkdirSync(destSessionServer, { recursive: true });
copyFileSync(sessionServerDist, join(destSessionServer, 'index.js'));
}
}
// ---------------------------------------------------------------------------
// uv binary — download into output dir (not electron resources)
// ---------------------------------------------------------------------------
async function downloadUvForServer(config: ServerBuildConfig): Promise<void> {
const { platform, arch, outputDir, skipDownload } = config;
const uvDest = join(outputDir, 'resources', 'bin', 'uv');
if (skipDownload && existsSync(uvDest)) {
console.log(' uv already present, skipping download');
return;
}
// Use common.ts downloadUv which writes to electronDir/resources/bin/{platform-arch}/
// Then we'll copy the binary to our flat layout
const platformKey = getPlatformKey(platform, arch);
const electronUvPath = join(config.electronDir, 'resources', 'bin', platformKey, 'uv');
if (!existsSync(electronUvPath)) {
// Download using the shared helper
const buildConfig: BuildConfig = {
platform,
arch,
upload: false,
uploadLatest: false,
uploadScript: false,
rootDir: config.rootDir,
electronDir: config.electronDir,
};
await downloadUv(buildConfig);
}
if (!existsSync(electronUvPath)) {
throw new Error(`uv binary not found after download at ${electronUvPath}`);
}
// Flatten: copy to resources/bin/uv (no platform subdirectory in server dist)
const binDir = join(outputDir, 'resources', 'bin');
mkdirSync(binDir, { recursive: true });
copyFileSync(electronUvPath, uvDest);
await $`chmod +x ${uvDest}`.quiet();
console.log(` uv binary installed (${(lstatSync(uvDest).size / 1024 / 1024).toFixed(1)} MB)`);
}
// ---------------------------------------------------------------------------
// Bun runtime — download into output dir
// ---------------------------------------------------------------------------
async function downloadBunForServer(config: ServerBuildConfig): Promise<void> {
const { platform, arch, outputDir, skipDownload } = config;
const runtimeDir = join(outputDir, 'vendor', 'bun');
const bunDest = join(runtimeDir, 'bun');
if (skipDownload && existsSync(bunDest)) {
console.log(' Bun already present, skipping download');
return;
}
// Download to electron's vendor dir using shared helper, then copy
const buildConfig: BuildConfig = {
platform,
arch,
upload: false,
uploadLatest: false,
uploadScript: false,
rootDir: config.rootDir,
electronDir: config.electronDir,
};
await downloadBun(buildConfig);
const electronBunPath = join(config.electronDir, 'vendor', 'bun', 'bun');
if (!existsSync(electronBunPath)) {
throw new Error(`Bun binary not found after download at ${electronBunPath}`);
}
mkdirSync(runtimeDir, { recursive: true });
copyFileSync(electronBunPath, bunDest);
await $`chmod +x ${bunDest}`.quiet();
console.log(` Bun runtime installed (${(lstatSync(bunDest).size / 1024 / 1024).toFixed(1)} MB)`);
}
// ---------------------------------------------------------------------------
// Production node_modules
// ---------------------------------------------------------------------------
/**
* Recursively resolve and copy a package and its entire dependency tree.
* Reads each package's package.json to discover transitive deps.
*/
function copyDependencyTree(
dep: string,
srcModules: string,
destModules: string,
visited: Set<string>,
): void {
if (visited.has(dep)) return;
visited.add(dep);
const src = join(srcModules, dep);
if (!existsSync(src)) return;
// Ensure scope directory exists
if (dep.startsWith('@')) {
const scope = dep.split('/')[0]!;
mkdirSync(join(destModules, scope), { recursive: true });
}
const dest = join(destModules, dep);
mkdirSync(dirname(dest), { recursive: true });
cpSync(src, dest, { recursive: true, dereference: true });
// Recurse into dependencies
const pkgPath = join(src, 'package.json');
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
for (const childDep of Object.keys(pkg.dependencies || {})) {
copyDependencyTree(childDep, srcModules, destModules, visited);
}
} catch {
// Skip if package.json is malformed
}
}
}
/**
* Scan all .ts files in a directory tree for import/require statements
* and return the set of external npm package names (not relative paths,
* not node: builtins, not workspace @craft-agent/* packages).
*/
function scanImports(dir: string): Set<string> {
const packages = new Set<string>();
// Match: import ... from 'pkg', require('pkg'), import('pkg')
const importRe = /(?:from\s+['"]|require\s*\(\s*['"]|import\s*\(\s*['"])([^'"]+)['"]/g;
function walk(d: string): void {
if (!existsSync(d)) return;
for (const entry of readdirSync(d, { withFileTypes: true })) {
const full = join(d, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'tests' && entry.name !== '__tests__') {
walk(full);
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
const content = readFileSync(full, 'utf-8');
let match: RegExpExecArray | null;
while ((match = importRe.exec(content)) !== null) {
const spec = match[1]!;
// Skip relative imports, node: builtins, workspace packages
if (spec.startsWith('.') || spec.startsWith('node:') || spec.startsWith('@craft-agent/')) continue;
// Extract package name (handle scoped: @scope/name)
const parts = spec.split('/');
const pkgName = spec.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0]!;
packages.add(pkgName);
}
}
}
}
walk(dir);
return packages;
}
function copyProductionDeps(config: ServerBuildConfig): void {
const { rootDir, outputDir, platform, arch } = config;
const srcModules = join(rootDir, 'node_modules');
const destModules = join(outputDir, 'node_modules');
// Track all copied packages to avoid duplicates
const copied = new Set<string>();
// -------------------------------------------------------------------------
// 1. Scan source code for ALL external imports across server packages
// This catches everything — declared deps, undeclared deps, transitive
// imports that happen to work due to hoisting. No more whack-a-mole.
// -------------------------------------------------------------------------
// messaging-gateway is included so its runtime deps (grammy, etc.) land in node_modules.
// messaging-whatsapp-worker is intentionally OMITTED: Baileys and its transitive deps
// are bundled directly into packages/messaging-whatsapp-worker/dist/worker.cjs by
// scripts/build-wa-worker.ts — pulling them into node_modules would duplicate the tree.
const SERVER_PACKAGES = ['server', 'server-core', 'shared', 'core', 'session-tools-core', 'session-mcp-server', 'messaging-gateway'];
const allImports = new Set<string>();
for (const pkg of SERVER_PACKAGES) {
const pkgSrc = join(rootDir, 'packages', pkg, 'src');
const imports = scanImports(pkgSrc);
for (const imp of imports) allImports.add(imp);
}
console.log(` Found ${allImports.size} external packages referenced in source`);
// Also include declared dependencies (catches deps used only at runtime / dynamically)
for (const pkg of SERVER_PACKAGES) {
const pkgJsonPath = join(rootDir, 'packages', pkg, 'package.json');
if (!existsSync(pkgJsonPath)) continue;
try {
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
for (const [dep, version] of Object.entries(pkgJson.dependencies || {}) as [string, string][]) {
if (typeof version === 'string' && version.startsWith('workspace:')) continue;
allImports.add(dep);
}
// Also peer dependencies (they're real runtime deps)
for (const dep of Object.keys(pkgJson.peerDependencies || {})) {
allImports.add(dep);
}
} catch { /* skip */ }
}
// Copy each discovered package and its full transitive dependency tree
for (const dep of allImports) {
copyDependencyTree(dep, srcModules, destModules, copied);
}
console.log(` Source imports + declared deps: ${copied.size} packages`);
// -------------------------------------------------------------------------
// 2. Platform-specific native binaries (optionalDependencies, not in dep trees)
// -------------------------------------------------------------------------
const PLATFORM_DEPS = [
`@img/sharp-${platform === 'darwin' ? 'darwin' : 'linux'}-${arch}`,
`@img/sharp-libvips-${platform === 'darwin' ? 'darwin' : 'linux'}-${arch}`,
'@img/colour',
];
for (const dep of PLATFORM_DEPS) {
if (copied.has(dep)) continue;
const src = join(srcModules, dep);
if (!existsSync(src)) {
console.log(` Skipping ${dep} (not installed for current platform)`);
continue;
}
if (dep.startsWith('@')) {
const scope = dep.split('/')[0]!;
mkdirSync(join(destModules, scope), { recursive: true });
}
mkdirSync(dirname(join(destModules, dep)), { recursive: true });
cpSync(src, join(destModules, dep), { recursive: true, dereference: true });
copied.add(dep);
}
console.log(` Total: ${copied.size} packages copied to node_modules`);
}
function getDirSize(dir: string): number {
let size = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isFile()) {
size += lstatSync(fullPath).size;
} else if (entry.isDirectory()) {
size += getDirSize(fullPath);
}
}
return size;
}
// ---------------------------------------------------------------------------
// Workspace packages
// ---------------------------------------------------------------------------
function copyWorkspacePackages(config: ServerBuildConfig): void {
const { rootDir, outputDir } = config;
// messaging-whatsapp-worker is included so dist/worker.cjs (built in step 4) ships.
// The worker is spawned as a Node subprocess against that file at runtime; see
// CRAFT_MESSAGING_WA_WORKER env resolution in packages/server/src/index.ts.
const packages = [
'server',
'server-core',
'shared',
'core',
'session-tools-core',
'session-mcp-server',
'messaging-gateway',
'messaging-whatsapp-worker',
];
for (const pkg of packages) {
const src = join(rootDir, 'packages', pkg);
const dest = join(outputDir, 'packages', pkg);
if (!existsSync(src)) {
console.warn(` Warning: package ${pkg} not found`);
continue;
}
mkdirSync(dest, { recursive: true });
// Copy package.json
copyFileSync(join(src, 'package.json'), join(dest, 'package.json'));
// Copy tsconfig.json if present
const tsconfig = join(src, 'tsconfig.json');
if (existsSync(tsconfig)) {
copyFileSync(tsconfig, join(dest, 'tsconfig.json'));
}
// Copy src/ directory
const srcDir = join(src, 'src');
if (existsSync(srcDir)) {
cpSync(srcDir, join(dest, 'src'), { recursive: true });
}
// Copy dist/ directory if present (built artifacts like session-mcp-server)
const distDir = join(src, 'dist');
if (existsSync(distDir)) {
cpSync(distDir, join(dest, 'dist'), { recursive: true });
}
}
}
// ---------------------------------------------------------------------------
// Root config files for workspace resolution
// ---------------------------------------------------------------------------
function createRootConfig(config: ServerBuildConfig): void {
const { outputDir, version } = config;
// Root package.json with workspaces (Bun resolves @craft-agent/* through this)
const rootPkg = {
name: 'craft-server-dist',
version,
private: true,
workspaces: ['packages/*'],
};
writeFileSync(join(outputDir, 'package.json'), JSON.stringify(rootPkg, null, 2) + '\n');
// Root tsconfig.json for path resolution
const rootTsconfig = {
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'bundler',
paths: {
'@craft-agent/server-core/*': ['./packages/server-core/src/*'],
'@craft-agent/shared/*': ['./packages/shared/src/*'],
'@craft-agent/core/*': ['./packages/core/src/*'],
'@craft-agent/session-tools-core/*': ['./packages/session-tools-core/src/*'],
},
},
};
writeFileSync(join(outputDir, 'tsconfig.json'), JSON.stringify(rootTsconfig, null, 2) + '\n');
// Create workspace symlinks in node_modules/@craft-agent/
// Bun needs these to resolve workspace package imports at runtime
const scopeDir = join(outputDir, 'node_modules', '@craft-agent');
mkdirSync(scopeDir, { recursive: true });
const packagesDir = join(outputDir, 'packages');
if (existsSync(packagesDir)) {
for (const pkg of readdirSync(packagesDir)) {
const pkgJsonPath = join(packagesDir, pkg, 'package.json');
if (!existsSync(pkgJsonPath)) continue;
try {
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
const name: string = pkgJson.name || '';
if (name.startsWith('@craft-agent/')) {
const shortName = name.replace('@craft-agent/', '');
const linkPath = join(scopeDir, shortName);
const target = join('..', '..', 'packages', pkg);
if (!existsSync(linkPath)) {
symlinkSync(target, linkPath, 'dir');
console.log(` Symlink: ${name} -> packages/${pkg}`);
}
}
} catch {
// Skip if package.json is malformed
}
}
}
}
// ---------------------------------------------------------------------------
// Entry scripts
// ---------------------------------------------------------------------------
function createEntryScripts(config: ServerBuildConfig): void {
const { outputDir } = config;
const binDir = join(outputDir, 'bin');
mkdirSync(binDir, { recursive: true });
// bin/craft-server — main entry wrapper
const craftServer = `#!/bin/sh
set -e
# Resolve the distribution root
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
# Set environment for resource resolution
export CRAFT_BUNDLED_ASSETS_ROOT="$ROOT"
export CRAFT_IS_PACKAGED=true
export CRAFT_APP_ROOT="$ROOT"
export CRAFT_RESOURCES_PATH="$ROOT/resources"
# CLI tools (doc tools use uv + Python scripts)
export CRAFT_UV="$ROOT/resources/bin/uv"
export CRAFT_SCRIPTS="$ROOT/resources/scripts"
# Prepend resource bin to PATH (makes doc tool wrappers available)
export PATH="$ROOT/resources/bin:$ROOT/vendor/bun:$PATH"
# Use bundled Bun runtime
exec "$ROOT/vendor/bun/bun" run "$ROOT/packages/server/src/index.ts" "$@"
`;
writeFileSync(join(binDir, 'craft-server'), craftServer);
// start.sh — convenience entry
const startSh = `#!/bin/sh
# Qwen Code Server — convenience entry point
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DIR/bin/craft-server" "$@"
`;
writeFileSync(join(outputDir, 'start.sh'), startSh);
// install.sh — setup + optional systemd
const installSh = `#!/bin/bash
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Qwen Code Server Setup ==="
echo ""
# Make binaries executable
chmod +x "$DIR/bin/craft-server" "$DIR/start.sh"
[ -f "$DIR/vendor/bun/bun" ] && chmod +x "$DIR/vendor/bun/bun"
[ -f "$DIR/resources/bin/uv" ] && chmod +x "$DIR/resources/bin/uv"
# Make doc tool wrappers executable
for wrapper in "$DIR/resources/bin/"*; do
[ -f "$wrapper" ] && chmod +x "$wrapper"
done
echo "Binaries configured."
# Generate token if not set
if [ -z "\${CRAFT_SERVER_TOKEN:-}" ]; then
TOKEN=\$(openssl rand -hex 32)
cat > "$DIR/.env" <<ENVFILE
CRAFT_SERVER_TOKEN=$TOKEN
# TLS — uncomment and set paths to enable wss://
# CRAFT_RPC_TLS_CERT=/path/to/cert.pem
# CRAFT_RPC_TLS_KEY=/path/to/key.pem
# CRAFT_RPC_TLS_CA=/path/to/ca.pem
ENVFILE
echo ""
echo "Generated server token (saved to $DIR/.env)"
else
TOKEN="\$CRAFT_SERVER_TOKEN"
echo ""
echo "Using CRAFT_SERVER_TOKEN from environment."
fi
# Systemd installation
if [ "\${1:-}" = "--systemd" ]; then
if [ "\$(id -u)" -ne 0 ]; then
echo "Error: --systemd requires root. Run with sudo."
exit 1
fi
SERVICE_USER="\${CRAFT_USER:-\$(logname 2>/dev/null || echo craft)}"
SERVICE_FILE="/etc/systemd/system/craft-server.service"
cat > "$SERVICE_FILE" <<UNIT
[Unit]
Description=Qwen Code Server
After=network.target
[Service]
Type=simple
User=$SERVICE_USER
WorkingDirectory=$DIR
EnvironmentFile=$DIR/.env
Environment=CRAFT_RPC_HOST=127.0.0.1
Environment=CRAFT_RPC_PORT=9100
ExecStart=$DIR/bin/craft-server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable craft-server
echo ""
echo "Systemd service installed."
echo " Start: sudo systemctl start craft-server"
echo " Status: sudo systemctl status craft-server"
echo " Logs: journalctl -u craft-server -f"
echo ""
exit 0
fi
echo ""
echo "Quick start:"
echo " CRAFT_SERVER_TOKEN=$TOKEN $DIR/start.sh"
echo ""
echo "Or with systemd:"
echo " sudo $DIR/install.sh --systemd"
echo ""
`;
writeFileSync(join(outputDir, 'install.sh'), installSh);
// Make scripts executable at build time
for (const script of [
join(binDir, 'craft-server'),
join(outputDir, 'start.sh'),
join(outputDir, 'install.sh'),
]) {
chmodSync(script, 0o755);
}
}
// ---------------------------------------------------------------------------
// Docker
// ---------------------------------------------------------------------------
function createDockerFiles(config: ServerBuildConfig): void {
const { outputDir, version } = config;
const dockerfile = `FROM oven/bun:1.3-slim
WORKDIR /app
# Copy pre-assembled server distribution
COPY . .
# Make binaries executable
RUN chmod +x bin/craft-server vendor/bun/bun resources/bin/uv && \\
for f in resources/bin/*; do [ -f "$f" ] && chmod +x "$f"; done
ENV CRAFT_IS_PACKAGED=true
ENV CRAFT_BUNDLED_ASSETS_ROOT=/app
ENV CRAFT_APP_ROOT=/app
ENV CRAFT_RESOURCES_PATH=/app/resources
ENV CRAFT_UV=/app/resources/bin/uv
ENV CRAFT_SCRIPTS=/app/resources/scripts
ENV CRAFT_RPC_HOST=0.0.0.0
ENV CRAFT_RPC_PORT=9100
ENV PATH="/app/resources/bin:/app/vendor/bun:\${PATH}"
EXPOSE 9100
ENTRYPOINT ["/app/bin/craft-server"]
`;
writeFileSync(join(outputDir, 'Dockerfile'), dockerfile);
const dockerCompose = `version: "3.8"
services:
craft-server:
build: .
ports:
- "9100:9100"
environment:
- CRAFT_SERVER_TOKEN=\${CRAFT_SERVER_TOKEN:?Set CRAFT_SERVER_TOKEN}
- CRAFT_RPC_PORT=9100
# TLS — uncomment to enable wss://
# - CRAFT_RPC_TLS_CERT=/certs/cert.pem
# - CRAFT_RPC_TLS_KEY=/certs/key.pem
volumes:
- craft-data:/root/.craft-agent
# TLS — mount cert directory
# - ./certs:/certs:ro
restart: unless-stopped
volumes:
craft-data:
`;
writeFileSync(join(outputDir, 'docker-compose.yml'), dockerCompose);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
platform: { type: 'string', default: process.platform },
arch: { type: 'string', default: process.arch === 'arm64' ? 'arm64' : 'x64' },
output: { type: 'string', default: 'dist/server' },
compress: { type: 'boolean', default: false },
'skip-download': { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
allowPositionals: true,
});
if (values.help) {
showHelp();
process.exit(0);
}
const platform = values.platform as ServerPlatform;
if (platform !== 'darwin' && platform !== 'linux') {
console.error(`Unsupported platform: ${platform}. Use darwin or linux.`);
process.exit(1);
}
const arch = values.arch as Arch;
if (arch !== 'x64' && arch !== 'arm64') {
console.error(`Unsupported arch: ${arch}. Use x64 or arm64.`);
process.exit(1);
}
const scriptDir = dirname(fileURLToPath(import.meta.url));
const rootDir = dirname(scriptDir);
const electronDir = join(rootDir, 'apps', 'electron');
if (!existsSync(join(rootDir, 'package.json'))) {
console.error('Must run from the repository root');
process.exit(1);
}
// Read version from electron package.json
const electronPkg = JSON.parse(readFileSync(join(electronDir, 'package.json'), 'utf-8'));
const version: string = electronPkg.version;
const outputDir = join(rootDir, values.output!);
const config: ServerBuildConfig = {
platform,
arch,
rootDir,
electronDir,
outputDir,
compress: values.compress ?? false,
skipDownload: values['skip-download'] ?? false,
version,
};
console.log(`=== Building Qwen Code Server ${version} for ${platform}-${arch} ===`);
console.log(` Output: ${outputDir}`);
// Step 1: Clean
console.log('\n[1/8] Cleaning output directory...');
if (existsSync(outputDir)) {
rmSync(outputDir, { recursive: true, force: true });
}
mkdirSync(outputDir, { recursive: true });
// Step 2: Download Bun runtime
console.log(`\n[2/8] Downloading Bun ${BUN_VERSION}...`);
await downloadBunForServer(config);
// Step 3: Download uv
console.log(`\n[3/8] Downloading uv ${UV_VERSION}...`);
await downloadUvForServer(config);
// Step 4: Build MCP servers
console.log('\n[4/8] Building MCP servers...');
const buildConfig: BuildConfig = {
platform,
arch,
upload: false,
uploadLatest: false,
uploadScript: false,
rootDir,
electronDir,
};
buildMcpServers(buildConfig);
// Build the WhatsApp worker bundle. Must happen before copyWorkspacePackages
// so dist/worker.cjs exists when we copy the messaging-whatsapp-worker package.
// The bundle embeds Baileys + transitive deps; see scripts/build-wa-worker.ts.
console.log(' Building WhatsApp worker bundle...');
await $`bun run ${join(rootDir, 'scripts', 'build-wa-worker.ts')}`.cwd(rootDir);
// Step 5: Assemble resources
console.log('\n[5/8] Assembling resources...');
assembleResources(config);
// Step 6: Copy production node_modules
console.log('\n[6/8] Copying production dependencies...');
copyProductionDeps(config);
// Step 7: Copy workspace packages
console.log('\n[7/8] Copying workspace packages...');
copyWorkspacePackages(config);
createRootConfig(config);
// Step 8: Create entry scripts + Docker files
console.log('\n[8/8] Creating entry scripts...');
createEntryScripts(config);
createDockerFiles(config);
// Calculate total size
const totalSize = getDirSize(outputDir);
console.log(`\n Assembly complete: ${(totalSize / 1024 / 1024).toFixed(0)} MB`);
// Compress if requested
if (config.compress) {
const archiveName = `craft-server-${version}-${platform}-${arch}.tar.gz`;
const archivePath = join(dirname(outputDir), archiveName);
console.log(`\nCompressing to ${archiveName}...`);
await $`tar -czf ${archivePath} -C ${outputDir} .`;
const archiveSize = lstatSync(archivePath).size;
console.log(` Archive: ${(archiveSize / 1024 / 1024).toFixed(0)} MB`);
console.log(` Path: ${archivePath}`);
}
console.log('\n Build completed successfully!');
console.log(`\nQuick start:`);
console.log(` CRAFT_SERVER_TOKEN=<secret> ${outputDir}/start.sh`);
}
main();