@typecad/cuttlefish 0.1.0-alpha.2 → 1.0.0-alpha.6

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 (54) hide show
  1. package/README.md +10 -10
  2. package/dist/api/board-types.d.ts +1 -1
  3. package/dist/api/shared/hal-op-ir.d.ts +10 -1
  4. package/dist/api/shared/platform-strategy.d.ts +6 -0
  5. package/dist/api/shared/types.d.ts +8 -0
  6. package/dist/cli-utils.d.ts +1 -0
  7. package/dist/cli-utils.js +3 -1
  8. package/dist/cli.js +75 -0
  9. package/dist/create/index.d.ts +1 -1
  10. package/dist/create/index.js +1 -1
  11. package/dist/create/init-scaffold.js +48 -2
  12. package/dist/create/init-templates.d.ts +2 -0
  13. package/dist/create/init-templates.js +189 -9
  14. package/dist/emit/emitters/setup.js +122 -1
  15. package/dist/ir/adc-range-validation.js +26 -25
  16. package/dist/ir/expression-to-ir.js +43 -6
  17. package/dist/ir/hal/hal-plugins.js +10 -0
  18. package/dist/ir/interrupt-analysis.js +8 -3
  19. package/dist/ir/memory-budget-validation.js +1 -0
  20. package/dist/ir/ownership-analysis.js +19 -0
  21. package/dist/ir/peripheral-ownership.js +5 -0
  22. package/dist/ir/peripheral-validation.d.ts +1 -1
  23. package/dist/ir/peripheral-validation.js +6 -3
  24. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  25. package/dist/ir/pin-alias-conflict.js +2 -1
  26. package/dist/ir/pin-capability-validation.js +34 -32
  27. package/dist/ir/pin-mode-validation.js +5 -0
  28. package/dist/ir/pin-safety.d.ts +1 -1
  29. package/dist/ir/pin-safety.js +2 -1
  30. package/dist/ir/program-analysis.d.ts +16 -0
  31. package/dist/ir/program-analysis.js +113 -0
  32. package/dist/ir/pulldown-validation.d.ts +1 -1
  33. package/dist/ir/pulldown-validation.js +2 -1
  34. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  35. package/dist/ir/pwm-timer-sharing.js +2 -1
  36. package/dist/ir/resource-analysis.js +2 -0
  37. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  38. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  39. package/dist/ir/timing-validation.js +1 -0
  40. package/dist/ir/transformers/variables.js +46 -17
  41. package/dist/ir/try-catch-validation.js +2 -0
  42. package/dist/ir/type-resolution.js +2 -2
  43. package/dist/ir/unit-suspicion-validation.js +9 -7
  44. package/dist/ir/validation-orchestrator.js +6 -6
  45. package/dist/licenses.d.ts +185 -0
  46. package/dist/licenses.js +963 -0
  47. package/dist/testing.d.ts +1 -1
  48. package/dist/testing.js +1 -1
  49. package/dist/transpile.js +17 -9
  50. package/dist/types.d.ts +7 -1
  51. package/dist/utils/cli.js +44 -0
  52. package/package.json +5 -4
  53. package/dist/ir/heap-array-validation.d.ts +0 -24
  54. package/dist/ir/heap-array-validation.js +0 -29
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  TypeScript → C++ transpiler for embedded firmware. Targets native (desktop),
4
4
  Arduino, and bare-metal MCU builds from a single TypeScript codebase.
5
5
 
