ttsc 0.10.2 → 0.11.0-dev.20260517

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 (106) hide show
  1. package/README.md +8 -0
  2. package/cmd/ttsc/api_compile.go +106 -106
  3. package/cmd/ttsc/api_transform.go +54 -54
  4. package/cmd/ttsc/main.go +103 -103
  5. package/cmd/ttscserver/helpers_test.go +27 -0
  6. package/cmd/ttscserver/main.go +154 -0
  7. package/cmd/ttscserver/run_lsp_reports_getwd_error_test.go +34 -0
  8. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +42 -0
  9. package/driver/lsp_envelope.go +136 -0
  10. package/driver/lsp_frame.go +121 -0
  11. package/driver/lsp_plugin.go +150 -0
  12. package/driver/lsp_proxy.go +394 -0
  13. package/driver/lsp_server.go +197 -0
  14. package/go.mod +4 -0
  15. package/go.sum +4 -0
  16. package/internal/cwd/cwd.go +8 -8
  17. package/lib/TtscCompiler.d.ts +15 -16
  18. package/lib/TtscCompiler.js +15 -16
  19. package/lib/TtscCompiler.js.map +1 -1
  20. package/lib/compiler/internal/compileProjectInMemory.d.ts +4 -1
  21. package/lib/compiler/internal/compileProjectInMemory.js +4 -1
  22. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  23. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  24. package/lib/compiler/internal/runBuild.js +43 -1
  25. package/lib/compiler/internal/runBuild.js.map +1 -1
  26. package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -12
  27. package/lib/compiler/internal/sharedHostHelpers.js +12 -12
  28. package/lib/defineTtscPlugin.d.ts +18 -0
  29. package/lib/defineTtscPlugin.js +7 -0
  30. package/lib/defineTtscPlugin.js.map +1 -0
  31. package/lib/index.d.ts +4 -3
  32. package/lib/index.js +4 -3
  33. package/lib/index.js.map +1 -1
  34. package/lib/launcher/internal/prepareExecution.js +1 -1
  35. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  36. package/lib/launcher/internal/resolveTtscserverBinary.d.ts +12 -0
  37. package/lib/launcher/internal/resolveTtscserverBinary.js +43 -0
  38. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -0
  39. package/lib/launcher/internal/runTtsc.js +47 -8
  40. package/lib/launcher/internal/runTtsc.js.map +1 -1
  41. package/lib/launcher/internal/runTtscserver.d.ts +20 -0
  42. package/lib/launcher/internal/runTtscserver.js +125 -0
  43. package/lib/launcher/internal/runTtscserver.js.map +1 -0
  44. package/lib/launcher/internal/runTtsx.js +1 -1
  45. package/lib/launcher/internal/runTtsx.js.map +1 -1
  46. package/lib/launcher/ttscserver.d.ts +2 -0
  47. package/lib/launcher/ttscserver.js +9 -0
  48. package/lib/launcher/ttscserver.js.map +1 -0
  49. package/lib/plugin/internal/buildSourcePlugin.d.ts +9 -0
  50. package/lib/plugin/internal/buildSourcePlugin.js +197 -2
  51. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  52. package/lib/plugin/internal/loadProjectPlugins.js +86 -8
  53. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  54. package/lib/structures/ITtscCompilerContext.d.ts +1 -1
  55. package/lib/structures/ITtscCompilerDiagnostic.d.ts +3 -3
  56. package/lib/structures/ITtscCompilerResult.d.ts +10 -22
  57. package/lib/structures/ITtscCompilerTransformation.d.ts +2 -2
  58. package/lib/structures/ITtscPlugin.d.ts +37 -4
  59. package/lib/structures/ITtscPlugin.js +0 -4
  60. package/lib/structures/ITtscPlugin.js.map +1 -1
  61. package/lib/structures/ITtscPluginContributor.d.ts +26 -0
  62. package/lib/structures/ITtscPluginContributor.js +3 -0
  63. package/lib/structures/ITtscPluginContributor.js.map +1 -0
  64. package/lib/structures/ITtscPluginFactoryContext.d.ts +5 -5
  65. package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
  66. package/lib/structures/TtscPluginStage.d.ts +4 -3
  67. package/lib/structures/index.d.ts +1 -0
  68. package/lib/structures/index.js +1 -0
  69. package/lib/structures/index.js.map +1 -1
  70. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +3 -0
  71. package/lib/structures/internal/TtscBuildOptions.d.ts +13 -0
  72. package/lib/structures/internal/TtscCommonOptions.d.ts +5 -2
  73. package/package.json +10 -9
  74. package/shim/lsp/extra-shim.json +4 -0
  75. package/shim/lsp/go.mod +16 -0
  76. package/shim/lsp/go.sum +26 -0
  77. package/shim/lsp/shim.go +42 -0
  78. package/shim/printer/shim.go +5 -5
  79. package/src/TtscCompiler.ts +15 -16
  80. package/src/compiler/internal/compileProjectInMemory.ts +4 -1
  81. package/src/compiler/internal/project/readProjectConfig.ts +0 -1
  82. package/src/compiler/internal/runBuild.ts +43 -1
  83. package/src/compiler/internal/sharedHostHelpers.ts +12 -12
  84. package/src/defineTtscPlugin.ts +26 -0
  85. package/src/index.ts +4 -3
  86. package/src/launcher/internal/prepareExecution.ts +1 -1
  87. package/src/launcher/internal/resolveTtscserverBinary.ts +51 -0
  88. package/src/launcher/internal/runTtsc.ts +53 -11
  89. package/src/launcher/internal/runTtscserver.ts +92 -0
  90. package/src/launcher/internal/runTtsx.ts +1 -1
  91. package/src/launcher/ttscserver.ts +8 -0
  92. package/src/plugin/internal/buildSourcePlugin.ts +231 -12
  93. package/src/plugin/internal/loadProjectPlugins.ts +108 -8
  94. package/src/structures/ITtscCompilerContext.ts +1 -1
  95. package/src/structures/ITtscCompilerDiagnostic.ts +3 -3
  96. package/src/structures/ITtscCompilerResult.ts +10 -22
  97. package/src/structures/ITtscCompilerTransformation.ts +2 -2
  98. package/src/structures/ITtscPlugin.ts +37 -11
  99. package/src/structures/ITtscPluginContributor.ts +27 -0
  100. package/src/structures/ITtscPluginFactoryContext.ts +5 -5
  101. package/src/structures/ITtscProjectPluginConfig.ts +2 -2
  102. package/src/structures/TtscPluginStage.ts +4 -3
  103. package/src/structures/index.ts +1 -0
  104. package/src/structures/internal/ITtscLoadedNativePlugin.ts +3 -0
  105. package/src/structures/internal/TtscBuildOptions.ts +13 -0
  106. package/src/structures/internal/TtscCommonOptions.ts +5 -2
