@typecad/expect 1.0.0-alpha.13 → 1.0.0-alpha.15

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/dist/host/cli.js CHANGED
@@ -20,6 +20,9 @@
20
20
  // ---------------------------------------------------------------------------
21
21
  import { loadConfig } from './config.js';
22
22
  import { run } from './runner.js';
23
+ import { boardTestPins } from './test-pins.js';
24
+ import { listUsbSerialPorts, matchUsbPorts, formatUsbIdentity } from './port-discovery.js';
25
+ import { resolveProjectRoot } from './project-root.js';
23
26
  import path from 'node:path';
24
27
  function parseArgs(argv) {
25
28
  const result = { files: [] };
@@ -78,6 +81,9 @@ function parseArgs(argv) {
78
81
  case '-v':
79
82
  result.verbose = true;
80
83
  break;
84
+ case '--discover':
85
+ result.discover = true;
86
+ break;
81
87
  case '--dry-run':
82
88
  result.dryRun = true;
83
89
  break;
@@ -103,41 +109,44 @@ function parseArgs(argv) {
103
109
  // ---------------------------------------------------------------------------
104
110
  // Help text
105
111
  // ---------------------------------------------------------------------------
106
- const HELP = `
107
- \x1b[1m\x1b[36m cuttlefish-test\x1b[0m — Hardware test runner for TypeCAD
108
-
109
- \x1b[1mUsage:\x1b[0m
110
- cuttlefish-test [options] [files...]
111
-
112
- \x1b[1mOptions:\x1b[0m
113
- --port, -p <port> Serial port (e.g. COM4, /dev/ttyACM0)
114
- --board, -b <board> Board package override
115
- --build-target <id> Framework-specific build target override
116
- --baud <rate> Serial baud rate (default: 115200)
117
- --timeout, -t <ms> Serial read timeout (default: 30000)
118
- --include, -i <glob> Test file pattern (repeatable)
119
- --exclude, -x <glob> Test file pattern to skip (repeatable)
120
- --verbose, -v Show debug serial output
121
- --help, -h Show this help
122
-
123
- \x1b[1mExamples:\x1b[0m
124
- cuttlefish-test --port COM4
125
- cuttlefish-test --port /dev/ttyACM0 tests/my-test.test.ts
126
- cuttlefish-test -p COM4 -v
127
-
128
- \x1b[1mConfiguration:\x1b[0m
129
- Add a \`test\` section to your cuttlefish.config.ts:
130
-
131
- const config = {
132
- board: '@typecad/board-arduino-uno',
133
- test: {
134
- buildTarget: 'arduino:avr:uno',
135
- port: 'COM4',
136
- include: ['tests/**/*.test.ts'],
137
- baudRate: 115200,
138
- timeout: 30000,
139
- },
140
- };
112
+ const HELP = `
113
+ \x1b[1m\x1b[36m cuttlefish-test\x1b[0m — Hardware test runner for TypeCAD
114
+
115
+ \x1b[1mUsage:\x1b[0m
116
+ cuttlefish-test [options] [files...]
117
+
118
+ \x1b[1mOptions:\x1b[0m
119
+ --port, -p <port> Serial port (e.g. COM4, /dev/ttyACM0)
120
+ --board, -b <board> Board package override
121
+ --build-target <id> Framework-specific build target override
122
+ --baud <rate> Serial baud rate (default: 115200)
123
+ --timeout, -t <ms> Serial read timeout (default: 30000)
124
+ --include, -i <glob> Test file pattern (repeatable)
125
+ --exclude, -x <glob> Test file pattern to skip (repeatable)
126
+ --verbose, -v Show debug serial output
127
+ --discover List attached USB serial ports (VID:PID, serial,
128
+ manufacturer) and which one the current config/board
129
+ identity matches, then exit — test-box bring-up aid
130
+ --help, -h Show this help
131
+
132
+ \x1b[1mExamples:\x1b[0m
133
+ cuttlefish-test --port COM4
134
+ cuttlefish-test --port /dev/ttyACM0 tests/my-test.test.ts
135
+ cuttlefish-test -p COM4 -v
136
+
137
+ \x1b[1mConfiguration:\x1b[0m
138
+ Add a \`test\` section to your cuttlefish.config.ts:
139
+
140
+ const config = {
141
+ board: 'xiao_ble/nrf52840',
142
+ test: {
143
+ buildTarget: 'blackpill/stm32f411ce',
144
+ port: 'COM4',
145
+ include: ['tests/**/*.test.ts'],
146
+ baudRate: 115200,
147
+ timeout: 30000,
148
+ },
149
+ };
141
150
  `.trim();
142
151
  // ---------------------------------------------------------------------------
143
152
  // Main
@@ -148,7 +157,11 @@ async function main() {
148
157
  console.log(HELP);
149
158
  process.exit(0);
150
159
  }
151
- const projectRoot = process.cwd();
160
+ // npm exec / npm run reset the child cwd to the npm local prefix (the
161
+ // nearest package.json ancestor), stashing the real invocation dir in
162
+ // INIT_CWD — resolve through it so nested suite dirs work under npx.
163
+ // See host/project-root.ts for the full rationale.
164
+ const projectRoot = resolveProjectRoot();
152
165
  // Build config overrides from CLI args
153
166
  const overrides = {};
154
167
  if (args.port)
@@ -190,6 +203,37 @@ async function main() {
190
203
  // CLI mode flags (not config-file settings).
191
204
  config.dryRun = args.dryRun;
192
205
  config.bail = args.bail;
206
+ // --discover: list attached USB serial ports and annotate the match for
207
+ // the active board identity (config test.usb, else the board's
208
+ // test-pins.json usb block). Bring-up aid for multi-board test boxes.
209
+ if (args.discover) {
210
+ const ports = await listUsbSerialPorts();
211
+ const identity = config.test.usb ?? boardTestPins(config.board, config.projectRoot, config.configPath)?.usb;
212
+ const matches = identity ? matchUsbPorts(ports, identity) : [];
213
+ console.log('USB serial ports:');
214
+ if (ports.length === 0) {
215
+ console.log(' (none found)');
216
+ }
217
+ for (const p of ports) {
218
+ const isMatch = identity ? matchUsbPorts([p], identity).length > 0 : false;
219
+ const id = `${p.vid.toUpperCase()}:${p.pid.toUpperCase()}`;
220
+ const serial = p.serialNumber ? ` serial ${p.serialNumber}` : '';
221
+ const mfr = p.manufacturer ? ` [${p.manufacturer}]` : '';
222
+ console.log(` ${p.path} ${id}${serial}${mfr}${isMatch ? ' <-- matches this config' : ''}`);
223
+ }
224
+ console.log();
225
+ if (identity) {
226
+ console.log(matches.length === 1
227
+ ? `config identity ${formatUsbIdentity(identity)} -> ${matches[0].path}`
228
+ : `config identity ${formatUsbIdentity(identity)} -> no unique match (${matches.length} found)`);
229
+ }
230
+ else {
231
+ console.log('(no USB identity on this config or board — set test.usb or a test-pins.json usb block)');
232
+ }
233
+ // Exit 1 when an identity is set but has no unique match, so scripts
234
+ // can gate on discovery success.
235
+ process.exit(identity ? (matches.length === 1 ? 0 : 1) : 0);
236
+ }
193
237
  // Run tests
194
238
  const exitCode = await run(config);
195
239
  process.exit(exitCode);
@@ -1,7 +1,7 @@
1
1
  export interface CompileResult {
2
2
  success: boolean;
3
- sketchDir: string;
4
- sketchPath: string;
3
+ projectDir: string;
4
+ sourcePath: string;
5
5
  output: string;
6
6
  error?: string;
7
7
  }
@@ -11,17 +11,17 @@ export interface UploadResult {
11
11
  error?: string;
12
12
  }
13
13
  /**
14
- * Transpile preprocessed TypeScript source to a C++ Arduino sketch (or Zephyr
15
- * project). Writes the preprocessed source to a temp file, invokes the cuttlefish
16
- * transpiler, and returns the path to the generated .ino (Arduino) or .cpp
17
- * (Zephyr) entry file.
14
+ * Transpile preprocessed TypeScript source to a C++ Zephyr project. Writes
15
+ * the preprocessed source to a temp file, invokes the cuttlefish transpiler,
16
+ * and returns the path to the generated .cpp entry file.
18
17
  */
19
- export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string, toolchainType?: 'arduino-cli' | 'west', configPath?: string): CompileResult;
18
+ export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string, configPath?: string): CompileResult;
20
19
  /**
21
- * Compile the sketch/project via the configured toolchain (arduino-cli or west).
20
+ * Compile the Zephyr project via `west build` (through the Zephyr Toolchain).
22
21
  */
23
- export declare function compileSketch(sketchDir: string, buildTarget: string, framework?: string, toolchainType?: 'arduino-cli' | 'west', zephyrConfig?: Record<string, unknown>): CompileResult;
22
+ export declare function compileProgram(projectDir: string, buildTarget: string, zephyrConfig?: Record<string, unknown>): CompileResult;
24
23
  /**
25
- * Upload the compiled sketch/project to the board via the configured toolchain.
24
+ * Upload the compiled project to the board via `west flash` (through the
25
+ * Zephyr Toolchain).
26
26
  */
27
- export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string, framework?: string, toolchainType?: 'arduino-cli' | 'west', zephyrConfig?: Record<string, unknown>): UploadResult;
27
+ export declare function uploadProgram(projectDir: string, buildTarget: string, port: string, zephyrConfig?: Record<string, unknown>): UploadResult;
@@ -1,34 +1,25 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // @typecad/expect — Compiler
3
3
  //
4
- // Wraps the cuttlefish transpiler + arduino-cli compile/upload cycle.
5
- // Takes preprocessed TypeScript source, transpiles to C++, compiles, uploads.
4
+ // Wraps the cuttlefish transpiler + west compile/flash cycle. Takes
5
+ // preprocessed TypeScript source, transpiles to C++, compiles, uploads.
6
6
  // ---------------------------------------------------------------------------
7
7
  import path from 'node:path';
8
8
  import fs from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { createRequire } from 'node:module';
11
11
  import { parseConfigAST } from './config.js';
12
- import { checkArduinoEnv } from '@typecad/arduino-cli';
13
12
  // createRequire lets us use require() in an ESM module for the optional
14
- // framework-zephyr dynamic import (avoids a hard dependency for Arduino users).
13
+ // framework-zephyr dynamic import.
15
14
  const require = createRequire(import.meta.url);
16
- /** Format a check failure into the `error` field used by CompileResult/UploadResult. */
17
- function formatEnvFailure(failure) {
18
- const lines = [...failure.messages];
19
- if (failure.fixCommand)
20
- lines.push(` Fix: ${failure.fixCommand}`);
21
- return lines.join('\n');
22
- }
23
15
  /**
24
- * Transpile preprocessed TypeScript source to a C++ Arduino sketch (or Zephyr
25
- * project). Writes the preprocessed source to a temp file, invokes the cuttlefish
26
- * transpiler, and returns the path to the generated .ino (Arduino) or .cpp
27
- * (Zephyr) entry file.
16
+ * Transpile preprocessed TypeScript source to a C++ Zephyr project. Writes
17
+ * the preprocessed source to a temp file, invokes the cuttlefish transpiler,
18
+ * and returns the path to the generated .cpp entry file.
28
19
  */
29
- export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget, toolchainType = 'arduino-cli', configPath) {
20
+ export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget, configPath) {
30
21
  // Create a build directory for this test file. Per-file directories are
31
- // used — arduino-cli compile has no incremental benefit from a shared dir.
22
+ // used — west builds have no incremental benefit from a shared dir.
32
23
  const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
33
24
  const buildDir = path.join(projectRoot, '.build', 'expect', baseName);
34
25
  try {
@@ -37,7 +28,7 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
37
28
  fs.mkdirSync(buildDir, { recursive: true });
38
29
  }
39
30
  catch {
40
- return { success: false, sketchDir: buildDir, sketchPath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
31
+ return { success: false, projectDir: buildDir, sourcePath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
41
32
  }
42
33
  const rewrittenSource = rewriteRelativeImports(preprocessedSource, originalFilePath, buildDir);
43
34
  // Write the preprocessed source as a .ts file
@@ -51,7 +42,7 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
51
42
  // OR when an explicit configPath was passed. The latter matters because
52
43
  // direct-file mode discovers cuttlefish.config.ts from cwd (projectRoot),
53
44
  // which is the DEFAULT config — so a project with several target-specific
54
- // configs (e.g. tests/hardware/cuttlefish.config.ts vs ble-demo.config.ts)
45
+ // configs (e.g. packages/hal/tests/network/cuttlefish.config.ts vs ble-demo.config.ts)
55
46
  // would always transpile against the default. Build mode writes a config
56
47
  // derived from the chosen configPath into the build dir, so the right
57
48
  // board/MCU/target is used.
@@ -59,99 +50,70 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
59
50
  if (useBuildMode) {
60
51
  writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget, configPath);
61
52
  }
62
- const result = spawnSync(process.execPath, useBuildMode
63
- ? [cuttlefishCmd, 'build', '--skip-type-check', '--force']
64
- : [cuttlefishCmd, tsPath, '--skip-type-check', '--force'], {
53
+ const result = spawnSync(process.execPath,
54
+ // Explicit heap headroom for the transpile child. The steady-state
55
+ // transpile peaks well under 1 GB, but Node's default old-space cap
56
+ // (~4 GB on large-RAM machines) has been hit transiently — a GC storm
57
+ // then kills the child with "JavaScript heap out of memory" and fails
58
+ // the whole test file. Dedicated headroom makes a spike recoverable.
59
+ [
60
+ '--max-old-space-size=6144',
61
+ cuttlefishCmd,
62
+ ...(useBuildMode
63
+ ? ['build', '--skip-type-check', '--force']
64
+ : [tsPath, '--skip-type-check', '--force']),
65
+ ], {
65
66
  encoding: 'utf8',
66
67
  cwd: useBuildMode ? buildDir : projectRoot,
67
- timeout: 60000,
68
+ timeout: 120000,
68
69
  env: { ...process.env },
69
70
  });
70
71
  const transpileOutput = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
71
72
  if (result.status !== 0) {
73
+ // Surface spawn-level errors (EMFILE/ENOENT from a degraded runner —
74
+ // e.g. after serial-handle leaks): spawnSync reports them on .error
75
+ // with a null status and EMPTY stdout/stderr, which previously masked
76
+ // the cause as a bare "Transpilation failed:".
77
+ const spawnErr = result.error ? ` (spawn error: ${result.error.message})` : '';
72
78
  return {
73
79
  success: false,
74
- sketchDir: buildDir,
75
- sketchPath: '',
80
+ projectDir: buildDir,
81
+ sourcePath: '',
76
82
  output: transpileOutput,
77
- error: `Transpilation failed:\n${transpileOutput}`,
83
+ error: `Transpilation failed${spawnErr}:\n${transpileOutput}`,
78
84
  };
79
85
  }
80
- // Find the generated entry file (.ino for Arduino, .cpp for Zephyr)
81
- const outDir = findOutputDir(buildDir, baseName, projectRoot, toolchainType);
82
- const entryPath = findEntryFile(outDir, toolchainType);
86
+ // Find the generated entry file (.cpp for Zephyr)
87
+ const outDir = findOutputDir(buildDir, baseName, projectRoot);
88
+ const entryPath = findEntryFile(outDir);
83
89
  if (!entryPath) {
84
- const ext = toolchainType === 'west' ? '.cpp' : '.ino';
85
90
  return {
86
91
  success: false,
87
- sketchDir: outDir,
88
- sketchPath: '',
92
+ projectDir: outDir,
93
+ sourcePath: '',
89
94
  output: transpileOutput,
90
- error: `No ${ext} file found in ${outDir} after transpilation`,
95
+ error: `No .cpp file found in ${outDir} after transpilation`,
91
96
  };
92
97
  }
93
98
  return {
94
99
  success: true,
95
- sketchDir: path.dirname(entryPath),
96
- sketchPath: entryPath,
100
+ projectDir: path.dirname(entryPath),
101
+ sourcePath: entryPath,
97
102
  output: transpileOutput,
98
103
  };
99
104
  }
100
105
  /**
101
- * Compile the sketch/project via the configured toolchain (arduino-cli or west).
106
+ * Compile the Zephyr project via `west build` (through the Zephyr Toolchain).
102
107
  */
103
- export function compileSketch(sketchDir, buildTarget, framework, toolchainType = 'arduino-cli', zephyrConfig) {
104
- if (toolchainType === 'west') {
105
- return compileWestProject(sketchDir, buildTarget, zephyrConfig);
106
- }
107
- return compileArduinoSketch(sketchDir, buildTarget);
108
- }
109
- /** Compile via arduino-cli. */
110
- function compileArduinoSketch(sketchDir, buildTarget) {
111
- // Hard gate: verify arduino-cli + core before spawning.
112
- {
113
- const gate = checkArduinoEnv(buildTarget);
114
- if (!gate.ok) {
115
- const message = formatEnvFailure(gate);
116
- return { success: false, sketchDir, sketchPath: '', output: message, error: message };
117
- }
118
- }
119
- const result = spawnSync('arduino-cli', ['compile', '--fqbn', buildTarget, sketchDir], { encoding: 'utf8', timeout: 120000 });
120
- const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
121
- return {
122
- success: result.status === 0,
123
- sketchDir,
124
- sketchPath: '',
125
- output,
126
- error: result.status !== 0 ? `Compilation failed:\n${output}` : undefined,
127
- };
108
+ export function compileProgram(projectDir, buildTarget, zephyrConfig) {
109
+ return compileWestProject(projectDir, buildTarget, zephyrConfig);
128
110
  }
129
111
  /**
130
- * Upload the compiled sketch/project to the board via the configured toolchain.
112
+ * Upload the compiled project to the board via `west flash` (through the
113
+ * Zephyr Toolchain).
131
114
  */
132
- export function uploadSketch(sketchDir, buildTarget, port, framework, toolchainType = 'arduino-cli', zephyrConfig) {
133
- if (toolchainType === 'west') {
134
- return uploadWestProject(sketchDir, buildTarget, port, zephyrConfig);
135
- }
136
- return uploadArduinoSketch(sketchDir, buildTarget, port);
137
- }
138
- /** Upload via arduino-cli. */
139
- function uploadArduinoSketch(sketchDir, buildTarget, port) {
140
- // Hard gate: verify arduino-cli + core before spawning.
141
- {
142
- const gate = checkArduinoEnv(buildTarget);
143
- if (!gate.ok) {
144
- const message = formatEnvFailure(gate);
145
- return { success: false, output: message, error: message };
146
- }
147
- }
148
- const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
149
- const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
150
- return {
151
- success: result.status === 0,
152
- output,
153
- error: result.status !== 0 ? `Upload failed:\n${output}` : undefined,
154
- };
115
+ export function uploadProgram(projectDir, buildTarget, port, zephyrConfig) {
116
+ return uploadWestProject(projectDir, buildTarget, port, zephyrConfig);
155
117
  }
156
118
  // ---------------------------------------------------------------------------
157
119
  // Zephyr (west) toolchain
@@ -159,25 +121,25 @@ function uploadArduinoSketch(sketchDir, buildTarget, port) {
159
121
  /**
160
122
  * Compile a Zephyr project via `west build`. The Zephyr Toolchain (from
161
123
  * @typecad/framework-zephyr) handles west discovery, ZEPHYR_BASE, scaffolding,
162
- * and the board target. We call it via dynamic import to avoid a hard
163
- * dependency on framework-zephyr (the Arduino path doesn't need it).
124
+ * and the board target. We call it via dynamic import so expect only gains
125
+ * the dependency when framework-zephyr is installed.
164
126
  */
165
- function compileWestProject(sketchDir, buildTarget, zephyrConfig) {
166
- // sketchDir for Zephyr is the project root containing src/, app/, build/.
127
+ function compileWestProject(projectDir, buildTarget, zephyrConfig) {
128
+ // projectDir for Zephyr is the project root containing src/, app/, build/.
167
129
  // The transpiler emits src/src.cpp; the west project root is the parent of src/.
168
- const srcDir = path.join(sketchDir, 'src');
169
- const projectRoot = fs.existsSync(srcDir) ? sketchDir : path.dirname(sketchDir);
130
+ const srcDir = path.join(projectDir, 'src');
131
+ const projectRoot = fs.existsSync(srcDir) ? projectDir : path.dirname(projectDir);
170
132
  const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
171
133
  ? path.join(srcDir, 'src.cpp')
172
- : path.join(sketchDir, 'src.cpp');
173
- const outputDir = fs.existsSync(srcDir) ? srcDir : sketchDir;
134
+ : path.join(projectDir, 'src.cpp');
135
+ const outputDir = fs.existsSync(srcDir) ? srcDir : projectDir;
174
136
  try {
175
137
  // Dynamic import — framework-zephyr is an optional dependency (only present
176
138
  // for Zephyr projects). The Toolchain object has compile()/upload().
177
139
  const mod = require('@typecad/framework-zephyr');
178
140
  const Toolchain = mod.Toolchain;
179
141
  if (!Toolchain || typeof Toolchain.compile !== 'function') {
180
- return { success: false, sketchDir, sketchPath: '', output: '', error: '@typecad/framework-zephyr did not export a usable Toolchain.compile().' };
142
+ return { success: false, projectDir, sourcePath: '', output: '', error: '@typecad/framework-zephyr did not export a usable Toolchain.compile().' };
181
143
  }
182
144
  const result = Toolchain.compile({
183
145
  outputDir,
@@ -187,27 +149,27 @@ function compileWestProject(sketchDir, buildTarget, zephyrConfig) {
187
149
  });
188
150
  return {
189
151
  success: result.success,
190
- sketchDir: projectRoot,
191
- sketchPath: sourcePath,
152
+ projectDir: projectRoot,
153
+ sourcePath: sourcePath,
192
154
  output: result.output,
193
155
  error: result.success ? undefined : `west build failed:\n${result.output}`,
194
156
  };
195
157
  }
196
158
  catch (e) {
197
- return { success: false, sketchDir, sketchPath: '', output: '', error: `Failed to compile via west: ${e.message}` };
159
+ return { success: false, projectDir, sourcePath: '', output: '', error: `Failed to compile via west: ${e.message}` };
198
160
  }
199
161
  }
200
162
  /**
201
163
  * Upload (flash) a Zephyr project via `west flash`. For ESP32 boards, west
202
164
  * uses the esptool runner; for nRF boards, nrfjprog. The port is forwarded.
203
165
  */
204
- function uploadWestProject(sketchDir, buildTarget, port, zephyrConfig) {
205
- const srcDir = path.join(sketchDir, 'src');
206
- const projectRoot = fs.existsSync(srcDir) ? sketchDir : path.dirname(sketchDir);
166
+ function uploadWestProject(projectDir, buildTarget, port, zephyrConfig) {
167
+ const srcDir = path.join(projectDir, 'src');
168
+ const projectRoot = fs.existsSync(srcDir) ? projectDir : path.dirname(projectDir);
207
169
  const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
208
170
  ? path.join(srcDir, 'src.cpp')
209
- : path.join(sketchDir, 'src.cpp');
210
- const outputDir = fs.existsSync(srcDir) ? srcDir : sketchDir;
171
+ : path.join(projectDir, 'src.cpp');
172
+ const outputDir = fs.existsSync(srcDir) ? srcDir : projectDir;
211
173
  try {
212
174
  const mod = require('@typecad/framework-zephyr');
213
175
  const Toolchain = mod.Toolchain;
@@ -258,7 +220,7 @@ function resolveCuttlefishCmd(projectRoot) {
258
220
  // Fallback: assume it's on PATH
259
221
  return 'cuttlefish';
260
222
  }
261
- function findOutputDir(buildDir, baseName, projectRoot, toolchainType = 'arduino-cli') {
223
+ function findOutputDir(buildDir, baseName, projectRoot) {
262
224
  // The cuttlefish transpiler writes output next to the source by default,
263
225
  // or to the configured outDir. Check common locations.
264
226
  const candidates = [
@@ -270,59 +232,54 @@ function findOutputDir(buildDir, baseName, projectRoot, toolchainType = 'arduino
270
232
  path.join(projectRoot, '.build', 'expect', baseName, baseName),
271
233
  ];
272
234
  for (const c of candidates) {
273
- if (fs.existsSync(c) && hasEntryFile(c, toolchainType))
235
+ if (fs.existsSync(c) && hasEntryFile(c))
274
236
  return c;
275
237
  }
276
238
  // Last resort: walk the buildDir tree recursively to find any entry file
277
- const found = findEntryFileRecursive(buildDir, toolchainType);
239
+ const found = findEntryFileRecursive(buildDir);
278
240
  if (found)
279
241
  return path.dirname(found);
280
242
  return buildDir;
281
243
  }
282
- /** Check for a .ino (Arduino) or .cpp (Zephyr) entry file in a directory. */
283
- function hasEntryFile(dir, toolchainType = 'arduino-cli') {
244
+ /** Check for a .cpp entry file in a directory. */
245
+ function hasEntryFile(dir) {
284
246
  if (!fs.existsSync(dir))
285
247
  return false;
286
- return fs.readdirSync(dir).some(f => isEntryFileName(f, toolchainType));
248
+ return fs.readdirSync(dir).some(f => isEntryFileName(f));
287
249
  }
288
- /** True if the filename is a valid entry file for the toolchain. */
289
- function isEntryFileName(name, toolchainType) {
290
- if (toolchainType === 'west') {
291
- return name.endsWith('.cpp') || name.endsWith('.cc');
292
- }
293
- return name.endsWith('.ino') || name.endsWith('.cc');
250
+ /** True if the filename is a valid entry file (.cpp/.cc). */
251
+ function isEntryFileName(name) {
252
+ return name.endsWith('.cpp') || name.endsWith('.cc');
294
253
  }
295
- /** Find the entry file (.ino for Arduino, .cpp for Zephyr) in a directory. */
296
- function findEntryFile(dir, toolchainType = 'arduino-cli') {
254
+ /** Find the entry .cpp file in a directory (or its src/ subdirectory). */
255
+ function findEntryFile(dir) {
297
256
  if (!fs.existsSync(dir))
298
257
  return undefined;
299
258
  for (const entry of fs.readdirSync(dir)) {
300
- if (isEntryFileName(entry, toolchainType)) {
259
+ if (isEntryFileName(entry)) {
301
260
  return path.join(dir, entry);
302
261
  }
303
262
  }
304
- // For Zephyr, the entry may be in a src/ subdirectory
305
- if (toolchainType === 'west') {
306
- const srcDir = path.join(dir, 'src');
307
- if (fs.existsSync(srcDir)) {
308
- for (const entry of fs.readdirSync(srcDir)) {
309
- if (isEntryFileName(entry, toolchainType)) {
310
- return path.join(srcDir, entry);
311
- }
263
+ // The entry may be in a src/ subdirectory
264
+ const srcDir = path.join(dir, 'src');
265
+ if (fs.existsSync(srcDir)) {
266
+ for (const entry of fs.readdirSync(srcDir)) {
267
+ if (isEntryFileName(entry)) {
268
+ return path.join(srcDir, entry);
312
269
  }
313
270
  }
314
271
  }
315
272
  return undefined;
316
273
  }
317
- function findEntryFileRecursive(dir, toolchainType = 'arduino-cli') {
274
+ function findEntryFileRecursive(dir) {
318
275
  if (!fs.existsSync(dir))
319
276
  return undefined;
320
277
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
321
- if (isEntryFileName(entry.name, toolchainType)) {
278
+ if (isEntryFileName(entry.name)) {
322
279
  return path.join(dir, entry.name);
323
280
  }
324
281
  if (entry.isDirectory()) {
325
- const result = findEntryFileRecursive(path.join(dir, entry.name), toolchainType);
282
+ const result = findEntryFileRecursive(path.join(dir, entry.name));
326
283
  if (result)
327
284
  return result;
328
285
  }
@@ -379,11 +336,6 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget, con
379
336
  lines.push(` framework: '${baseValues.output?.framework}',`);
380
337
  lines.push(` outDir: './out',`);
381
338
  lines.push(' },');
382
- if (baseValues.console?.baudRate) {
383
- lines.push(' console: {');
384
- lines.push(` baudRate: ${baseValues.console?.baudRate},`);
385
- lines.push(' },');
386
- }
387
339
  // Pass the zephyr section (kconfig, runner) through verbatim — the Zephyr
388
340
  // toolchain reads runner from it (e.g. zephyr.runner: 'uf2'). Serialize the
389
341
  // parsed object as a minimal object literal (string values only; sufficient
@@ -406,12 +358,12 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget, con
406
358
  '',
407
359
  'const config: CuttlefishConfig = {',
408
360
  ` entry: './${entryFileName}',`,
409
- ` target: 'avr',`,
361
+ ` framework: '@typecad/framework-zephyr',`,
410
362
  ` frameworkData: { buildTarget: '${buildTarget}' },`,
411
363
  ' output: {',
412
- ` framework: 'arduino',`,
413
364
  ` outDir: './out',`,
414
365
  ' },',
366
+ ' toolchain: { type: \'west\' },',
415
367
  '};',
416
368
  '',
417
369
  'export default config;',
@@ -26,8 +26,5 @@ export interface RawConfig {
26
26
  framework?: string;
27
27
  outDir?: string;
28
28
  };
29
- console?: {
30
- baudRate?: number;
31
- };
32
29
  }
33
30
  export declare function parseConfigAST(configPath: string): RawConfig;