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