@@ -106,6 +106,21 @@ export function runBuild(
106
106
  }
107
107
 
108
108
  if (options.emit === false) {
109
+ if (options.format === true) {
110
+ // Format mode is write-only by contract: the lint sidecar
111
+ // already rewrote source files and reported nothing. Running
112
+ // tsgo --noEmit OR a transform compiler afterwards would either
113
+ // surface unrelated type errors as if they were format failures
114
+ // (tsgo path) or apply transform-stage rewrites on top of the
115
+ // formatted source (transform path), both of which break the
116
+ // documented "ttsc format only formats" guarantee. The
117
+ // short-circuit fires before either branch so format mode is a
118
+ // single concern regardless of how many compilers the project
119
+ // configures. Callers that want a recheck or a transform pass
120
+ // after format should run `ttsc check` / `ttsc build` as a
121
+ // separate invocation.
122
+ return checked;
123
+ }
109
124
  if (compilers.length !== 0) {
110
125
  assertSharedHostCompatibility(compilers, "emit");
111
126
  return appendBuildOutput(
@@ -143,6 +158,21 @@ export function runBuild(
143
158
  return result;
144
159
  }
145
160
 
161
+ if (options.format === true) {
162
+ // Format mode is write-only by contract — see the matching
163
+ // short-circuit in the with-native-plugins branch above. When no
164
+ // native plugin is loaded there is nothing for `ttsc format` to
165
+ // rewrite, so emit an empty success result rather than falling
166
+ // through to a tsgo pass that would surface unrelated type errors
167
+ // as if they were format failures.
168
+ return {
169
+ diagnostics: [],
170
+ status: 0,
171
+ stdout: "",
172
+ stderr: "",
173
+ };
174
+ }
175
+
146
176
  if (options.emit !== false && options.skipDiagnosticsCheck !== true) {
147
177
  const checked = runTsgo(execution, ["--noEmit"], options);
148
178
  if (checked.status !== 0) {
@@ -298,7 +328,7 @@ function createNativeCheckArgs(
298
328
  options: TtscBuildOptions,
299
329
  ): string[] {
300
330
  const args = [
301
- "check",
331
+ nativeCheckSubcommand(options),
302
332
  "--tsconfig=" + execution.tsconfig,
303
333
  "--plugins-json=" + serializeNativePlugins(execution.nativePlugins),
304
334
  "--cwd=" + execution.projectRoot,
@@ -314,6 +344,18 @@ function createNativeCheckArgs(
314
344
  return args;
315
345
  }
316
346
 
347
+ /**
348
+ * Decide which native plugin subcommand the lint sidecar should run for the
349
+ * current `runBuild` invocation. The launcher selects exactly one of `fix` /
350
+ * `format` / `check` via subcommand dispatch, so at most one of the
351
+ * `options.fix` / `options.format` booleans is true.
352
+ */
353
+ function nativeCheckSubcommand(options: TtscBuildOptions): string {
354
+ if (options.format === true) return "format";
355
+ if (options.fix === true) return "fix";
356
+ return "check";
357
+ }
358
+
317
359
  function serializeNativePlugins(
318
360
  plugins: readonly ITtscLoadedNativePlugin[],
319
361
  ): string {
@@ -10,12 +10,12 @@ import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoa
10
10
  * plugin must declare itself with one of these names AND its source path must
11
11
  * resolve to a `package.json` whose `name` field matches.
12
12
  *
13
- * This whitelist exists because `@ttsc/banner`, `@ttsc/paths`, and `@ttsc/strip`
14
- * are AST-only mutators that operate on the same `*shimast.SourceFile` graph
15
- * and do not own emit. They are composed through `packages/ttsc/utility/host.go`,
16
- * so any one of their binaries can host the shared run. Third-party plugins
17
- * cannot participate in this fast path; they must opt into composition through
18
- * the explicit `ITtscPlugin.composes` field.
13
+ * This whitelist exists because `@ttsc/banner`, `@ttsc/paths`, and
14
+ * `@ttsc/strip` are AST-only mutators that operate on the same
15
+ * `*shimast.SourceFile` graph and do not own emit. They are composed through
16
+ * `packages/ttsc/utility/host.go`, so any one of their binaries can host the
17
+ * shared run. Third-party plugins cannot participate in this fast path; they
18
+ * must opt into composition through the explicit `ITtscPlugin.composes` field.
19
19
  */
20
20
  export const FIRST_PARTY_UTILITY_PLUGIN_NAMES: ReadonlySet<string> = new Set([
21
21
  "@ttsc/banner",
@@ -42,8 +42,8 @@ export function isFirstPartyUtilityTransformPlugin(
42
42
  /**
43
43
  * Walks upward from `source` (a directory or file path) up to four parent
44
44
  * levels searching for the nearest `package.json`. Returns the parsed manifest
45
- * object on success, or `undefined` when no manifest is found or parsing
46
- * fails. The depth bound mirrors the pre-existing inline implementations in
45
+ * object on success, or `undefined` when no manifest is found or parsing fails.
46
+ * The depth bound mirrors the pre-existing inline implementations in
47
47
  * `runBuild.ts` and `transformProjectInMemory.ts`.
48
48
  */
49
49
  export function readNearestPackageManifest(
@@ -77,10 +77,10 @@ export function readNearestPackageManifest(
77
77
  *
78
78
  * Two callers exist with subtly different error wording: the build path
79
79
  * (`runBuild.ts`) reports "multiple compiler native backends cannot share one
80
- * emit pass" while the source-to-source path
81
- * (`transformProjectInMemory.ts`) reports "cannot share one source-to-source
82
- * pass". The `pass` argument selects the appropriate phrase so the error
83
- * message remains diagnostic-grade instead of generic.
80
+ * emit pass" while the source-to-source path (`transformProjectInMemory.ts`)
81
+ * reports "cannot share one source-to-source pass". The `pass` argument selects
82
+ * the appropriate phrase so the error message remains diagnostic-grade instead
83
+ * of generic.
84
84
  */
85
85
  export function assertSharedHostCompatibility(
86
86
  plugins: readonly ITtscLoadedNativePlugin[],
@@ -0,0 +1,26 @@
1
+ import type { ITtscPlugin } from "./structures/ITtscPlugin";
2
+
3
+ /**
4
+ * Identity helper for authoring `ITtscPlugin` descriptors with full literal
5
+ * type preservation.
6
+ *
7
+ * Two call shapes:
8
+ *
9
+ * - Object form: returns the descriptor unchanged but pins its narrowed type so
10
+ * editor tooling and `satisfies` clauses can see exact field values.
11
+ * - Factory form: wraps a `(context) => descriptor` factory so the returned
12
+ * function carries the literal descriptor type through to consumers without
13
+ * forcing a manual `as const`.
14
+ *
15
+ * Pure pass-through at runtime — the function exists to teach TypeScript the
16
+ * const-narrowing the user would otherwise have to spell out.
17
+ */
18
+ export function defineTtscPlugin<const T extends ITtscPlugin>(plugin: T): T;
19
+ export function defineTtscPlugin<TContext, const T extends ITtscPlugin>(
20
+ factory: (context: TContext) => T,
21
+ ): (context: TContext) => T;
22
+ export function defineTtscPlugin(
23
+ value: ITtscPlugin | ((context: unknown) => ITtscPlugin),
24
+ ): ITtscPlugin | ((context: unknown) => ITtscPlugin) {
25
+ return value;
26
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * ttsc — public TypeScript entry.
2
+ * Ttsc — public TypeScript entry.
3
3
  *
4
4
  * The package root intentionally exposes only the programmatic compiler class
5
5
  * and the plugin-author contracts. CLI launcher functions, binary resolution,
6
- * project parsing helpers, and native build helpers stay internal so the
7
- * public package surface remains small and stable.
6
+ * project parsing helpers, and native build helpers stay internal so the public
7
+ * package surface remains small and stable.
8
8
  */
9
9
 
10
10
  export * from "./TtscCompiler";
11
+ export * from "./defineTtscPlugin";
11
12
  export * from "./structures/index";
@@ -2,9 +2,9 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
+ import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
5
6
  import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmittedJavaScript";
6
7
  import { runBuild } from "../../compiler/internal/runBuild";
7
- import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
8
8
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
9
9
 
10
10
  const PROCESS_CACHE_KEY = String(process.pid);
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Resolve the platform-specific ttscserver binary path. Looks first at the
6
+ * TTSCSERVER_BINARY environment override (must be absolute), then at the
7
+ * shipped per-platform npm package (`@ttsc/<platform>-<arch>/bin/ttscserver`),
8
+ * then at the local-build fallback under `packages/native/`.
9
+ *
10
+ * Mirrors `resolveBinary` for the ttsc helper so editors that install ttsc via
11
+ * pnpm see the LSP host alongside the existing helper.
12
+ */
13
+ export function resolveTtscserverBinary(
14
+ opts: { env?: NodeJS.ProcessEnv } = {},
15
+ ): string | null {
16
+ const env = opts.env ?? process.env;
17
+ if (env.TTSCSERVER_BINARY && path.isAbsolute(env.TTSCSERVER_BINARY)) {
18
+ return env.TTSCSERVER_BINARY;
19
+ }
20
+
21
+ try {
22
+ return require.resolve(
23
+ `@ttsc/${process.platform}-${process.arch}/bin/${
24
+ process.platform === "win32" ? "ttscserver.exe" : "ttscserver"
25
+ }`,
26
+ );
27
+ } catch {
28
+ /* fall through to local lookup */
29
+ }
30
+
31
+ const local = defaultLocalBinaryPath();
32
+ if (local) return local;
33
+
34
+ return null;
35
+ }
36
+
37
+ function defaultLocalBinaryPath(): string | null {
38
+ const root = packageRootDir();
39
+ const candidate = path.resolve(
40
+ root,
41
+ "..",
42
+ "native",
43
+ process.platform === "win32" ? "ttscserver.exe" : "ttscserver",
44
+ );
45
+ return fs.existsSync(candidate) ? candidate : null;
46
+ }
47
+
48
+ function packageRootDir(): string {
49
+ const moduleDir = path.resolve(__dirname, "..", "..");
50
+ return fs.realpathSync.native?.(moduleDir) ?? fs.realpathSync(moduleDir);
51
+ }
@@ -3,10 +3,10 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { TtscCompiler } from "../../TtscCompiler";
6
+ import { resolveProjectConfig } from "../../compiler/internal/project/resolveProjectConfig";
6
7
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
7
8
  import { runBuild } from "../../compiler/internal/runBuild";
8
9
  import { runSingleFileEmit } from "../../compiler/internal/runSingleFileEmit";
9
- import { resolveProjectConfig } from "../../compiler/internal/project/resolveProjectConfig";
10
10
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
11
11
  import { getCompilerVersionText } from "./getCompilerVersionText";
12
12
 
@@ -15,7 +15,7 @@ export function runTtsc(
15
15
  ): number {
16
16
  try {
17
17
  if (argv.length === 0) {
18
- return runCompatibleBuild([], false);
18
+ return runCompatibleBuild([], "build");
19
19
  }
20
20
 
21
21
  const [command, ...rest] = argv as [string, ...string[]];
@@ -31,9 +31,13 @@ export function runTtsc(
31
31
  process.stdout.write(`${getCompilerVersionText()}\n`);
32
32
  return 0;
33
33
  case "build":
34
- return runCompatibleBuild(rest, false);
34
+ return runCompatibleBuild(rest, "build");
35
35
  case "check":
36
- return runCompatibleBuild(rest, true);
36
+ return runCompatibleBuild(rest, "check");
37
+ case "fix":
38
+ return runCompatibleBuild(rest, "fix");
39
+ case "format":
40
+ return runCompatibleBuild(rest, "format");
37
41
  case "clean":
38
42
  return runClean(rest);
39
43
  case "prepare":
@@ -42,10 +46,10 @@ export function runTtsc(
42
46
  return delegateToNative(argv);
43
47
  case "-p":
44
48
  case "--project":
45
- return runCompatibleBuild(argv, false);
49
+ return runCompatibleBuild(argv, "build");
46
50
  default:
47
51
  if (isBuildAlias(command)) {
48
- return runCompatibleBuild(argv, false);
52
+ return runCompatibleBuild(argv, "build");
49
53
  }
50
54
  process.stderr.write(
51
55
  `ttsc: unknown command ${JSON.stringify(command)}\n`,
@@ -68,15 +72,45 @@ function isBuildAlias(command: string): boolean {
68
72
  );
69
73
  }
70
74
 
71
- function runCompatibleBuild(
72
- argv: readonly string[],
73
- checkOnly: boolean,
74
- ): number {
75
+ type TtscMode = "build" | "check" | "fix" | "format";
76
+
77
+ function runCompatibleBuild(argv: readonly string[], mode: TtscMode): number {
78
+ const checkOnly = mode !== "build";
75
79
  const options = normalizeBuildOptions(parseBuildArgs(argv, checkOnly));
80
+ if (mode === "fix") {
81
+ if (options.emit === true) {
82
+ throw new Error("ttsc: fix and --emit are mutually exclusive");
83
+ }
84
+ options.fix = true;
85
+ options.emit = false;
86
+ }
87
+ if (mode === "format") {
88
+ if (options.emit === true) {
89
+ throw new Error("ttsc: format and --emit are mutually exclusive");
90
+ }
91
+ options.format = true;
92
+ options.emit = false;
93
+ }
76
94
  if (options.watch) {
95
+ if (mode === "fix") {
96
+ throw new Error(
97
+ "ttsc: fix does not support watch mode; use ttsc --noEmit --watch for incremental checks",
98
+ );
99
+ }
100
+ if (mode === "format") {
101
+ throw new Error(
102
+ "ttsc: format does not support watch mode; use ttsc --noEmit --watch for incremental checks",
103
+ );
104
+ }
77
105
  return runWatch(options, checkOnly);
78
106
  }
79
107
  if (options.files.length !== 0) {
108
+ if (mode === "fix") {
109
+ throw new Error("ttsc: fix requires a project, not single-file mode");
110
+ }
111
+ if (mode === "format") {
112
+ throw new Error("ttsc: format requires a project, not single-file mode");
113
+ }
80
114
  return runSingleFile(options);
81
115
  }
82
116
  const result = runBuild(checkOnly ? { ...options, emit: false } : options);
@@ -268,6 +302,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
268
302
  let cwd: string | undefined;
269
303
  let emit: boolean | undefined = checkOnly ? false : undefined;
270
304
  const files: string[] = [];
305
+ let fix = false;
306
+ let format = false;
271
307
  let outDir: string | undefined;
272
308
  let preserveWatchOutput = false;
273
309
  let quiet = true;
@@ -348,6 +384,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
348
384
  cwd,
349
385
  emit,
350
386
  files,
387
+ fix,
388
+ format,
351
389
  outDir,
352
390
  preserveWatchOutput,
353
391
  quiet,
@@ -366,6 +404,8 @@ function printHelp(): void {
366
404
  " ttsc -p tsconfig.json",
367
405
  " ttsc --watch",
368
406
  " ttsc --noEmit",
407
+ " ttsc fix",
408
+ " ttsc format",
369
409
  " ttsc prepare [options]",
370
410
  " ttsc clean [options]",
371
411
  " ttsc version",
@@ -390,9 +430,11 @@ function printHelp(): void {
390
430
  " Plugin modules are descriptors for ordered native transformer backends.",
391
431
  " JS transformOutput/transformSource functions are not part of the public contract.",
392
432
  "",
393
- "Compatibility aliases:",
433
+ "Subcommands:",
394
434
  " ttsc build [options] Same project build lane as `ttsc [options]`.",
395
435
  " ttsc check [options] Same as `ttsc --noEmit [options]`.",
436
+ " ttsc fix [options] Apply check-plugin lint + format edits, then run `ttsc check`.",
437
+ " ttsc format [options] Apply check-plugin format-class edits only (write-only, no type check).",
396
438
  " ttsc prepare [options] Build configured source-plugin binaries into cache.",
397
439
  " ttsc clean [options] Delete local source-plugin cache directories.",
398
440
  ].join("\n"),
@@ -0,0 +1,92 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import * as os from "node:os";
4
+
5
+ import { resolveTtscserverBinary } from "./resolveTtscserverBinary";
6
+
7
+ /**
8
+ * Drive the ttscserver native binary from a node launcher. The launcher is
9
+ * deliberately thin: argument parsing, version banners, and help text are owned
10
+ * by the Go binary so future flags only need to change one layer. The JS side
11
+ * performs only:
12
+ *
13
+ * - Resolve the platform binary,
14
+ * - Inject `--stdio` when the first arg is not a meta-command,
15
+ * - Delegate to the binary with inherited stdio so OS-level signals reach the
16
+ * child via the parent's process group.
17
+ */
18
+ export function runTtscserver(
19
+ argv: readonly string[] = process.argv.slice(2),
20
+ ): number {
21
+ const binary = resolveTtscserverBinary();
22
+ if (!binary) {
23
+ process.stderr.write(
24
+ [
25
+ `ttscserver: platform-specific binary not found (@ttsc/${process.platform}-${process.arch}).`,
26
+ `Set TTSCSERVER_BINARY to an absolute path or reinstall ttsc with optional dependencies enabled.`,
27
+ ].join("\n") + "\n",
28
+ );
29
+ return 1;
30
+ }
31
+ ensureExecutable(binary);
32
+
33
+ const args = needsStdio(argv) ? ["--stdio", ...argv] : [...argv];
34
+ const result = spawnSync(binary, args, {
35
+ stdio: "inherit",
36
+ env: process.env,
37
+ windowsHide: true,
38
+ });
39
+ if (result.error) {
40
+ process.stderr.write(`ttscserver: ${result.error.message}\n`);
41
+ return 1;
42
+ }
43
+ if (result.signal) {
44
+ // POSIX convention: 128 + signum so wrappers (bash, npm-script, CI)
45
+ // can decode the signal that killed the child (130 = SIGINT, 143 =
46
+ // SIGTERM, etc.). On Windows, `spawnSync` does not surface a signal
47
+ // (TerminateProcess carries no signum) so this branch is POSIX-only
48
+ // by design; Windows-killed children take the `result.status ?? 1`
49
+ // path below.
50
+ const signum = (os.constants.signals as Record<string, number | undefined>)[
51
+ result.signal
52
+ ];
53
+ return typeof signum === "number" ? 128 + signum : 1;
54
+ }
55
+ return result.status ?? 1;
56
+ }
57
+
58
+ /**
59
+ * `--stdio` is the only transport the native host accepts today. The launcher
60
+ * injects it only when the first argv token looks like a forwarded option;
61
+ * meta-commands (`-v`, `--help`, `version`, etc.) pass through untouched so the
62
+ * Go binary owns the canonical banner. This mirrors
63
+ * `cmd/ttscserver/main.go::run`, which dispatches on `args[0]` only.
64
+ */
65
+ export function needsStdio(argv: readonly string[]): boolean {
66
+ if (argv.length === 0) return false;
67
+ if (argv.includes("--stdio")) return false;
68
+ const head = argv[0];
69
+ if (
70
+ head === "-v" ||
71
+ head === "--version" ||
72
+ head === "version" ||
73
+ head === "-h" ||
74
+ head === "--help" ||
75
+ head === "help"
76
+ ) {
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ /** Mirror the ttsc helper-binary chmod hint so first-run from npm works. */
83
+ function ensureExecutable(binary: string): void {
84
+ if (process.platform === "win32") return;
85
+ try {
86
+ const mode = fs.statSync(binary).mode & 0o777;
87
+ if ((mode & 0o111) !== 0) return;
88
+ fs.chmodSync(binary, mode | 0o755);
89
+ } catch {
90
+ /* spawn will surface the underlying error */
91
+ }
92
+ }
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { getCompilerVersionText } from "./getCompilerVersionText";
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runTtscserver } from "./internal/runTtscserver";
4
+
5
+ const code = runTtscserver(process.argv.slice(2));
6
+ if (typeof code === "number") {
7
+ process.exitCode = code;
8
+ }