@tagma/sdk 0.4.18 → 0.5.0

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/src/runner.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, statSync } from 'node:fs';
2
- import { isAbsolute, join } from 'node:path';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, resolve as pathResolve } from 'node:path';
3
3
  import type { SpawnSpec, DriverPlugin, TaskResult } from './types';
4
4
  import { shellArgs } from './utils';
5
5
 
@@ -45,14 +45,24 @@ export interface RunOptions {
45
45
  * manually resolve the command against PATH + PATHEXT here so Drivers can
46
46
  * keep using short names (`claude`, `npx`, etc.) cross-platform.
47
47
  *
48
+ * We also auto-unwrap npm-generated .cmd shims into direct `node <js>`
49
+ * invocations. Spawning the .cmd routes argv through cmd.exe, which silently
50
+ * truncates any argv element at the first newline — a multi-line prompt
51
+ * reaches the child as just its first line. By targeting the underlying JS
52
+ * entry point directly we bypass cmd.exe entirely and newlines survive.
53
+ *
48
54
  * Results are cached by (cmd, envPath) key so repeated spawns of the same
49
- * command don't block the event loop with synchronous PATH scans.
55
+ * command don't block the event loop with synchronous PATH/shim scans.
50
56
  *
51
57
  * Returns the original name if resolution fails; Bun will raise the same
52
58
  * ENOENT it would have otherwise.
53
59
  */
54
60
  const RESOLVED_EXE_CACHE_MAX = 128;
55
- const resolvedExeCache = new Map<string, string | null>();
61
+ // A cache entry is the replacement argv head for the command:
62
+ // - [path] — a single resolved executable (e.g. `foo.exe`)
63
+ // - [node, jsEntry] — an npm-shim unwrapped into `node <js>`
64
+ // - null — resolution failed, leave the original name
65
+ const resolvedExeCache = new Map<string, readonly string[] | null>();
56
66
 
57
67
  /** Evict the oldest entry when the cache is at capacity. */
58
68
  function evictIfFull(): void {
@@ -63,18 +73,72 @@ function evictIfFull(): void {
63
73
  }
64
74
  }
65
75
 
76
+ /**
77
+ * Parse an npm-generated .cmd shim and return the underlying JS entry path.
78
+ *
79
+ * npm's shim has the shape:
80
+ * "%_prog%" "%dp0%\node_modules\<pkg>\bin\<script>" %*
81
+ *
82
+ * We extract the second double-quoted path, substitute `%dp0%` with the
83
+ * wrapper's own directory, and return the absolute JS path. Returns null for
84
+ * anything that doesn't match the npm-shim pattern (user-written .cmd
85
+ * scripts, non-node tools, etc.), which keeps the caller on the .cmd path.
86
+ */
87
+ function parseNpmCmdShim(wrapperPath: string): string | null {
88
+ let contents: string;
89
+ try {
90
+ contents = readFileSync(wrapperPath, 'utf8');
91
+ } catch {
92
+ return null;
93
+ }
94
+ const execLine = contents
95
+ .split(/\r?\n/)
96
+ .find((l) => l.includes('%*') && l.includes('%dp0%'));
97
+ if (!execLine) return null;
98
+ const quoted = execLine.match(/"([^"]+)"/g);
99
+ if (!quoted || quoted.length < 2) return null;
100
+ const rawTarget = quoted[1]!.slice(1, -1); // strip surrounding quotes
101
+ const wrapperDir = dirname(wrapperPath);
102
+ // %dp0% expands to wrapper dir with a trailing backslash; strip either form.
103
+ const expanded = rawTarget.replace(/%dp0%\\?/i, '').replace(/\//g, '\\');
104
+ const abs = isAbsolute(expanded) ? expanded : pathResolve(wrapperDir, expanded);
105
+ return existsSync(abs) ? abs : null;
106
+ }
107
+
108
+ /**
109
+ * Given a resolved .cmd/.bat path, return the argv prefix that should be
110
+ * spawned instead. For npm shims this is `[node, js-entry]`; for everything
111
+ * else it's `[wrapperPath]` (unchanged, caller keeps using the wrapper).
112
+ */
113
+ function unwrapCmdShim(wrapperPath: string): readonly string[] {
114
+ if (!/\.(cmd|bat)$/i.test(wrapperPath)) return [wrapperPath];
115
+ const jsEntry = parseNpmCmdShim(wrapperPath);
116
+ if (!jsEntry) return [wrapperPath];
117
+ // Prefer node colocated with the wrapper (npm global bin often ships one).
118
+ const colocated = join(dirname(wrapperPath), 'node.exe');
119
+ const nodeExe = existsSync(colocated) ? colocated : 'node';
120
+ return [nodeExe, jsEntry];
121
+ }
122
+
66
123
  function resolveWindowsExe(args: readonly string[], envPath: string): readonly string[] {
67
124
  if (process.platform !== 'win32' || args.length === 0) return args;
68
125
  const cmd = args[0]!;
69
- // Already a full path or has an extension → trust caller.
70
- if (isAbsolute(cmd) || /\.[a-z0-9]+$/i.test(cmd)) return args;
126
+ // Already a full path or has an extension → trust caller. We still attempt
127
+ // shim unwrapping when the caller handed us a bare .cmd/.bat so drivers
128
+ // that resolve the shim themselves still benefit from the cmd.exe bypass.
129
+ if (isAbsolute(cmd) || /\.[a-z0-9]+$/i.test(cmd)) {
130
+ if (/\.(cmd|bat)$/i.test(cmd) && existsSync(cmd)) {
131
+ const unwrapped = unwrapCmdShim(cmd);
132
+ if (unwrapped.length === 2) return [...unwrapped, ...args.slice(1)];
133
+ }
134
+ return args;
135
+ }
71
136
 
72
137
  const cacheKey = `${cmd}\x00${envPath}`;
73
138
  if (resolvedExeCache.has(cacheKey)) {
74
- // ?? null coerces undefined→null so cached is string|null and the !== null
75
- // check narrows it to string without a spurious 'undefined' arm.
139
+ // ?? null coerces undefined→null so the subsequent guard narrows cleanly.
76
140
  const cached = resolvedExeCache.get(cacheKey) ?? null;
77
- return cached !== null ? [cached, ...args.slice(1)] : args;
141
+ return cached !== null ? [...cached, ...args.slice(1)] : args;
78
142
  }
79
143
 
80
144
  const exts = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC')
@@ -87,9 +151,10 @@ function resolveWindowsExe(args: readonly string[], envPath: string): readonly s
87
151
  const candidate = join(dir, cmd + ext);
88
152
  try {
89
153
  if (existsSync(candidate) && statSync(candidate).isFile()) {
154
+ const head = unwrapCmdShim(candidate);
90
155
  evictIfFull();
91
- resolvedExeCache.set(cacheKey, candidate);
92
- return [candidate, ...args.slice(1)];
156
+ resolvedExeCache.set(cacheKey, head);
157
+ return [...head, ...args.slice(1)];
93
158
  }
94
159
  } catch {
95
160
  /* stat race — skip */
package/src/sdk.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  // ── Core engine ──
7
7
  export { runPipeline, TriggerBlockedError, TriggerTimeoutError } from './engine';
8
- export type { EngineResult, RunPipelineOptions, PipelineEvent } from './engine';
8
+ export type { EngineResult, RunPipelineOptions, RunEventPayload } from './engine';
9
9
 
10
10
  // ── Pipeline runner (multi-pipeline lifecycle management) ──
11
11
  export { PipelineRunner } from './pipeline-runner';