6
- `cuttlefish` is the command-line tool at the center of the [TypeCAD](https://github.com/justind000/typecode)
6
+ `cuttlefish` is the command-line tool at the center of the [TypeCAD](https://cuttlefish.typecad.net)
7
7
  toolchain: it loads `cuttlefish.config.ts`, transpiles TypeScript firmware to
8
8
  C++, and can chain compile, upload, and serial-monitor steps.
9
9
 
@@ -12,15 +12,15 @@ C++, and can chain compile, upload, and serial-monitor steps.
12
12
  From a project root containing `cuttlefish.config.ts`:
13
13
 
14
14
  ```bash
15
- npx cuttlefish build
16
- npx cuttlefish build --compile --upload --port COM4
17
- npx cuttlefish build --compile --upload --monitor --port COM4 --baud 115200
15
+ npx @typecad/cuttlefish build
16
+ npx @typecad/cuttlefish build --compile --upload --port COM4
17
+ npx @typecad/cuttlefish build --compile --upload --monitor --port COM4 --baud 115200
18
18
  ```
19
19
 
20
20
  Scaffold a starter project with the built-in wizard:
21
21
 
22
22
  ```bash
23
- npx cuttlefish create --board arduino:avr:uno --framework arduino
23
+ npx @typecad/cuttlefish create --board arduino:avr:uno --framework arduino
24
24
  ```
25
25
 
26
26
  ## Commands
@@ -67,11 +67,11 @@ export default config;
67
67
 
68
68
  `@typecad/cuttlefish` is the transpiler core. It pairs with sibling packages:
69
69
 
70
- - [`@typecad/hal`](https://github.com/justind000/typecode/tree/main/packages/hal) — hardware abstraction (GPIO, I2C, SPI, UART) as regular TypeScript.
71
- - [`@typecad/ui`](https://github.com/justind000/typecode/tree/main/packages/ui) — HTML/CSS-driven graphics for microcontroller displays.
72
- - [`@typecad/expect`](https://github.com/justind000/typecode/tree/main/packages/expect) — hardware test framework (vitest-style assertions over serial).
73
- - [`@typecad/framework-arduino`](https://github.com/justind000/typecode/tree/main/packages/framework-arduino) — Arduino framework code-gen strategy.
74
- - [`@typecad/framework-native`](https://github.com/justind000/typecode/tree/main/packages/framework-native) — native desktop C++ code-gen strategy.
70
+ - [`@typecad/hal`](https://cuttlefish.typecad.net) — hardware abstraction (GPIO, I2C, SPI, UART) as regular TypeScript.
71
+ - [`@typecad/ui`](https://cuttlefish.typecad.net) — HTML/CSS-driven graphics for microcontroller displays.
72
+ - [`@typecad/expect`](https://cuttlefish.typecad.net) — hardware test framework (vitest-style assertions over serial).
73
+ - [`@typecad/framework-arduino`](https://cuttlefish.typecad.net) — Arduino framework code-gen strategy.
74
+ - [`@typecad/framework-native`](https://cuttlefish.typecad.net) — native desktop C++ code-gen strategy.
75
75
  - `@typecad/mcu-*` and `@typecad/board-*` — silicon- and board-level pin/peripheral definitions.
76
76
 
77
77
  ## License
@@ -1 +1 @@
1
- export type ArchitectureIdentifier = 'avr' | 'esp32' | 'esp32s2' | 'esp32s3' | 'esp32c3' | 'esp32c6' | 'rp2040' | 'samd' | 'stm32' | 'nrf52' | (string & {});
1
+ export type ArchitectureIdentifier = 'avr' | 'esp32' | 'esp32s2' | 'esp32s3' | 'esp32c3' | 'esp32c6' | 'rp2040' | 'rp2350' | 'samd' | 'stm32' | 'nrf52' | (string & {});
@@ -267,6 +267,15 @@ export interface SpiCsHighOp {
267
267
  port?: string;
268
268
  pin: number;
269
269
  }
270
+ export interface SpiReadBufferOp {
271
+ operation: "spi.read_buffer";
272
+ bus: string;
273
+ /** Number of bytes — numeric or runtime expression string */
274
+ count: number | string;
275
+ /** Buffer variable name, "__HAL_READ_BUF__" placeholder (rewritten to the
276
+ * caller's variable by the variable-init transformer), or "__DISCARD__" */
277
+ buffer: string;
278
+ }
270
279
  export interface UartBeginOp {
271
280
  operation: "uart.begin";
272
281
  port: string;
@@ -385,7 +394,7 @@ export interface RawCppOp {
385
394
  * operations. Each node carries a semantic `operation` tag and typed
386
395
  * arguments. Framework strategies translate these into concrete C++.
387
396
  */
388
- export type HALOpIR = GpioWriteOp | GpioReadOp | GpioToggleOp | GpioSetModeOp | PwmWriteOp | PwmGetFrequencyOp | PwmGetResolutionOp | AdcReadOp | AdcGetResolutionOp | AdcSetReferenceOp | AdcGetReferenceOp | AdcReadVoltageOp | DacWriteOp | InterruptAttachOp | InterruptDetachOp | TonePlayOp | ToneStopOp | TimingDelayOp | TimingDelayMicrosecondsOp | TimingMillisOp | TimingMicrosOp | TimingFreeHeapOp | TimingSetIntervalOp | TimingSetTimeoutOp | TimingClearIntervalOp | TimingClearTimeoutOp | PowerDeepSleepOp | PowerLightSleepOp | PowerSetCpuFrequencyOp | I2cBeginOp | I2cEndOp | I2cSetClockOp | I2cBeginTransmissionOp | I2cWriteOp | I2cWriteBytesOp | I2cWriteBufferOp | I2cReadBufferOp | I2cEndTransmissionOp | I2cRequestFromOp | I2cAvailableOp | I2cReadOp | I2cRecoverOp | SpiBeginOp | SpiEndOp | SpiTransferOp | SpibeginTransactionOp | SpiEndTransactionOp | SpiSetModeOp | SpiSetBitOrderOp | SpiCsLowOp | SpiCsHighOp | UartBeginOp | UartEndOp | UartPrintOp | UartPrintlnOp | UartPrintfOp | UartWriteOp | UartReadOp | UartPeekOp | UartAvailableOp | UartFlushOp | PulseInOp | PulseInLongOp | ShiftOutOp | ShiftInOp | BoardResolveOp | WdtEnableOp | WdtResetOp | WdtDisableOp | SnprintfEmitOp | RawCppOp | DisplayHALOp;
397
+ export type HALOpIR = GpioWriteOp | GpioReadOp | GpioToggleOp | GpioSetModeOp | PwmWriteOp | PwmGetFrequencyOp | PwmGetResolutionOp | AdcReadOp | AdcGetResolutionOp | AdcSetReferenceOp | AdcGetReferenceOp | AdcReadVoltageOp | DacWriteOp | InterruptAttachOp | InterruptDetachOp | TonePlayOp | ToneStopOp | TimingDelayOp | TimingDelayMicrosecondsOp | TimingMillisOp | TimingMicrosOp | TimingFreeHeapOp | TimingSetIntervalOp | TimingSetTimeoutOp | TimingClearIntervalOp | TimingClearTimeoutOp | PowerDeepSleepOp | PowerLightSleepOp | PowerSetCpuFrequencyOp | I2cBeginOp | I2cEndOp | I2cSetClockOp | I2cBeginTransmissionOp | I2cWriteOp | I2cWriteBytesOp | I2cWriteBufferOp | I2cReadBufferOp | I2cEndTransmissionOp | I2cRequestFromOp | I2cAvailableOp | I2cReadOp | I2cRecoverOp | SpiBeginOp | SpiEndOp | SpiTransferOp | SpibeginTransactionOp | SpiEndTransactionOp | SpiSetModeOp | SpiSetBitOrderOp | SpiCsLowOp | SpiCsHighOp | SpiReadBufferOp | UartBeginOp | UartEndOp | UartPrintOp | UartPrintlnOp | UartPrintfOp | UartWriteOp | UartReadOp | UartPeekOp | UartAvailableOp | UartFlushOp | PulseInOp | PulseInLongOp | ShiftOutOp | ShiftInOp | BoardResolveOp | WdtEnableOp | WdtResetOp | WdtDisableOp | SnprintfEmitOp | RawCppOp | DisplayHALOp;
389
398
  /**
390
399
  * Helper type: extracts the operation string from a HALOpIR variant.
391
400
  * Useful for type-safe switch statements in framework strategies.
@@ -8,6 +8,12 @@ import type { PlatformGraphicsStrategy } from './graphics-strategy.js';
8
8
  export interface PlatformProfileStrategy {
9
9
  /** System #include headers forced at the top of every emitted file. */
10
10
  forcedIncludes(program: ProgramIR, ctx?: PlatformContext): string[];
11
+ /**
12
+ * Filter the HAL-discovered required includes (from __includes metadata).
13
+ * Frameworks that provide native peripheral drivers can strip stale Arduino
14
+ * library includes here. Defaults to returning the includes unchanged.
15
+ */
16
+ filterRequiredIncludes?(includes: string[]): string[];
11
17
  /** Symbol aliases (TypeScript name → C++ name). */
12
18
  symbolAliases(program: ProgramIR, ctx?: PlatformContext): Record<string, string>;
13
19
  /** Extra shim lines emitted after includes (e.g. #define fallbacks). */
@@ -26,6 +26,14 @@ export interface Diagnostic {
26
26
  column?: number;
27
27
  code?: string;
28
28
  source?: string;
29
+ /**
30
+ * Path (or basename) of the source file the diagnostic refers to. When set,
31
+ * it is rendered in the diagnostic header so the location is not just
32
+ * `(line,col)` but `path/to/file.ts (line,col)`. Emitters should prefer the
33
+ * real file path from the originating node's sourceSpan, falling back to a
34
+ * basename when the full path is unavailable.
35
+ */
36
+ filePath?: string;
29
37
  /** Text of the source line for display context. */
30
38
  sourceLine?: string;
31
39
  }
@@ -13,6 +13,7 @@ export declare function printDiagnostics(diagnostics: Array<Diagnostic | {
13
13
  line?: number;
14
14
  column?: number;
15
15
  code?: string;
16
+ filePath?: string;
16
17
  sourceLine?: string;
17
18
  }>): void;
18
19
  /**
package/dist/cli-utils.js CHANGED
@@ -50,6 +50,7 @@ export function printDiagnostics(diagnostics) {
50
50
  for (const diagnostic of diagnostics) {
51
51
  const position = diagnostic.line && diagnostic.column ? chalk.gray(`(${diagnostic.line},${diagnostic.column})`) : "";
52
52
  const code = diagnostic.code ? chalk.gray(` [${diagnostic.code}]`) : "";
53
+ const file = 'filePath' in diagnostic && diagnostic.filePath ? chalk.cyan(diagnostic.filePath) : "";
53
54
  let severityLabel;
54
55
  let hintColor;
55
56
  if (diagnostic.severity === "error") {
@@ -65,7 +66,8 @@ export function printDiagnostics(diagnostics) {
65
66
  hintColor = chalk.cyan;
66
67
  }
67
68
  const location = position ? ` ${position}` : "";
68
- const header = `${severityLabel}${code}${location}: ${chalk.white(diagnostic.message)}`;
69
+ const filePrefix = file ? `${file} ` : "";
70
+ const header = `${filePrefix}${severityLabel}${code}${location}: ${chalk.white(diagnostic.message)}`;
69
71
  if (diagnostic.severity === "error") {
70
72
  console.error(header);
71
73
  }
package/dist/cli.js CHANGED
@@ -19,6 +19,8 @@ import { runExpectTests, assertTypeScriptInput, printDiagnostics, printMappedCom
19
19
  import { runPreviewServer } from "./preview/server.js";
20
20
  import * as ui from "./utils/ui.js";
21
21
  import chalk from "chalk";
22
+ import { checkArduinoEnv } from "@typecad/arduino-cli";
23
+ import { runLicensesPresenter } from "./licenses.js";
22
24
  function hasFatalDiagnostics(result) {
23
25
  return result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
24
26
  }
@@ -107,6 +109,62 @@ async function handleBoardAdd(options) {
107
109
  }
108
110
  console.log(generateFrameworkChecklist(spec));
109
111
  }
112
+ /**
113
+ * `cuttlefish doctor` — verify arduino-cli is installed and the board's core
114
+ * (derived from the FQBN in cuttlefish.config.ts) is present. Exits 0 if the
115
+ * environment is OK, non-zero otherwise. Reuses checkArduinoEnv so the
116
+ * detection logic is shared with the build/test gates.
117
+ */
118
+ function runDoctor() {
119
+ ui.printHeader();
120
+ ui.printStep("Checking arduino-cli environment...");
121
+ const config = loadCuttlefishConfig(process.cwd());
122
+ const fqbn = config?.buildTarget;
123
+ const result = checkArduinoEnv(fqbn);
124
+ const check = result.check;
125
+ // arduino-cli presence line
126
+ if (check.arduinoCliInstalled) {
127
+ ui.printInfo(`arduino-cli .... ${check.arduinoCliVersion ?? "unknown"} ✓`);
128
+ }
129
+ else if (!result.ok && result.reason === "arduino-cli-not-found") {
130
+ ui.printError(`arduino-cli .... NOT FOUND on PATH`);
131
+ }
132
+ else {
133
+ ui.printError(`arduino-cli .... found but unresponsive`);
134
+ }
135
+ // core presence line (only meaningful if we have an FQBN)
136
+ if (fqbn) {
137
+ if (check.requiredCore) {
138
+ const status = check.requiredCoreInstalled ? "installed ✓" : "NOT installed ✗";
139
+ const line = `${check.requiredCore} ....... ${status}`;
140
+ if (check.requiredCoreInstalled) {
141
+ ui.printInfo(line);
142
+ }
143
+ else {
144
+ ui.printError(line);
145
+ ui.printInfo(` → run: arduino-cli core install ${check.requiredCore}`);
146
+ }
147
+ }
148
+ }
149
+ else {
150
+ ui.printInfo("(no buildTarget in cuttlefish.config.ts — skipping core check)");
151
+ }
152
+ // Exit code
153
+ if (result.ok) {
154
+ ui.printSuccess("Environment OK");
155
+ return; // exitCode stays unset => 0
156
+ }
157
+ if (!result.ok) {
158
+ for (const line of result.messages)
159
+ ui.printInfo(line);
160
+ process.exitCode = 1;
161
+ }
162
+ }
163
+ /**
164
+ * `cuttlefish licenses` dispatch — the presenter lives in licenses.ts so it is
165
+ * unit-testable without importing this binary entry module (cli.ts has a
166
+ * shebang and runs main() at import time). See runLicensesPresenter.
167
+ */
110
168
  async function main() {
111
169
  try {
112
170
  const options = parseCommandLine(process.argv);
@@ -154,6 +212,14 @@ async function main() {
154
212
  }
155
213
  return;
156
214
  }
215
+ if (options.command === "doctor") {
216
+ runDoctor();
217
+ return;
218
+ }
219
+ if (options.command === "licenses") {
220
+ runLicensesPresenter(options.strict ?? false, options.all ?? false);
221
+ return;
222
+ }
157
223
  // ── Handle build command — entry point comes from config ──────────
158
224
  if (options.command === "build") {
159
225
  const buildConfig = loadCuttlefishConfig(process.cwd());
@@ -361,6 +427,9 @@ async function main() {
361
427
  });
362
428
  printDiagnostics(result.diagnostics);
363
429
  ui.printTasks(result.asyncTaskNames ?? [], result.usesTimers ?? false);
430
+ if (result.diagnosticsReportPath) {
431
+ ui.printInfo(`Diagnostics report: ${result.diagnosticsReportPath}`);
432
+ }
364
433
  if (result.diagnostics.length === 0) {
365
434
  ui.printSuccess();
366
435
  initialBuildOk = true;
@@ -451,6 +520,9 @@ async function main() {
451
520
  });
452
521
  printDiagnostics(rebuildResult.diagnostics);
453
522
  ui.printTasks(rebuildResult.asyncTaskNames ?? [], rebuildResult.usesTimers ?? false);
523
+ if (rebuildResult.diagnosticsReportPath) {
524
+ ui.printInfo(`Diagnostics report: ${rebuildResult.diagnosticsReportPath}`);
525
+ }
454
526
  if (rebuildResult.diagnostics.length > 0) {
455
527
  // Errors shown via printDiagnostics — skip compile/upload
456
528
  }
@@ -535,6 +607,9 @@ async function main() {
535
607
  });
536
608
  printDiagnostics(result.diagnostics);
537
609
  ui.printTasks(result.asyncTaskNames ?? [], result.usesTimers ?? false);
610
+ if (result.diagnosticsReportPath) {
611
+ ui.printInfo(`Diagnostics report: ${result.diagnosticsReportPath}`);
612
+ }
538
613
  }
539
614
  if (hasFatalDiagnostics(result)) {
540
615
  ui.printError("Transpilation failed. Fix the transpiler diagnostics above before compiling.");
@@ -1,6 +1,6 @@
1
1
  export { scaffoldProject, normalizeProjectName, KNOWN_TARGETS, KNOWN_BOARDS, registerKnownTarget, printInitNextSteps } from './init-scaffold.js';
2
2
  export type { KnownTarget, KnownBoard, ScaffoldProjectResult } from './init-scaffold.js';
3
- export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateGitignore, generateEslintConfig, } from './init-templates.js';
3
+ export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
4
4
  export type { InitProjectOptions } from './init-templates.js';
5
5
  export { runInitWizard } from './init-wizard.js';
6
6
  export { scaffoldBoardPackages } from './board-codegen.js';
@@ -1,5 +1,5 @@
1
1
  export { scaffoldProject, normalizeProjectName, KNOWN_TARGETS, KNOWN_BOARDS, registerKnownTarget, printInitNextSteps } from './init-scaffold.js';
2
- export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateGitignore, generateEslintConfig, } from './init-templates.js';
2
+ export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
3
3
  export { runInitWizard } from './init-wizard.js';
4
4
  // Board codegen tool (`cuttlefish board add`)
5
5
  export { scaffoldBoardPackages } from './board-codegen.js';
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import chalk from "chalk";
4
- import { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateGitignore, generateBoardForwardingFile, generateEslintConfig, } from "./init-templates.js";
4
+ import { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateBoardForwardingFile, generateEslintConfig, } from "./init-templates.js";
5
5
  import { generateEslintRules } from "./eslint-rules-template.js";
6
6
  const _knownTargets = [
7
7
  {
@@ -66,6 +66,28 @@ const _knownTargets = [
66
66
  buildTarget: 'esp32:esp32:esp32c6',
67
67
  mcu: 'esp32c6',
68
68
  },
69
+ {
70
+ id: 'rp2040',
71
+ displayName: 'RP2040 (Pico)',
72
+ isNative: false,
73
+ architecture: 'rp2040',
74
+ boardPackage: '@typecad/board-rp2040',
75
+ frameworkPackage: '@typecad/framework-arduino',
76
+ framework: 'arduino',
77
+ buildTarget: 'rp2040:rp2040:rpipico',
78
+ mcu: 'rp2040',
79
+ },
80
+ {
81
+ id: 'rp2350',
82
+ displayName: 'RP2350 (Pico 2)',
83
+ isNative: false,
84
+ architecture: 'rp2350',
85
+ boardPackage: '@typecad/board-rp2350',
86
+ frameworkPackage: '@typecad/framework-arduino',
87
+ framework: 'arduino',
88
+ buildTarget: 'rp2040:rp2040:rpipico2',
89
+ mcu: 'rp2350',
90
+ },
69
91
  ];
70
92
  export function registerKnownTarget(target) {
71
93
  const existing = _knownTargets.findIndex(t => t.id === target.id);
@@ -128,6 +150,24 @@ export function scaffoldProject(options, outDir) {
128
150
  fs.writeFileSync(sketchPath, generateStarterSketch(options), 'utf-8');
129
151
  createdFiles.push(sketchPath);
130
152
  }
153
+ // Embedded projects get a starter hardware test (@typecad/expect / cuttlefish-test).
154
+ // Native projects have no serial/board path, so they get no test setup.
155
+ if (!options.isNative) {
156
+ const testsDir = path.join(resolvedOutDir, 'tests');
157
+ fs.mkdirSync(testsDir, { recursive: true });
158
+ const testPath = path.join(testsDir, '01-basics.test.ts');
159
+ fs.writeFileSync(testPath, generateStarterTest(options), 'utf-8');
160
+ createdFiles.push(testPath);
161
+ // Host-side simulation (@typecad/simulator + vitest). sim/ is kept separate
162
+ // from tests/ so `vitest run sim/` never loads the @typecad/expect no-op
163
+ // stubs, and `cuttlefish-test` (which globs tests/) never tries to flash a
164
+ // simulator file as firmware.
165
+ const simDir = path.join(resolvedOutDir, 'sim');
166
+ fs.mkdirSync(simDir, { recursive: true });
167
+ const simPath = path.join(simDir, 'main.test.ts');
168
+ fs.writeFileSync(simPath, generateStarterSim(options), 'utf-8');
169
+ createdFiles.push(simPath);
170
+ }
131
171
  return { createdFiles, outDir: resolvedOutDir, options };
132
172
  }
133
173
  export function printInitNextSteps(options, outDir) {
@@ -142,9 +182,15 @@ export function printInitNextSteps(options, outDir) {
142
182
  if (!options.isNative) {
143
183
  const portHint = process.platform === 'win32' ? 'COM4' : '/dev/ttyACM0';
144
184
  console.log();
185
+ console.log(chalk.bold.white("To simulate without hardware:"));
186
+ console.log(` ${chalk.cyan("npm run simulate")}`);
187
+ console.log();
145
188
  console.log(chalk.bold.white("To upload to your board:"));
146
189
  console.log(` ${chalk.cyan("npm run upload")}`);
147
190
  console.log();
148
- console.log(chalk.dim(`Edit ${chalk.white("package.json")} to change the serial port from ${chalk.white(portHint)} to your port.`));
191
+ console.log(chalk.bold.white("To run hardware tests:"));
192
+ console.log(` ${chalk.cyan("npm run test:hw")}`);
193
+ console.log();
194
+ console.log(chalk.dim(`Edit ${chalk.white("cuttlefish.config.ts")} to change the serial port from ${chalk.white(portHint)} to your port.`));
149
195
  }
150
196
  }
@@ -19,6 +19,8 @@ export declare function generateProjectTsconfig(options: InitProjectOptions): st
19
19
  export declare function generateProjectConfig(options: InitProjectOptions): string;
20
20
  export declare function generateProjectEnvDts(options: InitProjectOptions): string;
21
21
  export declare function generateStarterSketch(options: InitProjectOptions): string;
22
+ export declare function generateStarterTest(_options: InitProjectOptions): string;
23
+ export declare function generateStarterSim(options: InitProjectOptions): string;
22
24
  export declare function generateBoardForwardingFile(boardPackage: string): string;
23
25
  export declare function generateGitignore(_options: InitProjectOptions): string;
24
26
  export declare function generateEslintConfig(_options: InitProjectOptions): string;
@@ -2,11 +2,11 @@ import { LINT_RULES } from '../ir/feature-registry.js';
2
2
  export function generateProjectPackageJson(options) {
3
3
  const { projectName, frameworkPackage, boardPackage } = options;
4
4
  const deps = {
5
- "@typecad/cuttlefish": "^0.1.0-alpha.1",
6
- [frameworkPackage]: "^0.1.0-alpha.1",
5
+ "@typecad/cuttlefish": "^1.0.0-alpha.3",
6
+ [frameworkPackage]: "^1.0.0-alpha.3",
7
7
  };
8
8
  if (boardPackage) {
9
- deps[boardPackage] = "^0.1.0-alpha.1";
9
+ deps[boardPackage] = "^1.0.0-alpha.3";
10
10
  }
11
11
  const depsJson = Object.entries(deps)
12
12
  .map(([k, v]) => ` "${k}": "${v}"`)
@@ -15,12 +15,27 @@ export function generateProjectPackageJson(options) {
15
15
  // eslint-transpiler-rules plugin (which is plain JS, no dep). Without these
16
16
  // devDependencies `npm run lint` fails to resolve the parser/plugin in a
17
17
  // freshly created project. Versions mirror the repo demo's package.json.
18
- const devDepsJson = [
18
+ const baseDevDeps = [
19
19
  ' "eslint": "^10.4.1"',
20
20
  ' "@typescript-eslint/parser": "^8.61.0"',
21
21
  ' "@typescript-eslint/eslint-plugin": "^8.61.0"',
22
- ].join(',\n');
22
+ ];
23
+ // Developer-utility scripts shared by every target (native + embedded).
24
+ // None require hardware or extra dependencies:
25
+ // dev — auto-retranspile on save (transpile-only; append --compile
26
+ // to also compile on each change). The fast "does it
27
+ // typecheck" feedback loop.
28
+ // gen-decls — generate TypeScript .d.ts from C++ headers. File-argument:
29
+ // `npm run gen-decls -- lib/foo.h` or `-- --all lib/`.
30
+ // gen-libdefs — generate library-definition stubs from a TS file's imports.
31
+ // File-argument: `npm run gen-libdefs -- src/main.ts`.
32
+ const devScripts = [
33
+ '"dev": "cuttlefish build --watch"',
34
+ '"gen-decls": "cuttlefish gen-decls"',
35
+ '"gen-libdefs": "cuttlefish gen-libdefs"',
36
+ ];
23
37
  if (options.isNative) {
38
+ const devDepsJson = baseDevDeps.join(',\n');
24
39
  return `{
25
40
  "name": "${projectName}",
26
41
  "version": "1.0.0",
@@ -28,7 +43,8 @@ export function generateProjectPackageJson(options) {
28
43
  "scripts": {
29
44
  "build": "cuttlefish build",
30
45
  "compile": "cuttlefish build --compile",
31
- "lint": "eslint --config .cuttlefish/eslint.config.mjs src/"
46
+ "lint": "eslint --config .cuttlefish/eslint.config.mjs src/",
47
+ ${devScripts.join(',\n ')}
32
48
  },
33
49
  "dependencies": {
34
50
  ${depsJson}
@@ -39,6 +55,19 @@ ${devDepsJson}
39
55
  }
40
56
  `;
41
57
  }
58
+ // Embedded projects get two host-side testing tiers:
59
+ // - @typecad/expect: hardware tests run on the board via cuttlefish-test
60
+ // (`npm run test:hw`), scoped to tests/**/*.test.ts.
61
+ // - @typecad/simulator + vitest: simulate the board in Node (`npm run
62
+ // simulate`), scoped to sim/**/*.test.ts so vitest never collides with the
63
+ // @typecad/expect no-op stubs under tests/.
64
+ // Versions mirror the workspace's published releases / root devDeps.
65
+ const devDepsJson = [
66
+ ...baseDevDeps,
67
+ ' "@typecad/expect": "^1.0.0-alpha.3"',
68
+ ' "@typecad/simulator": "^1.0.0-alpha.3"',
69
+ ' "vitest": "^4.0.18"',
70
+ ].join(',\n');
42
71
  return `{
43
72
  "name": "${projectName}",
44
73
  "version": "1.0.0",
@@ -48,7 +77,10 @@ ${devDepsJson}
48
77
  "compile": "cuttlefish build --compile",
49
78
  "upload": "cuttlefish build --compile --upload",
50
79
  "monitor": "cuttlefish build --compile --upload --monitor",
51
- "lint": "eslint --config .cuttlefish/eslint.config.mjs src/"
80
+ "test:hw": "npm exec -- cuttlefish-test",
81
+ "simulate": "vitest run sim/",
82
+ "lint": "eslint --config .cuttlefish/eslint.config.mjs src/",
83
+ ${devScripts.join(',\n ')}
52
84
  },
53
85
  "dependencies": {
54
86
  ${depsJson}
@@ -85,9 +117,10 @@ export function generateProjectTsconfig(options) {
85
117
  "noEmit": true,
86
118
  "resolveJsonModule": true,
87
119
  "allowArbitraryExtensions": true,
120
+ "allowImportingTsExtensions": true,
88
121
  "rootDirs": ["src", "types"]${paths}
89
122
  },
90
- "include": ["src/**/*.ts", "types/**/*.ts", "cuttlefish.config.ts"${options.boardPackage ? ', ".cuttlefish/cuttlefish-env.d.ts"' : ''}]
123
+ "include": ["src/**/*.ts", "types/**/*.ts", "cuttlefish.config.ts"${options.boardPackage ? ', ".cuttlefish/cuttlefish-env.d.ts"' : ''}${options.isNative ? '' : ', "sim/**/*.ts"'}]
91
124
  }
92
125
  `;
93
126
  }
@@ -127,6 +160,11 @@ export default config;
127
160
  : '';
128
161
  const portHint = process.platform === 'win32' ? 'COM4' : '/dev/ttyACM0';
129
162
  const baudLine = options.baudRate ? `\n\n // Console polyfill configuration\n console: {\n baudRate: ${options.baudRate},\n // Serial port for upload/monitor. Override with --port on the CLI.\n port: '${portHint}',\n },` : '';
163
+ // Hardware test runner configuration — used by \`npm run test:hw\` (cuttlefish-test,
164
+ // provided by @typecad/expect). It transpiles each tests/**/*.test.ts file,
165
+ // flashes it to the board, and evaluates the assertions over serial.
166
+ const resolvedBaud = options.baudRate ?? 115200;
167
+ const testLine = `\n\n // Hardware test runner (@typecad/expect / \`npm run test:hw\`)\n test: {\n // Serial port for the test board. Override with --port on the CLI or the\n // CUTTLEFISH_PORT env var (e.g. CUTTLEFISH_PORT=/dev/ttyUSB0 npm run test:hw).\n port: '${portHint}',\n baudRate: ${resolvedBaud},\n timeout: 30000,\n include: ['tests/**/*.test.ts'],\n },`;
130
168
  return `// ---------------------------------------------------------------------------
131
169
  // cuttlefish.config.ts — Project configuration
132
170
  //
@@ -155,7 +193,7 @@ const config: CuttlefishConfig = {
155
193
  // Toolchain configuration
156
194
  toolchain: {
157
195
  type: '${resolvedToolchain}',
158
- },${baudLine}
196
+ },${baudLine}${testLine}
159
197
  };
160
198
 
161
199
  export default config;
@@ -286,6 +324,148 @@ while (true) {
286
324
  }
287
325
  `;
288
326
  }
327
+ export function generateStarterTest(_options) {
328
+ return `// ---------------------------------------------------------------------------
329
+ // Hardware test — Basics
330
+ //
331
+ // Runs on the board via \`npm run test:hw\` (cuttlefish-test). Each test file is
332
+ // transpiled, flashed to the board, and its assertions are evaluated on the host
333
+ // over serial. Change the serial port in cuttlefish.config.ts (the \`test.port\`
334
+ // field) or override it with the CUTTLEFISH_PORT env var.
335
+ //
336
+ // API: describe(...).it(...).expect(value).<matcher>() chains. Import pin
337
+ // objects from '@typecad/board' to assert on real hardware I/O. Every file ends
338
+ // with done().
339
+ // ---------------------------------------------------------------------------
340
+
341
+ import { describe, done } from '@typecad/expect';
342
+
343
+ describe("Basics")
344
+ .it("adds two numbers")
345
+ .expect(
346
+ (() => {
347
+ const a = 1;
348
+ let b = 2;
349
+ return a + b;
350
+ })
351
+ ).toBe(3)
352
+ .it("multiplies two numbers")
353
+ .expect(
354
+ (() => {
355
+ let a = 3;
356
+ let b = 4;
357
+ return a * b;
358
+ })
359
+ ).toBe(12)
360
+ .it("reads an array element")
361
+ .expect(
362
+ (() => {
363
+ const data = new Uint8Array([0xAA, 0x10, 0x20]);
364
+ return data[1];
365
+ })
366
+ ).toBe(0x10)
367
+ .it("clamps a value to a range")
368
+ .expect(
369
+ (() => {
370
+ const value = 2000;
371
+ return Math.max(0, Math.min(1023, value));
372
+ })
373
+ ).toBe(1023);
374
+
375
+ done();
376
+ `;
377
+ }
378
+ export function generateStarterSim(options) {
379
+ const boardType = options.targetId;
380
+ return `// ---------------------------------------------------------------------------
381
+ // Hardware simulation — Button + LED
382
+ //
383
+ // Runs entirely on your computer with \`npm run simulate\` (vitest + the
384
+ // @typecad/simulator package). No board, serial port, or arduino-cli required.
385
+ // The simulator mirrors the pins/peripherals of your ${options.targetDisplayName}
386
+ // (${boardType}); you inject fake inputs and assert on the outputs in Node.
387
+ //
388
+ // This is the fast tier — iterate on logic here, then confirm on real hardware
389
+ // with \`npm run test:hw\` (which flashes tests/ to the board).
390
+ // ---------------------------------------------------------------------------
391
+
392
+ import { describe, it, expect, beforeEach } from "vitest";
393
+ import {
394
+ createSimBoard,
395
+ type SimBoard,
396
+ type SimDigitalPin,
397
+ } from "@typecad/simulator";
398
+
399
+ // ===========================================================================
400
+ // FIRMWARE LOGIC
401
+ // ---------------------------------------------------------------------------
402
+ // Factor your firmware into a function that takes the simulated pins as
403
+ // arguments. In a real project this same logic runs on the board against real
404
+ // pins — here it runs against the sim board so you can test it without hardware.
405
+ // ===========================================================================
406
+
407
+ /**
408
+ * Reads a button and reflects its state on an LED.
409
+ *
410
+ * To use your own logic: replace the body of this function with whatever your
411
+ * firmware does (read a sensor, drive a motor, print to serial, ...). As long
412
+ * as it only touches pins you pass in, the simulator can exercise it.
413
+ */
414
+ function reflectButtonOnLed(button: SimDigitalPin, led: SimDigitalPin): void {
415
+ // The button pin is pulled HIGH (1) at rest and reads LOW (0) when pressed.
416
+ if (button.isLow()) {
417
+ led.high();
418
+ } else {
419
+ led.low();
420
+ }
421
+ }
422
+
423
+ // ===========================================================================
424
+ // TEST BENCH
425
+ // ---------------------------------------------------------------------------
426
+ // \`createSimBoard\` builds an in-memory version of your board. The pin numbers
427
+ // below match the physical pinout. Add the pins/peripherals your firmware uses:
428
+ // board.digital(n), board.analog(n), board.pwm(n), board.serial(n),
429
+ // board.i2c(n), board.spi(n), board.interrupt(n).
430
+ // ===========================================================================
431
+
432
+ function setupSim(): { board: SimBoard; button: SimDigitalPin; led: SimDigitalPin } {
433
+ // boardType mirrors the target chosen with \`cuttlefish create\`.
434
+ const board = createSimBoard({ boardType: "${boardType}" });
435
+
436
+ const button = board.digital(2).asInputPullUp(); // button on pin 2 (INPUT_PULLUP)
437
+ const led = board.digital(13).asOutput(false); // LED on pin 13
438
+
439
+ return { board, button, led };
440
+ }
441
+
442
+ describe("Button + LED (simulator)", () => {
443
+ beforeEach(() => {
444
+ // A fresh board per test keeps state isolated. For a long-running sim you
445
+ // can call board.reset() between cycles instead.
446
+ });
447
+
448
+ it("keeps the LED off while the button is released", () => {
449
+ const { button, led } = setupSim();
450
+
451
+ // Button at rest: INPUT_PULLUP reads HIGH.
452
+ reflectButtonOnLed(button, led);
453
+
454
+ expect(led.getBitValue()).toBe(0);
455
+ });
456
+
457
+ it("turns the LED on while the button is pressed", () => {
458
+ const { button, led } = setupSim();
459
+
460
+ // Simulate a press: drive the button pin LOW.
461
+ button.injectValue(0);
462
+ reflectButtonOnLed(button, led);
463
+
464
+ expect(led.getBitValue()).toBe(1);
465
+ });
466
+ });
467
+ `;
468
+ }
289
469
  export function generateBoardForwardingFile(boardPackage) {
290
470
  return `// ---------------------------------------------------------------------------
291
471
  // .cuttlefish/board.ts — Dynamically generated forwarding board package