thinkwell 0.4.5 → 0.5.0-alpha.2
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/README.md +3 -5
- package/bin/thinkwell +130 -174
- package/dist/agent.d.ts +82 -56
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +178 -174
- package/dist/agent.js.map +1 -1
- package/dist/cli/build.d.ts +15 -43
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +199 -1231
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/bundle.d.ts +61 -0
- package/dist/cli/bundle.d.ts.map +1 -0
- package/dist/cli/bundle.js +1299 -0
- package/dist/cli/bundle.js.map +1 -0
- package/dist/cli/check.d.ts +19 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +248 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/commands.d.ts +30 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +64 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/compiler-host.d.ts +109 -0
- package/dist/cli/compiler-host.d.ts.map +1 -0
- package/dist/cli/compiler-host.js +173 -0
- package/dist/cli/compiler-host.js.map +1 -0
- package/dist/cli/fmt.d.ts +13 -0
- package/dist/cli/fmt.d.ts.map +1 -0
- package/dist/cli/fmt.js +14 -0
- package/dist/cli/fmt.js.map +1 -0
- package/dist/cli/init-command.js +12 -12
- package/dist/cli/init-command.js.map +1 -1
- package/dist/cli/loader.d.ts +0 -21
- package/dist/cli/loader.d.ts.map +1 -1
- package/dist/cli/loader.js +1 -50
- package/dist/cli/loader.js.map +1 -1
- package/dist/cli/schema.d.ts +2 -0
- package/dist/cli/schema.d.ts.map +1 -1
- package/dist/cli/schema.js +11 -4
- package/dist/cli/schema.js.map +1 -1
- package/dist/cli/workspace.d.ts +82 -0
- package/dist/cli/workspace.d.ts.map +1 -0
- package/dist/cli/workspace.js +248 -0
- package/dist/cli/workspace.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/think-builder.d.ts +50 -2
- package/dist/think-builder.d.ts.map +1 -1
- package/dist/think-builder.js +137 -14
- package/dist/think-builder.js.map +1 -1
- package/dist/thought-event.d.ts +80 -0
- package/dist/thought-event.d.ts.map +1 -0
- package/dist/thought-event.js +2 -0
- package/dist/thought-event.js.map +1 -0
- package/dist/thought-stream.d.ts +45 -0
- package/dist/thought-stream.d.ts.map +1 -0
- package/dist/thought-stream.js +99 -0
- package/dist/thought-stream.js.map +1 -0
- package/dist-pkg/acp.cjs +37 -11
- package/dist-pkg/thinkwell.cjs +49 -18
- package/package.json +4 -9
- package/dist/cli/index.d.ts +0 -11
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -11
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/main-pkg.d.ts +0 -18
- package/dist/cli/main-pkg.d.ts.map +0 -1
- package/dist/cli/main.d.ts +0 -14
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -256
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/types-command.d.ts +0 -8
- package/dist/cli/types-command.d.ts.map +0 -1
- package/dist/cli/types-command.js +0 -110
- package/dist/cli/types-command.js.map +0 -1
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle command for creating self-contained executables from user scripts.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the `thinkwell bundle` command that compiles user scripts
|
|
5
|
+
* into standalone binaries using the same pkg-based tooling as the thinkwell CLI.
|
|
6
|
+
*
|
|
7
|
+
* The bundle process follows a two-stage pipeline:
|
|
8
|
+
* 1. **Pre-bundle with esbuild** - Bundle user script + thinkwell packages into CJS
|
|
9
|
+
* 2. **Compile with pkg** - Create self-contained binary with Node.js runtime
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync, copyFileSync, chmodSync, createWriteStream, watch as fsWatch, } from "node:fs";
|
|
12
|
+
import { dirname, resolve, basename, join, isAbsolute } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { styleText } from "node:util";
|
|
15
|
+
import { homedir, tmpdir } from "node:os";
|
|
16
|
+
import { cyan, cyanBold, greenBold, whiteBold, dim } from "./fmt.js";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { spawn, execSync } from "node:child_process";
|
|
19
|
+
import * as esbuild from "esbuild";
|
|
20
|
+
import { transformJsonSchemas, hasJsonSchemaMarkers } from "./schema.js";
|
|
21
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
22
|
+
const SPINNER_INTERVAL = 80;
|
|
23
|
+
function createSpinnerImpl(options) {
|
|
24
|
+
let text = options.text;
|
|
25
|
+
let interval;
|
|
26
|
+
let frameIndex = 0;
|
|
27
|
+
const isSilent = options.isSilent ?? false;
|
|
28
|
+
// Check TTY lazily - only when we actually try to render
|
|
29
|
+
const isTTY = () => process.stderr.isTTY === true;
|
|
30
|
+
const clearLine = () => {
|
|
31
|
+
if (isTTY()) {
|
|
32
|
+
process.stderr.write("\r\x1b[K");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const render = () => {
|
|
36
|
+
if (isSilent)
|
|
37
|
+
return;
|
|
38
|
+
if (isTTY()) {
|
|
39
|
+
const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
|
|
40
|
+
process.stderr.write(`\r${frame} ${text}`);
|
|
41
|
+
frameIndex++;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const spinner = {
|
|
45
|
+
get text() {
|
|
46
|
+
return text;
|
|
47
|
+
},
|
|
48
|
+
set text(value) {
|
|
49
|
+
text = value;
|
|
50
|
+
},
|
|
51
|
+
start(newText) {
|
|
52
|
+
if (newText)
|
|
53
|
+
text = newText;
|
|
54
|
+
if (isSilent)
|
|
55
|
+
return this;
|
|
56
|
+
if (isTTY()) {
|
|
57
|
+
render();
|
|
58
|
+
interval = setInterval(render, SPINNER_INTERVAL);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Non-TTY: just print the text with a dash prefix
|
|
62
|
+
process.stderr.write(`- ${text}\n`);
|
|
63
|
+
}
|
|
64
|
+
return this;
|
|
65
|
+
},
|
|
66
|
+
stop() {
|
|
67
|
+
if (interval) {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
interval = undefined;
|
|
70
|
+
}
|
|
71
|
+
clearLine();
|
|
72
|
+
return this;
|
|
73
|
+
},
|
|
74
|
+
succeed(successText) {
|
|
75
|
+
if (interval) {
|
|
76
|
+
clearInterval(interval);
|
|
77
|
+
interval = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (isSilent)
|
|
80
|
+
return this;
|
|
81
|
+
const finalText = successText ?? text;
|
|
82
|
+
if (isTTY()) {
|
|
83
|
+
process.stderr.write(`\r\x1b[K✔ ${finalText}\n`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
process.stderr.write(`✔ ${finalText}\n`);
|
|
87
|
+
}
|
|
88
|
+
return this;
|
|
89
|
+
},
|
|
90
|
+
fail(failText) {
|
|
91
|
+
if (interval) {
|
|
92
|
+
clearInterval(interval);
|
|
93
|
+
interval = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (isSilent)
|
|
96
|
+
return this;
|
|
97
|
+
const finalText = failText ?? text;
|
|
98
|
+
if (isTTY()) {
|
|
99
|
+
process.stderr.write(`\r\x1b[K✖ ${finalText}\n`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
process.stderr.write(`✖ ${finalText}\n`);
|
|
103
|
+
}
|
|
104
|
+
return this;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
return spinner;
|
|
108
|
+
}
|
|
109
|
+
// Handle both ESM and CJS contexts for __dirname
|
|
110
|
+
// When bundled to CJS, import.meta.url won't work, but global __dirname will
|
|
111
|
+
const __dirname = typeof import.meta?.url === "string"
|
|
112
|
+
? dirname(fileURLToPath(import.meta.url))
|
|
113
|
+
: globalThis.__dirname || dirname(process.argv[1]);
|
|
114
|
+
// Map user-friendly target names to pkg target names
|
|
115
|
+
const TARGET_MAP = {
|
|
116
|
+
"darwin-arm64": "node24-macos-arm64",
|
|
117
|
+
"darwin-x64": "node24-macos-x64",
|
|
118
|
+
"linux-x64": "node24-linux-x64",
|
|
119
|
+
"linux-arm64": "node24-linux-arm64",
|
|
120
|
+
};
|
|
121
|
+
// Detect the current host platform
|
|
122
|
+
function detectHostTarget() {
|
|
123
|
+
const platform = process.platform;
|
|
124
|
+
const arch = process.arch;
|
|
125
|
+
if (platform === "darwin" && arch === "arm64")
|
|
126
|
+
return "darwin-arm64";
|
|
127
|
+
if (platform === "darwin" && arch === "x64")
|
|
128
|
+
return "darwin-x64";
|
|
129
|
+
if (platform === "linux" && arch === "x64")
|
|
130
|
+
return "linux-x64";
|
|
131
|
+
if (platform === "linux" && arch === "arm64")
|
|
132
|
+
return "linux-arm64";
|
|
133
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}. ` +
|
|
134
|
+
`Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read build configuration from package.json in the given directory.
|
|
138
|
+
* Returns undefined if no configuration is found.
|
|
139
|
+
*/
|
|
140
|
+
function readPackageJsonConfig(dir) {
|
|
141
|
+
const pkgPath = join(dir, "package.json");
|
|
142
|
+
if (!existsSync(pkgPath)) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
147
|
+
const pkg = JSON.parse(content);
|
|
148
|
+
// Look for "thinkwell.bundle" configuration
|
|
149
|
+
const config = pkg?.thinkwell?.bundle;
|
|
150
|
+
if (!config || typeof config !== "object") {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
// Validate and extract configuration
|
|
154
|
+
const result = {};
|
|
155
|
+
if (typeof config.output === "string") {
|
|
156
|
+
result.output = config.output;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(config.targets)) {
|
|
159
|
+
const validTargets = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "host"];
|
|
160
|
+
result.targets = config.targets.filter((t) => typeof t === "string" && validTargets.includes(t));
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(config.include)) {
|
|
163
|
+
result.include = config.include.filter((i) => typeof i === "string");
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(config.external)) {
|
|
166
|
+
result.external = config.external.filter((e) => typeof e === "string");
|
|
167
|
+
}
|
|
168
|
+
if (typeof config.minify === "boolean") {
|
|
169
|
+
result.minify = config.minify;
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Ignore JSON parse errors
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Merge package.json configuration with CLI options.
|
|
180
|
+
* CLI options take precedence over package.json configuration.
|
|
181
|
+
*/
|
|
182
|
+
function mergeWithPackageConfig(options, entryDir) {
|
|
183
|
+
const pkgConfig = readPackageJsonConfig(entryDir);
|
|
184
|
+
if (!pkgConfig) {
|
|
185
|
+
return options;
|
|
186
|
+
}
|
|
187
|
+
// CLI options take precedence - only use package.json defaults for unset values
|
|
188
|
+
return {
|
|
189
|
+
...options,
|
|
190
|
+
output: options.output ?? pkgConfig.output,
|
|
191
|
+
targets: options.targets && options.targets.length > 0
|
|
192
|
+
? options.targets
|
|
193
|
+
: pkgConfig.targets ?? options.targets,
|
|
194
|
+
include: [
|
|
195
|
+
...(pkgConfig.include || []),
|
|
196
|
+
...(options.include || []),
|
|
197
|
+
],
|
|
198
|
+
external: [
|
|
199
|
+
...(pkgConfig.external || []),
|
|
200
|
+
...(options.external || []),
|
|
201
|
+
],
|
|
202
|
+
minify: options.minify ?? pkgConfig.minify,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Parse and validate build options from command-line arguments.
|
|
207
|
+
*/
|
|
208
|
+
export function parseBundleArgs(args) {
|
|
209
|
+
const options = {
|
|
210
|
+
entry: "",
|
|
211
|
+
targets: [],
|
|
212
|
+
include: [],
|
|
213
|
+
external: [],
|
|
214
|
+
};
|
|
215
|
+
let i = 0;
|
|
216
|
+
while (i < args.length) {
|
|
217
|
+
const arg = args[i];
|
|
218
|
+
if (arg === "-o" || arg === "--output") {
|
|
219
|
+
i++;
|
|
220
|
+
if (i >= args.length) {
|
|
221
|
+
throw new Error("Missing value for --output");
|
|
222
|
+
}
|
|
223
|
+
options.output = args[i];
|
|
224
|
+
}
|
|
225
|
+
else if (arg === "-t" || arg === "--target") {
|
|
226
|
+
i++;
|
|
227
|
+
if (i >= args.length) {
|
|
228
|
+
throw new Error("Missing value for --target");
|
|
229
|
+
}
|
|
230
|
+
const target = args[i];
|
|
231
|
+
const validTargets = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "host"];
|
|
232
|
+
if (!validTargets.includes(target)) {
|
|
233
|
+
throw new Error(`Invalid target '${target}'. Valid targets: ${validTargets.join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
options.targets.push(target);
|
|
236
|
+
}
|
|
237
|
+
else if (arg === "--include") {
|
|
238
|
+
i++;
|
|
239
|
+
if (i >= args.length) {
|
|
240
|
+
throw new Error("Missing value for --include");
|
|
241
|
+
}
|
|
242
|
+
options.include.push(args[i]);
|
|
243
|
+
}
|
|
244
|
+
else if (arg === "--external" || arg === "-e") {
|
|
245
|
+
i++;
|
|
246
|
+
if (i >= args.length) {
|
|
247
|
+
throw new Error("Missing value for --external");
|
|
248
|
+
}
|
|
249
|
+
options.external.push(args[i]);
|
|
250
|
+
}
|
|
251
|
+
else if (arg === "--verbose" || arg === "-v") {
|
|
252
|
+
options.verbose = true;
|
|
253
|
+
}
|
|
254
|
+
else if (arg === "--quiet" || arg === "-q") {
|
|
255
|
+
options.quiet = true;
|
|
256
|
+
}
|
|
257
|
+
else if (arg === "--dry-run" || arg === "-n") {
|
|
258
|
+
options.dryRun = true;
|
|
259
|
+
}
|
|
260
|
+
else if (arg === "--minify" || arg === "-m") {
|
|
261
|
+
options.minify = true;
|
|
262
|
+
}
|
|
263
|
+
else if (arg === "--watch" || arg === "-w") {
|
|
264
|
+
options.watch = true;
|
|
265
|
+
}
|
|
266
|
+
else if (arg.startsWith("-")) {
|
|
267
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Positional argument - entry file
|
|
271
|
+
if (options.entry) {
|
|
272
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
273
|
+
}
|
|
274
|
+
options.entry = arg;
|
|
275
|
+
}
|
|
276
|
+
i++;
|
|
277
|
+
}
|
|
278
|
+
// Validate entry
|
|
279
|
+
if (!options.entry) {
|
|
280
|
+
throw new Error("No entry file specified");
|
|
281
|
+
}
|
|
282
|
+
// Default target is host
|
|
283
|
+
if (options.targets.length === 0) {
|
|
284
|
+
options.targets = ["host"];
|
|
285
|
+
}
|
|
286
|
+
return options;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Initialize the build context with resolved paths and validated inputs.
|
|
290
|
+
*/
|
|
291
|
+
function initBundleContext(options) {
|
|
292
|
+
// Resolve entry path
|
|
293
|
+
const entryPath = isAbsolute(options.entry)
|
|
294
|
+
? options.entry
|
|
295
|
+
: resolve(process.cwd(), options.entry);
|
|
296
|
+
if (!existsSync(entryPath)) {
|
|
297
|
+
const suggestion = options.entry.endsWith(".ts") || options.entry.endsWith(".js")
|
|
298
|
+
? ""
|
|
299
|
+
: "\n Did you mean to add a .ts or .js extension?";
|
|
300
|
+
throw new Error(`Entry file not found: ${options.entry}${suggestion}\n` +
|
|
301
|
+
` Working directory: ${process.cwd()}`);
|
|
302
|
+
}
|
|
303
|
+
const entryBasename = basename(entryPath).replace(/\.(ts|js|mts|mjs|cts|cjs)$/, "");
|
|
304
|
+
const entryDir = dirname(entryPath);
|
|
305
|
+
// Merge CLI options with package.json configuration
|
|
306
|
+
// Check both entry directory and current working directory for package.json
|
|
307
|
+
let mergedOptions = mergeWithPackageConfig(options, entryDir);
|
|
308
|
+
if (entryDir !== process.cwd()) {
|
|
309
|
+
mergedOptions = mergeWithPackageConfig(mergedOptions, process.cwd());
|
|
310
|
+
}
|
|
311
|
+
// Create build directory in system temp directory using mkdtempSync for atomicity
|
|
312
|
+
const buildDir = mkdtempSync(join(tmpdir(), `thinkwell-bundle-${entryBasename}-`));
|
|
313
|
+
// Find the thinkwell dist-pkg directory
|
|
314
|
+
// When running from npm install: node_modules/thinkwell/dist-pkg
|
|
315
|
+
// When running from source: packages/thinkwell/dist-pkg
|
|
316
|
+
const thinkwellDistPkg = resolve(__dirname, "../../dist-pkg");
|
|
317
|
+
if (!existsSync(thinkwellDistPkg)) {
|
|
318
|
+
throw new Error(`Thinkwell dist-pkg not found at ${thinkwellDistPkg}.\n` +
|
|
319
|
+
` This may indicate a corrupted installation.\n` +
|
|
320
|
+
` Try reinstalling thinkwell: npm install thinkwell`);
|
|
321
|
+
}
|
|
322
|
+
// Resolve "host" targets to actual platform
|
|
323
|
+
const resolvedTargets = mergedOptions.targets.map((t) => t === "host" ? detectHostTarget() : t);
|
|
324
|
+
// Deduplicate targets
|
|
325
|
+
const uniqueTargets = [...new Set(resolvedTargets)];
|
|
326
|
+
return {
|
|
327
|
+
entryPath,
|
|
328
|
+
entryBasename,
|
|
329
|
+
entryDir,
|
|
330
|
+
buildDir,
|
|
331
|
+
thinkwellDistPkg,
|
|
332
|
+
resolvedTargets: uniqueTargets,
|
|
333
|
+
options: mergedOptions,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Generate the output path for a given target.
|
|
338
|
+
*/
|
|
339
|
+
function getOutputPath(ctx, target) {
|
|
340
|
+
if (ctx.options.output) {
|
|
341
|
+
if (ctx.resolvedTargets.length === 1) {
|
|
342
|
+
// Single target: use exact output path
|
|
343
|
+
return isAbsolute(ctx.options.output)
|
|
344
|
+
? ctx.options.output
|
|
345
|
+
: resolve(process.cwd(), ctx.options.output);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// Multiple targets: append target suffix
|
|
349
|
+
const base = isAbsolute(ctx.options.output)
|
|
350
|
+
? ctx.options.output
|
|
351
|
+
: resolve(process.cwd(), ctx.options.output);
|
|
352
|
+
return `${base}-${target}`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Default: <entry-basename>-<target> in current directory
|
|
357
|
+
return resolve(process.cwd(), `${ctx.entryBasename}-${target}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Generate the wrapper entry point that sets up global.__bundled__.
|
|
362
|
+
*
|
|
363
|
+
* This creates a CJS file that:
|
|
364
|
+
* 1. Loads the pre-bundled thinkwell packages
|
|
365
|
+
* 2. Registers them in global.__bundled__
|
|
366
|
+
* 3. Loads and runs the user's bundled code
|
|
367
|
+
*/
|
|
368
|
+
function generateWrapperSource(userBundlePath) {
|
|
369
|
+
return `#!/usr/bin/env node
|
|
370
|
+
/**
|
|
371
|
+
* Generated wrapper for thinkwell bundle.
|
|
372
|
+
* This file is auto-generated - do not edit.
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
// Register bundled thinkwell packages
|
|
376
|
+
const thinkwell = require('./thinkwell.cjs');
|
|
377
|
+
const acpModule = require('./acp.cjs');
|
|
378
|
+
const protocolModule = require('./protocol.cjs');
|
|
379
|
+
|
|
380
|
+
global.__bundled__ = {
|
|
381
|
+
'thinkwell': thinkwell,
|
|
382
|
+
'@thinkwell/acp': acpModule,
|
|
383
|
+
'@thinkwell/protocol': protocolModule,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Load the user's bundled code
|
|
387
|
+
require('./${basename(userBundlePath)}');
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Stage 1: Bundle user script with esbuild.
|
|
392
|
+
*
|
|
393
|
+
* This bundles the user's entry point along with all its dependencies
|
|
394
|
+
* into a single CJS file. The thinkwell packages are marked as external
|
|
395
|
+
* since they'll be provided via global.__bundled__.
|
|
396
|
+
*/
|
|
397
|
+
async function bundleUserScript(ctx) {
|
|
398
|
+
const outputFile = join(ctx.buildDir, `${ctx.entryBasename}-bundle.cjs`);
|
|
399
|
+
if (ctx.options.verbose) {
|
|
400
|
+
console.log(` Bundling ${ctx.entryPath}...`);
|
|
401
|
+
}
|
|
402
|
+
// Note: When running from a compiled binary, ESBUILD_BINARY_PATH is set
|
|
403
|
+
// by main-pkg.cjs before this module loads.
|
|
404
|
+
try {
|
|
405
|
+
// Combine Node built-ins with user-specified external packages
|
|
406
|
+
const externalPackages = ["node:*", ...(ctx.options.external || [])];
|
|
407
|
+
await esbuild.build({
|
|
408
|
+
entryPoints: [ctx.entryPath],
|
|
409
|
+
bundle: true,
|
|
410
|
+
platform: "node",
|
|
411
|
+
format: "cjs",
|
|
412
|
+
outfile: outputFile,
|
|
413
|
+
// External: Node built-ins and user-specified packages
|
|
414
|
+
external: externalPackages,
|
|
415
|
+
// Mark thinkwell packages as external - they're provided via global.__bundled__
|
|
416
|
+
// But actually, we need to transform the imports, so let's bundle them
|
|
417
|
+
// and use a banner to set up the module aliases
|
|
418
|
+
banner: {
|
|
419
|
+
js: `
|
|
420
|
+
// Alias thinkwell packages to global.__bundled__
|
|
421
|
+
const __origRequire = require;
|
|
422
|
+
require = function(id) {
|
|
423
|
+
if (id === 'thinkwell') {
|
|
424
|
+
return global.__bundled__['thinkwell'];
|
|
425
|
+
}
|
|
426
|
+
if (id === '@thinkwell/acp') {
|
|
427
|
+
return global.__bundled__['@thinkwell/acp'];
|
|
428
|
+
}
|
|
429
|
+
if (id === '@thinkwell/protocol') {
|
|
430
|
+
return global.__bundled__['@thinkwell/protocol'];
|
|
431
|
+
}
|
|
432
|
+
return __origRequire(id);
|
|
433
|
+
};
|
|
434
|
+
require.resolve = __origRequire.resolve;
|
|
435
|
+
require.cache = __origRequire.cache;
|
|
436
|
+
require.extensions = __origRequire.extensions;
|
|
437
|
+
require.main = __origRequire.main;
|
|
438
|
+
`,
|
|
439
|
+
},
|
|
440
|
+
// Resolve thinkwell imports to bundled versions during bundle time
|
|
441
|
+
plugins: [
|
|
442
|
+
// Transform @JSONSchema types into namespace declarations with schema providers
|
|
443
|
+
{
|
|
444
|
+
name: "jsonschema-transformer",
|
|
445
|
+
setup(build) {
|
|
446
|
+
build.onLoad({ filter: /\.(ts|tsx|mts|cts)$/ }, async (args) => {
|
|
447
|
+
// Skip node_modules
|
|
448
|
+
if (args.path.includes("node_modules")) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const source = readFileSync(args.path, "utf-8");
|
|
452
|
+
// Fast path: skip files without @JSONSchema markers
|
|
453
|
+
if (!hasJsonSchemaMarkers(source)) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
// Transform the source to inject schema namespaces
|
|
457
|
+
const transformed = transformJsonSchemas(args.path, source);
|
|
458
|
+
return {
|
|
459
|
+
contents: transformed,
|
|
460
|
+
loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "thinkwell-resolver",
|
|
467
|
+
setup(build) {
|
|
468
|
+
// Mark thinkwell packages as external - provided via global.__bundled__ at runtime
|
|
469
|
+
build.onResolve({ filter: /^(thinkwell|@thinkwell\/(acp|protocol))$/ }, (args) => {
|
|
470
|
+
return { path: args.path, external: true };
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
sourcemap: false,
|
|
476
|
+
minify: ctx.options.minify ?? false,
|
|
477
|
+
keepNames: !ctx.options.minify, // Keep names unless minifying
|
|
478
|
+
target: "node24",
|
|
479
|
+
logLevel: ctx.options.verbose ? "info" : "silent",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
// Provide helpful error messages for common failures
|
|
484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
485
|
+
if (message.includes("Could not resolve")) {
|
|
486
|
+
const match = message.match(/Could not resolve "([^"]+)"/);
|
|
487
|
+
const moduleName = match ? match[1] : "unknown module";
|
|
488
|
+
throw new Error(`Could not resolve dependency "${moduleName}".\n` +
|
|
489
|
+
` Make sure all dependencies are installed: npm install\n` +
|
|
490
|
+
` If this is a dev dependency, it may need to be a regular dependency.`);
|
|
491
|
+
}
|
|
492
|
+
if (message.includes("No loader is configured")) {
|
|
493
|
+
throw new Error(`Unsupported file type in import.\n` +
|
|
494
|
+
` esbuild cannot bundle this file type by default.\n` +
|
|
495
|
+
` Consider using --include to embed the file as an asset instead.`);
|
|
496
|
+
}
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
return outputFile;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Copy thinkwell pre-bundled packages to build directory.
|
|
503
|
+
*/
|
|
504
|
+
function copyThinkwellBundles(ctx) {
|
|
505
|
+
const bundles = ["thinkwell.cjs", "acp.cjs", "protocol.cjs"];
|
|
506
|
+
for (const bundle of bundles) {
|
|
507
|
+
const src = join(ctx.thinkwellDistPkg, bundle);
|
|
508
|
+
const dest = join(ctx.buildDir, bundle);
|
|
509
|
+
if (!existsSync(src)) {
|
|
510
|
+
throw new Error(`Thinkwell bundle not found: ${src}`);
|
|
511
|
+
}
|
|
512
|
+
const content = readFileSync(src);
|
|
513
|
+
writeFileSync(dest, content);
|
|
514
|
+
if (ctx.options.verbose) {
|
|
515
|
+
console.log(` Copied ${bundle}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if running from a pkg-compiled binary.
|
|
521
|
+
*/
|
|
522
|
+
function isRunningFromCompiledBinary() {
|
|
523
|
+
// @ts-expect-error process.pkg is set by pkg at runtime
|
|
524
|
+
return typeof process.pkg !== "undefined";
|
|
525
|
+
}
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// Portable Node.js Download (for compiled binary builds)
|
|
528
|
+
// ============================================================================
|
|
529
|
+
/** Pinned Node.js version for portable runtime */
|
|
530
|
+
const PORTABLE_NODE_VERSION = "24.1.0";
|
|
531
|
+
/** Get the thinkwell cache directory */
|
|
532
|
+
function getCacheDir() {
|
|
533
|
+
return process.env.THINKWELL_CACHE_DIR || join(homedir(), ".cache", "thinkwell");
|
|
534
|
+
}
|
|
535
|
+
/** Get the thinkwell version from package.json */
|
|
536
|
+
function getThinkwellVersion() {
|
|
537
|
+
try {
|
|
538
|
+
const pkgPath = resolve(__dirname, "../../package.json");
|
|
539
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
540
|
+
return pkg.version || "unknown";
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return "unknown";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Map process.platform/arch to Node.js download format.
|
|
548
|
+
*/
|
|
549
|
+
function getNodePlatformArch() {
|
|
550
|
+
const platform = process.platform === "darwin" ? "darwin" : "linux";
|
|
551
|
+
const arch = process.arch; // x64 or arm64
|
|
552
|
+
return { platform, arch };
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Download a file from a URL with progress reporting.
|
|
556
|
+
*/
|
|
557
|
+
async function downloadFile(url, destPath, spinner) {
|
|
558
|
+
const response = await fetch(url);
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
561
|
+
}
|
|
562
|
+
const contentLength = response.headers.get("content-length");
|
|
563
|
+
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
564
|
+
// Ensure directory exists
|
|
565
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
566
|
+
const fileStream = createWriteStream(destPath);
|
|
567
|
+
const reader = response.body?.getReader();
|
|
568
|
+
if (!reader) {
|
|
569
|
+
throw new Error("No response body");
|
|
570
|
+
}
|
|
571
|
+
let downloadedBytes = 0;
|
|
572
|
+
try {
|
|
573
|
+
while (true) {
|
|
574
|
+
const { done, value } = await reader.read();
|
|
575
|
+
if (done)
|
|
576
|
+
break;
|
|
577
|
+
fileStream.write(Buffer.from(value));
|
|
578
|
+
downloadedBytes += value.length;
|
|
579
|
+
if (spinner && totalBytes > 0) {
|
|
580
|
+
const percent = Math.round((downloadedBytes / totalBytes) * 100);
|
|
581
|
+
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
|
|
582
|
+
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
|
|
583
|
+
spinner.text = `Downloading Node.js runtime... ${downloadedMB} MB / ${totalMB} MB (${percent}%)`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
fileStream.end();
|
|
589
|
+
}
|
|
590
|
+
// Wait for file to be fully written
|
|
591
|
+
await new Promise((resolve, reject) => {
|
|
592
|
+
fileStream.on("finish", resolve);
|
|
593
|
+
fileStream.on("error", reject);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Compute SHA-256 hash of a file.
|
|
598
|
+
*/
|
|
599
|
+
function hashFile(filePath) {
|
|
600
|
+
const content = readFileSync(filePath);
|
|
601
|
+
return createHash("sha256").update(content).digest("hex");
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Fetch the expected SHA-256 checksum for a Node.js download.
|
|
605
|
+
*/
|
|
606
|
+
async function fetchExpectedChecksum(version, filename) {
|
|
607
|
+
const url = `https://nodejs.org/dist/v${version}/SHASUMS256.txt`;
|
|
608
|
+
const response = await fetch(url);
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
throw new Error(`Failed to fetch checksums: ${response.status}`);
|
|
611
|
+
}
|
|
612
|
+
const text = await response.text();
|
|
613
|
+
for (const line of text.split("\n")) {
|
|
614
|
+
// Format: "hash filename"
|
|
615
|
+
const parts = line.trim().split(/\s+/);
|
|
616
|
+
if (parts.length === 2 && parts[1] === filename) {
|
|
617
|
+
return parts[0];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw new Error(`Checksum not found for ${filename}`);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Extract a .tar.gz archive using the system tar command.
|
|
624
|
+
*/
|
|
625
|
+
function extractTarGz(archivePath, destDir) {
|
|
626
|
+
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, {
|
|
627
|
+
stdio: "pipe",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Ensure portable Node.js is available in the cache.
|
|
632
|
+
*
|
|
633
|
+
* Downloads from nodejs.org if not cached, verifies checksum, and extracts.
|
|
634
|
+
* Returns the path to the node binary.
|
|
635
|
+
*/
|
|
636
|
+
async function ensurePortableNode(spinner) {
|
|
637
|
+
const version = PORTABLE_NODE_VERSION;
|
|
638
|
+
const { platform, arch } = getNodePlatformArch();
|
|
639
|
+
const cacheDir = join(getCacheDir(), "node", `v${version}`);
|
|
640
|
+
const nodeBinary = process.platform === "win32" ? "node.exe" : "node";
|
|
641
|
+
const nodePath = join(cacheDir, nodeBinary);
|
|
642
|
+
// Check if already cached
|
|
643
|
+
if (existsSync(nodePath)) {
|
|
644
|
+
return nodePath;
|
|
645
|
+
}
|
|
646
|
+
const filename = `node-v${version}-${platform}-${arch}.tar.gz`;
|
|
647
|
+
const url = `https://nodejs.org/dist/v${version}/${filename}`;
|
|
648
|
+
const archivePath = join(cacheDir, filename);
|
|
649
|
+
spinner?.start("Downloading Node.js runtime (first time only)...");
|
|
650
|
+
try {
|
|
651
|
+
// Ensure cache directory exists
|
|
652
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
653
|
+
// Download
|
|
654
|
+
await downloadFile(url, archivePath, spinner);
|
|
655
|
+
// Verify checksum
|
|
656
|
+
spinner?.start("Verifying download integrity...");
|
|
657
|
+
const expectedHash = await fetchExpectedChecksum(version, filename);
|
|
658
|
+
const actualHash = hashFile(archivePath);
|
|
659
|
+
if (actualHash !== expectedHash) {
|
|
660
|
+
// Clean up the corrupted download
|
|
661
|
+
rmSync(archivePath, { force: true });
|
|
662
|
+
throw new Error(`Node.js download verification failed.\n\n` +
|
|
663
|
+
` Expected: ${expectedHash}\n` +
|
|
664
|
+
` Actual: ${actualHash}\n\n` +
|
|
665
|
+
`This may indicate a corrupted download or network interference.\n` +
|
|
666
|
+
`Please retry or report this issue.`);
|
|
667
|
+
}
|
|
668
|
+
// Extract
|
|
669
|
+
spinner?.start("Extracting Node.js...");
|
|
670
|
+
extractTarGz(archivePath, cacheDir);
|
|
671
|
+
// Move node binary to cache root
|
|
672
|
+
// The tarball extracts to node-v{version}-{platform}-{arch}/bin/node
|
|
673
|
+
const extractedDir = join(cacheDir, `node-v${version}-${platform}-${arch}`);
|
|
674
|
+
const extractedBin = join(extractedDir, "bin", nodeBinary);
|
|
675
|
+
copyFileSync(extractedBin, nodePath);
|
|
676
|
+
chmodSync(nodePath, 0o755);
|
|
677
|
+
// Cleanup: remove extracted directory and archive
|
|
678
|
+
rmSync(extractedDir, { recursive: true, force: true });
|
|
679
|
+
rmSync(archivePath, { force: true });
|
|
680
|
+
spinner?.succeed(`Node.js v${version} cached to ${cacheDir}`);
|
|
681
|
+
return nodePath;
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
// Cleanup on error
|
|
685
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
// Provide helpful error messages
|
|
688
|
+
if (message.includes("ETIMEDOUT") || message.includes("ENOTFOUND")) {
|
|
689
|
+
throw new Error(`Failed to download Node.js runtime.\n\n` +
|
|
690
|
+
` URL: ${url}\n` +
|
|
691
|
+
` Error: ${message}\n\n` +
|
|
692
|
+
`Check your network connection and try again.\n` +
|
|
693
|
+
`If behind a proxy, set HTTPS_PROXY environment variable.`);
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Ensure the pkg CLI bundle and its auxiliary files are extracted from the
|
|
700
|
+
* compiled binary's assets.
|
|
701
|
+
*
|
|
702
|
+
* pkg requires several auxiliary files at runtime:
|
|
703
|
+
* - pkg-cli.cjs - The main bundled CLI
|
|
704
|
+
* - package.json - pkg's version info (read as ../package.json from cacheDir)
|
|
705
|
+
* - pkg-prelude/ - JavaScript files injected into compiled binaries
|
|
706
|
+
* - pkg-dictionary/ - Compression dictionaries for bytecode
|
|
707
|
+
* - pkg-common.cjs - Common utilities
|
|
708
|
+
*
|
|
709
|
+
* Returns the path to the extracted pkg-cli.cjs file.
|
|
710
|
+
*/
|
|
711
|
+
function ensurePkgCli() {
|
|
712
|
+
const version = getThinkwellVersion();
|
|
713
|
+
const pkgCliBaseDir = join(getCacheDir(), "pkg-cli");
|
|
714
|
+
const cacheDir = join(pkgCliBaseDir, version);
|
|
715
|
+
const pkgCliPath = join(cacheDir, "pkg-cli.cjs");
|
|
716
|
+
// Check if already cached (check for main file and a prelude file)
|
|
717
|
+
const preludeCheck = join(cacheDir, "pkg-prelude", "bootstrap.js");
|
|
718
|
+
if (existsSync(pkgCliPath) && existsSync(preludeCheck)) {
|
|
719
|
+
return pkgCliPath;
|
|
720
|
+
}
|
|
721
|
+
// Base path for pkg assets in the compiled binary's snapshot
|
|
722
|
+
const distPkgPath = resolve(__dirname, "../../dist-pkg");
|
|
723
|
+
// Extract main CLI bundle
|
|
724
|
+
const cliSrc = join(distPkgPath, "pkg-cli.cjs");
|
|
725
|
+
if (!existsSync(cliSrc)) {
|
|
726
|
+
throw new Error(`pkg CLI not found in compiled binary assets.\n` +
|
|
727
|
+
` Expected at: ${cliSrc}\n\n` +
|
|
728
|
+
`This may indicate a build issue. Please report this.`);
|
|
729
|
+
}
|
|
730
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
731
|
+
copyFileSync(cliSrc, pkgCliPath);
|
|
732
|
+
// Extract pkg's package.json (for version info)
|
|
733
|
+
// pkg reads ../package.json relative to __dirname (which is cacheDir)
|
|
734
|
+
// So we place it in the parent directory (pkgCliBaseDir)
|
|
735
|
+
const pkgJsonSrc = join(distPkgPath, "package.json");
|
|
736
|
+
if (existsSync(pkgJsonSrc)) {
|
|
737
|
+
copyFileSync(pkgJsonSrc, join(pkgCliBaseDir, "package.json"));
|
|
738
|
+
}
|
|
739
|
+
// Extract prelude files
|
|
740
|
+
const preludeDir = join(cacheDir, "pkg-prelude");
|
|
741
|
+
mkdirSync(preludeDir, { recursive: true });
|
|
742
|
+
for (const file of ["bootstrap.js", "diagnostic.js"]) {
|
|
743
|
+
const src = join(distPkgPath, "pkg-prelude", file);
|
|
744
|
+
if (existsSync(src)) {
|
|
745
|
+
copyFileSync(src, join(preludeDir, file));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// Extract common.js
|
|
749
|
+
const commonSrc = join(distPkgPath, "pkg-common.cjs");
|
|
750
|
+
if (existsSync(commonSrc)) {
|
|
751
|
+
copyFileSync(commonSrc, join(cacheDir, "pkg-common.cjs"));
|
|
752
|
+
}
|
|
753
|
+
// Extract dictionary files
|
|
754
|
+
// pkg reads ../dictionary relative to __dirname (which is cacheDir)
|
|
755
|
+
// So we place it in the parent directory (pkgCliBaseDir/dictionary/)
|
|
756
|
+
const dictionaryDir = join(pkgCliBaseDir, "dictionary");
|
|
757
|
+
mkdirSync(dictionaryDir, { recursive: true });
|
|
758
|
+
for (const file of ["v8-7.8.js", "v8-8.4.js", "v8-12.4.js"]) {
|
|
759
|
+
const src = join(distPkgPath, "pkg-dictionary", file);
|
|
760
|
+
if (existsSync(src)) {
|
|
761
|
+
copyFileSync(src, join(dictionaryDir, file));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return pkgCliPath;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Spawn a subprocess and wait for completion.
|
|
768
|
+
*/
|
|
769
|
+
function spawnAsync(command, args, options = {}) {
|
|
770
|
+
return new Promise((resolve) => {
|
|
771
|
+
const proc = spawn(command, args, {
|
|
772
|
+
cwd: options.cwd,
|
|
773
|
+
env: options.env || process.env,
|
|
774
|
+
stdio: options.verbose ? "inherit" : "pipe",
|
|
775
|
+
});
|
|
776
|
+
let stdout = "";
|
|
777
|
+
let stderr = "";
|
|
778
|
+
if (!options.verbose) {
|
|
779
|
+
proc.stdout?.on("data", (data) => {
|
|
780
|
+
stdout += data.toString();
|
|
781
|
+
});
|
|
782
|
+
proc.stderr?.on("data", (data) => {
|
|
783
|
+
stderr += data.toString();
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
proc.on("close", (code) => {
|
|
787
|
+
resolve({
|
|
788
|
+
exitCode: code ?? 1,
|
|
789
|
+
stdout,
|
|
790
|
+
stderr,
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
proc.on("error", (error) => {
|
|
794
|
+
resolve({
|
|
795
|
+
exitCode: 1,
|
|
796
|
+
stdout,
|
|
797
|
+
stderr: error.message,
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Compile using pkg via subprocess (for compiled binary environment).
|
|
804
|
+
*
|
|
805
|
+
* This function is called when running from a compiled thinkwell binary.
|
|
806
|
+
* It downloads a portable Node.js runtime and uses the bundled pkg CLI
|
|
807
|
+
* to perform the compilation as a subprocess.
|
|
808
|
+
*/
|
|
809
|
+
async function compileWithPkgSubprocess(ctx, wrapperPath, target, outputPath, spinner) {
|
|
810
|
+
// Ensure portable Node.js is available
|
|
811
|
+
const nodePath = await ensurePortableNode(spinner);
|
|
812
|
+
// Extract pkg CLI from snapshot
|
|
813
|
+
const pkgCliPath = ensurePkgCli();
|
|
814
|
+
const pkgTarget = TARGET_MAP[target];
|
|
815
|
+
// Ensure output directory exists
|
|
816
|
+
const outputDir = dirname(outputPath);
|
|
817
|
+
if (!existsSync(outputDir)) {
|
|
818
|
+
mkdirSync(outputDir, { recursive: true });
|
|
819
|
+
}
|
|
820
|
+
// Build pkg CLI arguments
|
|
821
|
+
const args = [
|
|
822
|
+
pkgCliPath,
|
|
823
|
+
wrapperPath,
|
|
824
|
+
"--targets",
|
|
825
|
+
pkgTarget,
|
|
826
|
+
"--output",
|
|
827
|
+
outputPath,
|
|
828
|
+
"--options",
|
|
829
|
+
"experimental-transform-types,disable-warning=ExperimentalWarning",
|
|
830
|
+
"--public",
|
|
831
|
+
];
|
|
832
|
+
// Add assets if specified
|
|
833
|
+
if (ctx.options.include && ctx.options.include.length > 0) {
|
|
834
|
+
for (const pattern of ctx.options.include) {
|
|
835
|
+
args.push("--assets", pattern);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
spinner?.start(`Compiling for ${target}...`);
|
|
839
|
+
const result = await spawnAsync(nodePath, args, {
|
|
840
|
+
cwd: ctx.buildDir,
|
|
841
|
+
env: {
|
|
842
|
+
...process.env,
|
|
843
|
+
// Set pkg cache path for pkg-fetch downloads
|
|
844
|
+
PKG_CACHE_PATH: join(getCacheDir(), "pkg-cache"),
|
|
845
|
+
},
|
|
846
|
+
verbose: ctx.options.verbose,
|
|
847
|
+
});
|
|
848
|
+
if (result.exitCode !== 0) {
|
|
849
|
+
const errorOutput = result.stderr || result.stdout;
|
|
850
|
+
throw new Error(`pkg compilation failed for ${target}.\n\n` +
|
|
851
|
+
`Exit code: ${result.exitCode}\n` +
|
|
852
|
+
(errorOutput ? `Output:\n${errorOutput}` : ""));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Stage 2: Compile with pkg.
|
|
857
|
+
*
|
|
858
|
+
* Uses @yao-pkg/pkg to create a self-contained binary.
|
|
859
|
+
*
|
|
860
|
+
* When running from a compiled thinkwell binary, this function uses a
|
|
861
|
+
* subprocess approach: downloading a portable Node.js runtime and executing
|
|
862
|
+
* the bundled pkg CLI as a child process. This works around pkg's dynamic
|
|
863
|
+
* import limitations in the virtual filesystem.
|
|
864
|
+
*
|
|
865
|
+
* When running from npm/source, this function uses @yao-pkg/pkg programmatically.
|
|
866
|
+
*/
|
|
867
|
+
async function compileWithPkg(ctx, wrapperPath, target, outputPath, spinner) {
|
|
868
|
+
// When running from a compiled binary, use subprocess approach
|
|
869
|
+
if (isRunningFromCompiledBinary()) {
|
|
870
|
+
await compileWithPkgSubprocess(ctx, wrapperPath, target, outputPath, spinner);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// Normal path: use pkg programmatically
|
|
874
|
+
const { exec } = await import("@yao-pkg/pkg");
|
|
875
|
+
const pkgTarget = TARGET_MAP[target];
|
|
876
|
+
// Ensure output directory exists
|
|
877
|
+
const outputDir = dirname(outputPath);
|
|
878
|
+
if (!existsSync(outputDir)) {
|
|
879
|
+
mkdirSync(outputDir, { recursive: true });
|
|
880
|
+
}
|
|
881
|
+
// Build pkg configuration
|
|
882
|
+
const pkgConfig = [
|
|
883
|
+
wrapperPath,
|
|
884
|
+
"--targets",
|
|
885
|
+
pkgTarget,
|
|
886
|
+
"--output",
|
|
887
|
+
outputPath,
|
|
888
|
+
"--options",
|
|
889
|
+
"experimental-transform-types,disable-warning=ExperimentalWarning",
|
|
890
|
+
"--public", // Include source instead of bytecode (required for cross-compilation)
|
|
891
|
+
];
|
|
892
|
+
// Add assets if specified
|
|
893
|
+
if (ctx.options.include && ctx.options.include.length > 0) {
|
|
894
|
+
for (const pattern of ctx.options.include) {
|
|
895
|
+
pkgConfig.push("--assets", pattern);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
await exec(pkgConfig);
|
|
899
|
+
}
|
|
900
|
+
// ============================================================================
|
|
901
|
+
// Top-Level Await Detection
|
|
902
|
+
// ============================================================================
|
|
903
|
+
/**
|
|
904
|
+
* Detect top-level await usage in the entry file.
|
|
905
|
+
* Returns an array of line numbers where top-level await is found.
|
|
906
|
+
*/
|
|
907
|
+
function detectTopLevelAwait(filePath) {
|
|
908
|
+
const content = readFileSync(filePath, "utf-8");
|
|
909
|
+
const lines = content.split("\n");
|
|
910
|
+
const awaits = [];
|
|
911
|
+
// Track nesting depth of functions/classes
|
|
912
|
+
let depth = 0;
|
|
913
|
+
let inMultiLineComment = false;
|
|
914
|
+
for (let i = 0; i < lines.length; i++) {
|
|
915
|
+
let line = lines[i];
|
|
916
|
+
// Handle multi-line comments
|
|
917
|
+
if (inMultiLineComment) {
|
|
918
|
+
const endIdx = line.indexOf("*/");
|
|
919
|
+
if (endIdx !== -1) {
|
|
920
|
+
line = line.slice(endIdx + 2);
|
|
921
|
+
inMultiLineComment = false;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// Remove single-line comments
|
|
928
|
+
const singleLineCommentIdx = line.indexOf("//");
|
|
929
|
+
if (singleLineCommentIdx !== -1) {
|
|
930
|
+
line = line.slice(0, singleLineCommentIdx);
|
|
931
|
+
}
|
|
932
|
+
// Handle multi-line comment start
|
|
933
|
+
const multiLineStart = line.indexOf("/*");
|
|
934
|
+
if (multiLineStart !== -1) {
|
|
935
|
+
const multiLineEnd = line.indexOf("*/", multiLineStart);
|
|
936
|
+
if (multiLineEnd !== -1) {
|
|
937
|
+
line = line.slice(0, multiLineStart) + line.slice(multiLineEnd + 2);
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
line = line.slice(0, multiLineStart);
|
|
941
|
+
inMultiLineComment = true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Count function/class/arrow function depth changes
|
|
945
|
+
// This is a simplified heuristic - not a full parser
|
|
946
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
947
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
948
|
+
// Check for function/class/arrow declarations that increase depth
|
|
949
|
+
if (/\b(function|class|async\s+function)\b/.test(line) && line.includes("{")) {
|
|
950
|
+
depth += 1;
|
|
951
|
+
}
|
|
952
|
+
else if (/=>\s*\{/.test(line)) {
|
|
953
|
+
depth += 1;
|
|
954
|
+
}
|
|
955
|
+
// Adjust depth for brace changes (simplified)
|
|
956
|
+
depth += openBraces - closeBraces;
|
|
957
|
+
if (depth < 0)
|
|
958
|
+
depth = 0;
|
|
959
|
+
// Check for await at top level (depth 0)
|
|
960
|
+
if (depth === 0 && /\bawait\b/.test(line)) {
|
|
961
|
+
// Make sure it's not inside a string
|
|
962
|
+
const withoutStrings = line.replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1/g, "");
|
|
963
|
+
if (/\bawait\b/.test(withoutStrings)) {
|
|
964
|
+
awaits.push(i + 1); // 1-indexed line numbers
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return awaits;
|
|
969
|
+
}
|
|
970
|
+
// ============================================================================
|
|
971
|
+
// Output Helpers
|
|
972
|
+
// ============================================================================
|
|
973
|
+
/** Log output respecting quiet mode */
|
|
974
|
+
function log(ctx, message) {
|
|
975
|
+
if (!ctx.options.quiet) {
|
|
976
|
+
console.log(message);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/** Create a spinner respecting quiet mode */
|
|
980
|
+
function createSpinner(ctx, text) {
|
|
981
|
+
return createSpinnerImpl({
|
|
982
|
+
text,
|
|
983
|
+
isSilent: ctx.options.quiet,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Run a dry-run build that shows what would be built without actually building.
|
|
988
|
+
*/
|
|
989
|
+
function runDryRun(ctx) {
|
|
990
|
+
console.log(styleText("bold", "Dry run mode - no files will be created\n"));
|
|
991
|
+
console.log(styleText("bold", "Entry point:"));
|
|
992
|
+
console.log(` ${ctx.entryPath}\n`);
|
|
993
|
+
console.log(styleText("bold", "Targets:"));
|
|
994
|
+
for (const target of ctx.resolvedTargets) {
|
|
995
|
+
const outputPath = getOutputPath(ctx, target);
|
|
996
|
+
console.log(` ${target} → ${outputPath}`);
|
|
997
|
+
}
|
|
998
|
+
console.log();
|
|
999
|
+
if (ctx.options.include && ctx.options.include.length > 0) {
|
|
1000
|
+
console.log(styleText("bold", "Assets to include:"));
|
|
1001
|
+
for (const pattern of ctx.options.include) {
|
|
1002
|
+
console.log(` ${pattern}`);
|
|
1003
|
+
}
|
|
1004
|
+
console.log();
|
|
1005
|
+
}
|
|
1006
|
+
if (ctx.options.external && ctx.options.external.length > 0) {
|
|
1007
|
+
console.log(styleText("bold", "External packages (not bundled):"));
|
|
1008
|
+
for (const pkg of ctx.options.external) {
|
|
1009
|
+
console.log(` ${pkg}`);
|
|
1010
|
+
}
|
|
1011
|
+
console.log();
|
|
1012
|
+
}
|
|
1013
|
+
if (ctx.options.minify) {
|
|
1014
|
+
console.log(styleText("bold", "Minification:"), "enabled");
|
|
1015
|
+
console.log();
|
|
1016
|
+
}
|
|
1017
|
+
console.log(styleText("bold", "Build steps:"));
|
|
1018
|
+
console.log(" 1. Bundle user script with esbuild");
|
|
1019
|
+
console.log(" 2. Copy thinkwell packages");
|
|
1020
|
+
console.log(" 3. Generate wrapper entry point");
|
|
1021
|
+
console.log(` 4. Compile with pkg for ${ctx.resolvedTargets.length} target(s)`);
|
|
1022
|
+
console.log();
|
|
1023
|
+
// Check for potential issues
|
|
1024
|
+
const topLevelAwaits = detectTopLevelAwait(ctx.entryPath);
|
|
1025
|
+
if (topLevelAwaits.length > 0) {
|
|
1026
|
+
console.log(styleText("yellow", "Warning: Top-level await detected"));
|
|
1027
|
+
console.log(" Top-level await is not supported in compiled binaries.");
|
|
1028
|
+
console.log(` Found at line(s): ${topLevelAwaits.join(", ")}`);
|
|
1029
|
+
console.log(" Wrap async code in an async main() function instead.\n");
|
|
1030
|
+
}
|
|
1031
|
+
console.log(styleText("dim", "Run without --dry-run to build."));
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Main build function.
|
|
1035
|
+
*/
|
|
1036
|
+
export async function runBundle(options) {
|
|
1037
|
+
// Handle watch mode separately
|
|
1038
|
+
if (options.watch) {
|
|
1039
|
+
await runWatchMode(options);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const ctx = initBundleContext(options);
|
|
1043
|
+
// Check for top-level await and warn
|
|
1044
|
+
const topLevelAwaits = detectTopLevelAwait(ctx.entryPath);
|
|
1045
|
+
if (topLevelAwaits.length > 0) {
|
|
1046
|
+
console.log(styleText("yellow", "Warning: Top-level await detected"));
|
|
1047
|
+
console.log(" Top-level await is not supported in compiled binaries.");
|
|
1048
|
+
console.log(` Found at line(s): ${topLevelAwaits.join(", ")}`);
|
|
1049
|
+
console.log(" Wrap async code in an async main() function instead.\n");
|
|
1050
|
+
}
|
|
1051
|
+
// Handle dry-run mode
|
|
1052
|
+
if (options.dryRun) {
|
|
1053
|
+
runDryRun(ctx);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
log(ctx, `Building ${styleText("bold", ctx.entryBasename)}...\n`);
|
|
1057
|
+
// Create build directory
|
|
1058
|
+
if (existsSync(ctx.buildDir)) {
|
|
1059
|
+
rmSync(ctx.buildDir, { recursive: true });
|
|
1060
|
+
}
|
|
1061
|
+
mkdirSync(ctx.buildDir, { recursive: true });
|
|
1062
|
+
try {
|
|
1063
|
+
// Stage 1: Bundle user script
|
|
1064
|
+
let spinner = createSpinner(ctx, "Bundling with esbuild...");
|
|
1065
|
+
spinner.start();
|
|
1066
|
+
const userBundlePath = await bundleUserScript(ctx);
|
|
1067
|
+
spinner.succeed("User script bundled");
|
|
1068
|
+
// Stage 2: Copy thinkwell bundles
|
|
1069
|
+
spinner = createSpinner(ctx, "Preparing thinkwell packages...");
|
|
1070
|
+
spinner.start();
|
|
1071
|
+
copyThinkwellBundles(ctx);
|
|
1072
|
+
spinner.succeed("Thinkwell packages ready");
|
|
1073
|
+
// Generate wrapper
|
|
1074
|
+
const wrapperPath = join(ctx.buildDir, "wrapper.cjs");
|
|
1075
|
+
const wrapperSource = generateWrapperSource(userBundlePath);
|
|
1076
|
+
writeFileSync(wrapperPath, wrapperSource);
|
|
1077
|
+
if (ctx.options.verbose) {
|
|
1078
|
+
log(ctx, " Generated wrapper entry point");
|
|
1079
|
+
}
|
|
1080
|
+
// Stage 3: Compile with pkg for each target
|
|
1081
|
+
const outputs = [];
|
|
1082
|
+
for (const target of ctx.resolvedTargets) {
|
|
1083
|
+
const outputPath = getOutputPath(ctx, target);
|
|
1084
|
+
spinner = createSpinner(ctx, `Compiling for ${target}...`);
|
|
1085
|
+
spinner.start();
|
|
1086
|
+
await compileWithPkg(ctx, wrapperPath, target, outputPath, spinner);
|
|
1087
|
+
outputs.push(outputPath);
|
|
1088
|
+
spinner.succeed(`Built ${basename(outputPath)}`);
|
|
1089
|
+
}
|
|
1090
|
+
log(ctx, "");
|
|
1091
|
+
log(ctx, styleText("green", "Build complete!"));
|
|
1092
|
+
log(ctx, "");
|
|
1093
|
+
log(ctx, styleText("bold", "Output:"));
|
|
1094
|
+
for (const output of outputs) {
|
|
1095
|
+
log(ctx, ` ${output}`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
// Clean up build directory
|
|
1100
|
+
if (!ctx.options.verbose) {
|
|
1101
|
+
try {
|
|
1102
|
+
rmSync(ctx.buildDir, { recursive: true });
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
// Ignore cleanup errors
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
log(ctx, `\nBuild artifacts preserved in: ${ctx.buildDir}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Run the build in watch mode, rebuilding on file changes.
|
|
1115
|
+
*/
|
|
1116
|
+
async function runWatchMode(options) {
|
|
1117
|
+
const ctx = initBundleContext(options);
|
|
1118
|
+
console.log(styleText("bold", `Watching ${ctx.entryBasename} for changes...`));
|
|
1119
|
+
console.log(styleText("dim", "Press Ctrl+C to stop.\n"));
|
|
1120
|
+
// Track if a build is currently in progress
|
|
1121
|
+
let buildInProgress = false;
|
|
1122
|
+
let rebuildQueued = false;
|
|
1123
|
+
// Debounce timer
|
|
1124
|
+
let debounceTimer = null;
|
|
1125
|
+
const DEBOUNCE_MS = 100;
|
|
1126
|
+
async function doBuild() {
|
|
1127
|
+
if (buildInProgress) {
|
|
1128
|
+
rebuildQueued = true;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
buildInProgress = true;
|
|
1132
|
+
rebuildQueued = false;
|
|
1133
|
+
const startTime = Date.now();
|
|
1134
|
+
console.log(styleText("dim", `[${new Date().toLocaleTimeString()}] Building...`));
|
|
1135
|
+
try {
|
|
1136
|
+
// Re-initialize context to pick up any config changes
|
|
1137
|
+
const freshCtx = initBundleContext(options);
|
|
1138
|
+
// Create build directory
|
|
1139
|
+
if (existsSync(freshCtx.buildDir)) {
|
|
1140
|
+
rmSync(freshCtx.buildDir, { recursive: true });
|
|
1141
|
+
}
|
|
1142
|
+
mkdirSync(freshCtx.buildDir, { recursive: true });
|
|
1143
|
+
// Bundle user script
|
|
1144
|
+
const userBundlePath = await bundleUserScript(freshCtx);
|
|
1145
|
+
// Copy thinkwell bundles
|
|
1146
|
+
copyThinkwellBundles(freshCtx);
|
|
1147
|
+
// Generate wrapper
|
|
1148
|
+
const wrapperPath = join(freshCtx.buildDir, "wrapper.cjs");
|
|
1149
|
+
const wrapperSource = generateWrapperSource(userBundlePath);
|
|
1150
|
+
writeFileSync(wrapperPath, wrapperSource);
|
|
1151
|
+
// Compile with pkg for each target
|
|
1152
|
+
const outputs = [];
|
|
1153
|
+
for (const target of freshCtx.resolvedTargets) {
|
|
1154
|
+
const outputPath = getOutputPath(freshCtx, target);
|
|
1155
|
+
await compileWithPkg(freshCtx, wrapperPath, target, outputPath);
|
|
1156
|
+
outputs.push(outputPath);
|
|
1157
|
+
}
|
|
1158
|
+
// Clean up build directory
|
|
1159
|
+
if (!freshCtx.options.verbose) {
|
|
1160
|
+
try {
|
|
1161
|
+
rmSync(freshCtx.buildDir, { recursive: true });
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
// Ignore cleanup errors
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const elapsed = Date.now() - startTime;
|
|
1168
|
+
console.log(styleText("green", `✓ Built in ${elapsed}ms`));
|
|
1169
|
+
for (const output of outputs) {
|
|
1170
|
+
console.log(styleText("dim", ` ${basename(output)}`));
|
|
1171
|
+
}
|
|
1172
|
+
console.log();
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1176
|
+
console.log(styleText("red", `✗ Build failed: ${message}`));
|
|
1177
|
+
console.log();
|
|
1178
|
+
}
|
|
1179
|
+
finally {
|
|
1180
|
+
buildInProgress = false;
|
|
1181
|
+
// If a rebuild was queued while building, start another build
|
|
1182
|
+
if (rebuildQueued) {
|
|
1183
|
+
doBuild();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function scheduleRebuild() {
|
|
1188
|
+
if (debounceTimer) {
|
|
1189
|
+
clearTimeout(debounceTimer);
|
|
1190
|
+
}
|
|
1191
|
+
debounceTimer = setTimeout(() => {
|
|
1192
|
+
debounceTimer = null;
|
|
1193
|
+
doBuild();
|
|
1194
|
+
}, DEBOUNCE_MS);
|
|
1195
|
+
}
|
|
1196
|
+
// Do initial build
|
|
1197
|
+
await doBuild();
|
|
1198
|
+
// Watch the entry file's directory for changes
|
|
1199
|
+
const watchDir = ctx.entryDir;
|
|
1200
|
+
const watcher = fsWatch(watchDir, { recursive: true }, (_eventType, filename) => {
|
|
1201
|
+
if (!filename)
|
|
1202
|
+
return;
|
|
1203
|
+
// Ignore common non-source files
|
|
1204
|
+
if (filename.includes("node_modules") ||
|
|
1205
|
+
filename.startsWith(".") ||
|
|
1206
|
+
filename.endsWith(".d.ts")) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
// Only watch TypeScript and JavaScript files
|
|
1210
|
+
if (!/\.(ts|tsx|js|jsx|mts|mjs|cts|cjs|json)$/.test(filename)) {
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (ctx.options.verbose) {
|
|
1214
|
+
console.log(styleText("dim", ` Changed: ${filename}`));
|
|
1215
|
+
}
|
|
1216
|
+
scheduleRebuild();
|
|
1217
|
+
});
|
|
1218
|
+
// Handle graceful shutdown
|
|
1219
|
+
const cleanup = () => {
|
|
1220
|
+
watcher.close();
|
|
1221
|
+
if (debounceTimer) {
|
|
1222
|
+
clearTimeout(debounceTimer);
|
|
1223
|
+
}
|
|
1224
|
+
console.log("\nStopped watching.");
|
|
1225
|
+
process.exit(0);
|
|
1226
|
+
};
|
|
1227
|
+
process.on("SIGINT", cleanup);
|
|
1228
|
+
process.on("SIGTERM", cleanup);
|
|
1229
|
+
// Keep process alive
|
|
1230
|
+
await new Promise(() => { });
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Show help for the build command.
|
|
1234
|
+
*/
|
|
1235
|
+
export function showBundleHelp() {
|
|
1236
|
+
console.log(`
|
|
1237
|
+
${cyanBold("thinkwell bundle")} - ${whiteBold("Compile TypeScript scripts into standalone executables")}
|
|
1238
|
+
|
|
1239
|
+
${greenBold("Usage:")}
|
|
1240
|
+
${cyanBold("thinkwell bundle")} ${cyan("[options] <entry>")}
|
|
1241
|
+
|
|
1242
|
+
${greenBold("Arguments:")}
|
|
1243
|
+
${cyan("entry")} TypeScript or JavaScript entry point
|
|
1244
|
+
|
|
1245
|
+
${greenBold("Options:")}
|
|
1246
|
+
${cyan("-o, --output")} ${dim("<path>")} Output file path ${dim("(default: ./<name>-<target>)")}
|
|
1247
|
+
${cyan("-t, --target")} ${dim("<target>")} Target platform ${dim("(can be specified multiple times)")}
|
|
1248
|
+
${cyan("--include")} ${dim("<glob>")} Additional files to embed as assets
|
|
1249
|
+
${cyan("-e, --external")} ${dim("<pkg>")} Exclude package from bundling ${dim("(can be repeated)")}
|
|
1250
|
+
${cyan("-m, --minify")} Minify the bundled code for smaller output
|
|
1251
|
+
${cyan("-w, --watch")} Watch for changes and rebuild automatically
|
|
1252
|
+
${cyan("-n, --dry-run")} Show what would be built without building
|
|
1253
|
+
${cyan("-q, --quiet")} Suppress all output except errors ${dim("(for CI)")}
|
|
1254
|
+
${cyan("-v, --verbose")} Show detailed build output
|
|
1255
|
+
${cyan("-h, --help")} Show this help message
|
|
1256
|
+
|
|
1257
|
+
${greenBold("Targets:")}
|
|
1258
|
+
${cyan("host")} Current platform ${dim("(default)")}
|
|
1259
|
+
${cyan("darwin-arm64")} macOS on Apple Silicon
|
|
1260
|
+
${cyan("darwin-x64")} macOS on Intel
|
|
1261
|
+
${cyan("linux-x64")} Linux on x64
|
|
1262
|
+
${cyan("linux-arm64")} Linux on ARM64
|
|
1263
|
+
|
|
1264
|
+
${greenBold("Examples:")}
|
|
1265
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts")} Bundle for current platform
|
|
1266
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts -o dist/my-agent")} Specify output path
|
|
1267
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts --target linux-x64")} Bundle for Linux
|
|
1268
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts -t darwin-arm64 -t linux-x64")} Multi-platform
|
|
1269
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts --dry-run")} Preview bundle without executing
|
|
1270
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts -e sqlite3")} Keep sqlite3 as external
|
|
1271
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts --minify")} Minify for smaller binary
|
|
1272
|
+
${cyanBold("thinkwell bundle")} ${cyan("src/agent.ts --watch")} Rebuild on file changes
|
|
1273
|
+
|
|
1274
|
+
The resulting binary is self-contained and includes:
|
|
1275
|
+
- Node.js 24 runtime with TypeScript support
|
|
1276
|
+
- All thinkwell packages
|
|
1277
|
+
- Your bundled application code
|
|
1278
|
+
|
|
1279
|
+
${greenBold("Configuration via package.json:")}
|
|
1280
|
+
Add a "thinkwell" key to your package.json to set defaults:
|
|
1281
|
+
|
|
1282
|
+
{
|
|
1283
|
+
"thinkwell": {
|
|
1284
|
+
"bundle": {
|
|
1285
|
+
"output": "dist/my-agent",
|
|
1286
|
+
"targets": ["darwin-arm64", "linux-x64"],
|
|
1287
|
+
"external": ["sqlite3"],
|
|
1288
|
+
"minify": true
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
CLI options override package.json settings.
|
|
1294
|
+
|
|
1295
|
+
${dim("Note: Binaries are ~70-90 MB due to the embedded Node.js runtime.")}
|
|
1296
|
+
${dim(" Use --minify to reduce bundle size (though Node.js runtime dominates).")}
|
|
1297
|
+
`.trim() + "\n");
|
|
1298
|
+
}
|
|
1299
|
+
//# sourceMappingURL=bundle.js.map
|