@typecad/cuttlefish 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (167) hide show
  1. package/dist/api/config.d.ts +32 -0
  2. package/dist/api/index.d.ts +1 -1
  3. package/dist/api/schema/types.d.ts +15 -0
  4. package/dist/api/shared/async-runtime-static.d.ts +12 -1
  5. package/dist/api/shared/async-runtime-static.js +69 -24
  6. package/dist/api/shared/async-symbol-detector.d.ts +11 -0
  7. package/dist/api/shared/async-symbol-detector.js +140 -0
  8. package/dist/api/shared/async-types.d.ts +24 -0
  9. package/dist/api/shared/coop-scheduler.d.ts +60 -0
  10. package/dist/api/shared/coop-scheduler.js +149 -0
  11. package/dist/api/shared/display-adapter.d.ts +2 -2
  12. package/dist/api/shared/display-adapter.js +9 -119
  13. package/dist/api/shared/display-adapters/sdl.js +29 -29
  14. package/dist/api/shared/display-profile.d.ts +38 -33
  15. package/dist/api/shared/display-profile.js +22 -170
  16. package/dist/api/shared/framework-manifest-registry.d.ts +1 -1
  17. package/dist/api/shared/framework-manifest-registry.js +8 -6
  18. package/dist/api/shared/framework-manifest.d.ts +147 -28
  19. package/dist/api/shared/framework-manifest.js +35 -2
  20. package/dist/api/shared/graphics-strategy.d.ts +15 -6
  21. package/dist/api/shared/hal-op-ir.d.ts +340 -3
  22. package/dist/api/shared/hal-op-ir.js +34 -0
  23. package/dist/api/shared/index.d.ts +9 -1
  24. package/dist/api/shared/index.js +11 -1
  25. package/dist/api/shared/ir-core.d.ts +4 -0
  26. package/dist/api/shared/ir-declarations.d.ts +6 -0
  27. package/dist/api/shared/platform-strategy.d.ts +104 -2
  28. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  29. package/dist/api/shared/promise-runtime.d.ts +1 -1
  30. package/dist/api/shared/promise-runtime.js +17 -13
  31. package/dist/api/shared/toolchain-types.d.ts +17 -0
  32. package/dist/api/shared/validate-framework-manifest.js +77 -0
  33. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  34. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  35. package/dist/api/shared/worker-runtime.d.ts +69 -0
  36. package/dist/api/shared/worker-runtime.js +163 -0
  37. package/dist/cli.js +125 -121
  38. package/dist/config-loader.d.ts +7 -2
  39. package/dist/config-loader.js +20 -19
  40. package/dist/config-schema.d.ts +107 -65
  41. package/dist/config-schema.js +14 -0
  42. package/dist/contract/board-generator.d.ts +17 -0
  43. package/dist/contract/board-generator.js +57 -0
  44. package/dist/contract/contract-parser.d.ts +217 -0
  45. package/dist/contract/contract-parser.js +224 -0
  46. package/dist/contract/index.d.ts +22 -0
  47. package/dist/contract/index.js +68 -0
  48. package/dist/create/board-codegen.js +4 -4
  49. package/dist/create/board-generators.js +4 -5
  50. package/dist/create/board-spec.d.ts +74 -77
  51. package/dist/create/board-spec.js +0 -1
  52. package/dist/create/init-scaffold.d.ts +7 -3
  53. package/dist/create/init-scaffold.js +0 -62
  54. package/dist/create/init-templates.js +62 -16
  55. package/dist/create/init-wizard.js +32 -27
  56. package/dist/debug/preprocessor.js +178 -39
  57. package/dist/debug/types.d.ts +23 -0
  58. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  59. package/dist/diagnostics/mermaid-builder.js +34 -24
  60. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  61. package/dist/emit/compliance/arxml-writer.js +34 -0
  62. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  63. package/dist/emit/compliance/compliance-context.js +113 -0
  64. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  65. package/dist/emit/compliance/deviation-ledger.js +47 -0
  66. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  67. package/dist/emit/compliance/deviation-writer.js +37 -0
  68. package/dist/emit/compliance/index.d.ts +7 -0
  69. package/dist/emit/compliance/index.js +6 -0
  70. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  71. package/dist/emit/compliance/rule-engine.js +101 -0
  72. package/dist/emit/compliance/rules.d.ts +16 -0
  73. package/dist/emit/compliance/rules.js +191 -0
  74. package/dist/emit/compliance/types.d.ts +59 -0
  75. package/dist/emit/compliance/types.js +8 -0
  76. package/dist/emit/emitters/class-emitter.js +6 -1
  77. package/dist/emit/emitters/emitter-context.d.ts +23 -1
  78. package/dist/emit/emitters/function-emitter-impl.js +96 -6
  79. package/dist/emit/emitters/line-appender.js +19 -0
  80. package/dist/emit/emitters/line-marker.d.ts +38 -0
  81. package/dist/emit/emitters/line-marker.js +39 -0
  82. package/dist/emit/emitters/output-finalizer.js +80 -1
  83. package/dist/emit/emitters/setup.d.ts +17 -0
  84. package/dist/emit/emitters/setup.js +217 -36
  85. package/dist/emit/emitters/top-level-prep.js +6 -0
  86. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  87. package/dist/emit/emitters/ui-emitter.js +17 -8
  88. package/dist/emit/expression-renderer.d.ts +1 -1
  89. package/dist/emit/expression-renderer.js +81 -5
  90. package/dist/emit/route-hal-op.js +18 -5
  91. package/dist/emit/snprintf-helpers.js +7 -4
  92. package/dist/emit/statement-renderer.d.ts +10 -0
  93. package/dist/emit/statement-renderer.js +21 -5
  94. package/dist/emit/utils/async-state-machine.js +39 -12
  95. package/dist/framework-package.js +2 -0
  96. package/dist/framework-registry.d.ts +17 -0
  97. package/dist/install/framework-catalog.d.ts +53 -0
  98. package/dist/install/framework-catalog.js +107 -0
  99. package/dist/install/handle-install.d.ts +35 -0
  100. package/dist/install/handle-install.js +177 -0
  101. package/dist/install/index.d.ts +4 -0
  102. package/dist/install/index.js +3 -0
  103. package/dist/ir/adc-range-validation.js +14 -1
  104. package/dist/ir/build-ir-state.d.ts +1 -0
  105. package/dist/ir/build-ir-state.js +14 -0
  106. package/dist/ir/build-ir.js +4 -4
  107. package/dist/ir/call-graph.js +16 -0
  108. package/dist/ir/expression-to-ir.js +150 -0
  109. package/dist/ir/function-builder.js +22 -0
  110. package/dist/ir/hal/hal-emitter.js +10 -6
  111. package/dist/ir/hal/hal-parser.js +16 -9
  112. package/dist/ir/hal/hal-plugins.js +164 -3
  113. package/dist/ir/identifier-collector.js +21 -4
  114. package/dist/ir/interrupt-analysis.d.ts +5 -1
  115. package/dist/ir/interrupt-analysis.js +8 -16
  116. package/dist/ir/ownership-analysis.js +6 -0
  117. package/dist/ir/pin-capability-validation.js +37 -0
  118. package/dist/ir/pin-mode-validation.d.ts +2 -2
  119. package/dist/ir/pin-mode-validation.js +49 -18
  120. package/dist/ir/program-analysis.d.ts +42 -0
  121. package/dist/ir/program-analysis.js +243 -6
  122. package/dist/ir/render-expr.js +11 -0
  123. package/dist/ir/timing-validation.js +11 -2
  124. package/dist/ir/transformers/call-statement.js +105 -0
  125. package/dist/ir/transformers/expressions.js +4 -0
  126. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  127. package/dist/ir/transformers/namespace-methods.js +17 -12
  128. package/dist/ir/transformers/ui-reactive.js +2 -2
  129. package/dist/ir/transformers/variables.js +51 -0
  130. package/dist/ir/type-resolution.js +18 -0
  131. package/dist/ir/ui-element-auto-wire.js +7 -5
  132. package/dist/ir/utils/map-statements.d.ts +4 -0
  133. package/dist/ir/utils/map-statements.js +79 -0
  134. package/dist/ir/validation-orchestrator.js +6 -1
  135. package/dist/ir/worker-analysis.d.ts +10 -0
  136. package/dist/ir/worker-analysis.js +261 -0
  137. package/dist/orchestrator/graph-builder.js +8 -7
  138. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  139. package/dist/platform/coop-scheduler-runtime.js +41 -0
  140. package/dist/platform/generic-strategy.d.ts +15 -3
  141. package/dist/platform/generic-strategy.js +49 -4
  142. package/dist/safety/safety-bridge.d.ts +11 -0
  143. package/dist/safety/safety-bridge.js +48 -0
  144. package/dist/safety/sidecar-bridge.d.ts +5 -0
  145. package/dist/safety/sidecar-bridge.js +22 -0
  146. package/dist/safety-hook.d.ts +79 -0
  147. package/dist/safety-hook.js +35 -0
  148. package/dist/stores/display-profile-store.d.ts +0 -1
  149. package/dist/stores/display-profile-store.js +0 -1
  150. package/dist/testing.d.ts +4 -0
  151. package/dist/testing.js +5 -0
  152. package/dist/transpile.js +124 -35
  153. package/dist/types.d.ts +22 -2
  154. package/dist/ui-hook.d.ts +0 -2
  155. package/dist/utils/cli.d.ts +2 -2
  156. package/dist/utils/cli.js +68 -3
  157. package/package.json +20 -5
  158. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  159. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  160. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  161. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  162. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  163. package/dist/api/shared/display-adapters/st7796.js +0 -132
  164. package/dist/libdef/idf-discovery.d.ts +0 -7
  165. package/dist/libdef/idf-discovery.js +0 -59
  166. package/dist/licenses.d.ts +0 -185
  167. package/dist/licenses.js +0 -963
@@ -0,0 +1,163 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared worker-offload runtime C++ code generator
3
+ //
4
+ // Generates the generic worker pool + the __tc_worker_submit / __tc_worker_done
5
+ // contract that generalizes the one-off ESP32 HTTP offload pattern
6
+ // (framework-esp32/src/lowering/http.ts: the __tc_http_send_task /
7
+ // __tc_http_send_start / __tc_http_done trio). The pool and contract live here;
8
+ // the per-framework spawn/signal/poll PRIMITIVES are injected via the strategy
9
+ // hooks workerSpawnLines / workerSignalDoneExpr / workerIsDoneExpr.
10
+ //
11
+ // Design (the safety property, per the approved plan):
12
+ // - The worker runtime is request-in / poll-out: the worker function receives
13
+ // its inputs by value and exposes outputs only through the opaque handle.
14
+ // - No mutex/semaphore is added to the IR. The worker-isolation analyzer
15
+ // (worker-analysis.ts) makes shared mutable state a transpile-time error.
16
+ // - STL-free: fixed-size static slot table, function-pointer + void*. Links
17
+ // on minimal-libc targets (Zephyr).
18
+ //
19
+ // The memory-barrier contract is load-bearing on dual-core targets: the signal
20
+ // primitive (workerSignalDoneExpr) MUST issue a full barrier so the consumer
21
+ // observes the worker's preceding output writes once it observes completion.
22
+ // ESP32 xTaskNotifyGive and Zephyr k_sem_give both satisfy this; bare volatile
23
+ // does NOT across cores.
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Generate the worker-offload runtime C++ for a framework that supplies a
27
+ * backing. Returns null if `backing` is null (unsupported on this framework).
28
+ *
29
+ * The emitted code defines, for each slot i in [0, poolSize):
30
+ * - a worker trampoline __tc_worker_fn_i(void* arg) that runs the recorded
31
+ * fn and then signals completion via signalDoneExpr,
32
+ * - __tc_worker_submit(i, fn, arg) — records the waiter, clears done, spawns,
33
+ * - __tc_worker_done(i) — poll predicate.
34
+ *
35
+ * AUTOSAR C++14: fixed-width types, static_cast, final structs, no STL, no heap.
36
+ */
37
+ export function generateWorkerRuntime(backing, options = {}) {
38
+ if (!backing)
39
+ return null;
40
+ const poolSize = options.poolSize ?? 4;
41
+ const stackSize = options.stackSize ?? 8192;
42
+ const lines = [];
43
+ lines.push(`// ── Worker offload runtime (generalized request-in / poll-out) ──────────────`);
44
+ lines.push(`// Slot table + __tc_worker_submit/done contract. Per-framework primitives`);
45
+ lines.push(`// (spawn/signal/poll) are injected by the framework strategy. STL-free so`);
46
+ lines.push(`// this links on minimal-libc targets. See worker-runtime.ts.`);
47
+ lines.push(`namespace typecad_worker {`);
48
+ lines.push(` using WorkerFn = void (*)(void*);`);
49
+ lines.push(``);
50
+ lines.push(` struct WorkerSlot final {`);
51
+ lines.push(` volatile bool done;`);
52
+ lines.push(` void* waiter; // framework-specific waiter handle (opaque)`);
53
+ lines.push(` WorkerFn fn;`);
54
+ lines.push(` void* arg;`);
55
+ lines.push(` };`);
56
+ lines.push(``);
57
+ // Per-slot static instances.
58
+ for (let i = 0; i < poolSize; i++) {
59
+ lines.push(` static WorkerSlot __slot_${i};`);
60
+ }
61
+ lines.push(``);
62
+ // Forward-declare the per-slot trampolines so backings that need to bind a
63
+ // named handler to one (e.g. Zephyr K_WORK_DEFINE(_work_i, handler_i)) can
64
+ // reference it before the trampoline definition appears below.
65
+ for (let i = 0; i < poolSize; i++) {
66
+ lines.push(` static void __tc_worker_fn_${i}(void* arg);`);
67
+ }
68
+ lines.push(``);
69
+ // Per-slot backing declarations (e.g. Zephyr K_SEM_DEFINE + K_WORK_DEFINE +
70
+ // a work handler that forwards to the trampoline). Emitted BEFORE the
71
+ // trampoline definitions so the trampoline bodies can reference per-slot
72
+ // objects declared here (e.g. the completion semaphore used by the signal
73
+ // expression). Backings must only forward-declare, not define, the
74
+ // trampoline itself.
75
+ if (backing.slotDeclarations) {
76
+ for (let i = 0; i < poolSize; i++) {
77
+ const trampolineName = `__tc_worker_fn_${i}`;
78
+ for (const d of backing.slotDeclarations(i, trampolineName)) {
79
+ lines.push(` ${d}`);
80
+ }
81
+ }
82
+ lines.push(``);
83
+ }
84
+ // Per-slot worker trampolines: run fn(arg), then signal completion.
85
+ for (let i = 0; i < poolSize; i++) {
86
+ const signal = backing.signalDoneExpr(i);
87
+ lines.push(` // Worker trampoline for slot ${i}.`);
88
+ lines.push(` static void __tc_worker_fn_${i}(void* arg) {`);
89
+ lines.push(` // The shared runtime recorded fn/waiter before spawn; run it.`);
90
+ lines.push(` if (__slot_${i}.fn != nullptr) { __slot_${i}.fn(arg); }`);
91
+ lines.push(` __slot_${i}.done = true;`);
92
+ lines.push(` ${signal}; // barrier: makes the fn's output writes visible`);
93
+ lines.push(` }`);
94
+ lines.push(``);
95
+ }
96
+ // submit / done per slot.
97
+ for (let i = 0; i < poolSize; i++) {
98
+ const trampolineName = `__tc_worker_fn_${i}`;
99
+ const spawn = backing.spawnLines(i, trampolineName, `__slot_${i}.waiter`);
100
+ lines.push(` static inline void __tc_worker_submit_${i}(WorkerFn fn, void* arg) {`);
101
+ lines.push(` __slot_${i}.done = false;`);
102
+ lines.push(` __slot_${i}.fn = fn;`);
103
+ lines.push(` __slot_${i}.arg = arg;`);
104
+ lines.push(` // waiter is set by the framework spawn primitive below.`);
105
+ for (const s of spawn)
106
+ lines.push(` ${s}`);
107
+ lines.push(` }`);
108
+ lines.push(``);
109
+ const poll = backing.isDoneExpr(i);
110
+ lines.push(` static inline bool __tc_worker_done_${i}(void) {`);
111
+ lines.push(` if (__slot_${i}.done) { return true; } // fast path`);
112
+ lines.push(` if (${poll}) { __slot_${i}.done = true; return true; }`);
113
+ lines.push(` return false;`);
114
+ lines.push(` }`);
115
+ lines.push(``);
116
+ }
117
+ lines.push(`} // namespace typecad_worker`);
118
+ lines.push(``);
119
+ // Global dispatch aliases so HAL emit (worker.submit / worker.done) resolves.
120
+ lines.push(`// Global dispatch: select the per-slot submit/done by handle id.`);
121
+ lines.push(`inline void __tc_worker_submit(int32_t handle, typecad_worker::WorkerFn fn, void* arg) {`);
122
+ lines.push(` switch (handle) {`);
123
+ for (let i = 0; i < poolSize; i++) {
124
+ lines.push(` case ${i}: typecad_worker::__tc_worker_submit_${i}(fn, arg); return;`);
125
+ }
126
+ lines.push(` default: break; // out-of-range handle: no-op`);
127
+ lines.push(` }`);
128
+ lines.push(`}`);
129
+ lines.push(`inline bool __tc_worker_done(int32_t handle) {`);
130
+ lines.push(` switch (handle) {`);
131
+ for (let i = 0; i < poolSize; i++) {
132
+ lines.push(` case ${i}: return typecad_worker::__tc_worker_done_${i}();`);
133
+ }
134
+ lines.push(` default: return true; // out-of-range handle: treat as already done`);
135
+ lines.push(` }`);
136
+ lines.push(`}`);
137
+ return lines.join("\n");
138
+ }
139
+ /**
140
+ * Resolve a HAL worker.* op to C++ against the shared contract.
141
+ * Returns undefined for ops the caller did not lower (unsupported).
142
+ *
143
+ * Note: the worker function itself (fnRef) and its argument (argRef) come from
144
+ * the IR as flat-scalar strings (per the hal-op-ir flat-fields contract). The
145
+ * lowering emits a straight call into __tc_worker_submit / __tc_worker_done.
146
+ */
147
+ export function lowerWorkerOp(op) {
148
+ const o = op;
149
+ switch (op.operation) {
150
+ case 'worker.submit': {
151
+ const handle = Number(o.handleId);
152
+ const fn = String(o.fnRef);
153
+ const arg = o.argRef != null ? String(o.argRef) : 'nullptr';
154
+ return { code: `__tc_worker_submit(static_cast<int32_t>(${handle}), ${fn}, ${arg});` };
155
+ }
156
+ case 'worker.done': {
157
+ const handle = Number(o.handleId);
158
+ return { expression: `__tc_worker_done(static_cast<int32_t>(${handle}))` };
159
+ }
160
+ default:
161
+ return undefined;
162
+ }
163
+ }
package/dist/cli.js CHANGED
@@ -4,24 +4,24 @@ import fs from "node:fs";
4
4
  import { parseCommandLine, printHelp } from "./utils/cli.js";
5
5
  import { scaffoldProject, printInitNextSteps, KNOWN_TARGETS } from "./create/index.js";
6
6
  import { runInitWizard } from "./create/index.js";
7
+ import { handleInstall } from "./install/index.js";
7
8
  import { generateLibraryDefinitions, transpileFile } from "./transpile.js";
8
9
  import { generateDecl, generateDeclsForDirectory, generateComponentDeclsForProject } from "./libdef/cpp-to-decl.js";
9
- import { discoverIdfRootForGenDecls } from "./libdef/idf-discovery.js";
10
10
  import { mapCppLocationToTs, readSourceMap, resolveMapPath } from "./mapping/source-map.js";
11
11
  import { compileSource, uploadFirmware, monitorDevice } from "./platform/toolchain.js";
12
12
  import { resolveStrategy } from "./platform/registry.js";
13
13
  import { loadFrameworkPackage } from "./framework-package.js";
14
14
  import { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
15
15
  import { loadCuttlefishConfig, generateVirtualTypeDeclaration } from "./config-loader.js";
16
+ import { generateContractBoard } from "./contract/index.js";
16
17
  import { requireUIHook, hasUIHook } from "./ui-hook.js";
17
18
  import { loadUIEngine } from "./ui/ui-bridge.js";
19
+ import { loadSafetyEngine } from "./safety/safety-bridge.js";
18
20
  import { runWatch, discoverWatchDirs } from "./watch.js";
19
21
  import { runExpectTests, assertTypeScriptInput, printDiagnostics, printMappedCompileErrors } from "./cli-utils.js";
20
22
  import { runPreviewServer } from "./preview/server.js";
21
23
  import * as ui from "./utils/ui.js";
22
24
  import chalk from "chalk";
23
- import { checkArduinoEnv } from "@typecad/arduino-cli";
24
- import { runLicensesPresenter } from "./licenses.js";
25
25
  function hasFatalDiagnostics(result) {
26
26
  return result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
27
27
  }
@@ -34,6 +34,27 @@ function displayConfigForTranspile(config) {
34
34
  }
35
35
  return display;
36
36
  }
37
+ /**
38
+ * Apply the PSRAM config to the Arduino build: append the `PSRAM={opi|quad}`
39
+ * menu option to the FQBN (so the Arduino core's psramFound()/ps_malloc work
40
+ * at runtime — they're gated on the board menu option, not just the define)
41
+ * and add `-DBOARD_HAS_PSRAM` to the defines (so the framework's PSRAM canvas
42
+ * allocator is compiled in). No-op when psram is unset or the target isn't an
43
+ * Arduino FQBN (the PSRAM= option is Arduino-core-specific).
44
+ */
45
+ function applyPsramToArduinoBuild(buildTarget, defines, psram) {
46
+ if (!psram || !buildTarget || !buildTarget.includes(':')) {
47
+ return { buildTarget, defines: defines ?? {} };
48
+ }
49
+ // Arduino FQBN config options follow the board:option as `key=value` pairs.
50
+ // Append PSRAM= if not already present (don't clobber an explicit override).
51
+ const psramOpt = `PSRAM=${psram}`;
52
+ const adjustedTarget = buildTarget.includes('PSRAM=') ? buildTarget : `${buildTarget}:${psramOpt}`;
53
+ const adjustedDefines = { ...(defines ?? {}) };
54
+ if (!('BOARD_HAS_PSRAM' in adjustedDefines))
55
+ adjustedDefines.BOARD_HAS_PSRAM = '';
56
+ return { buildTarget: adjustedTarget, defines: adjustedDefines };
57
+ }
37
58
  async function handleCreate(options) {
38
59
  const targetId = options.target ?? options.board;
39
60
  const hasTarget = !!targetId;
@@ -44,20 +65,17 @@ async function handleCreate(options) {
44
65
  throw new Error(`Unknown target '${targetId}'. Available targets:\n${available}`);
45
66
  }
46
67
  const framework = options.framework ?? target.framework;
47
- const frameworkPackage = target.isNative
48
- ? target.frameworkPackage
49
- : framework === 'avr'
50
- ? '@typecad/framework-avr'
51
- : framework === 'arduino'
52
- ? '@typecad/framework-arduino'
53
- : framework === 'esp32'
54
- ? '@typecad/framework-esp32'
55
- : `@typecad/framework-${framework}`;
68
+ const frameworkPackage = target.frameworkPackage
69
+ ?? (framework ? `@typecad/framework-${framework}` : undefined);
70
+ if (!frameworkPackage) {
71
+ throw new Error(`No framework package resolved for target '${target.id}'. ` +
72
+ `Pass --framework <name> or install a @typecad/framework-* package.`);
73
+ }
74
+ // Derive the short framework id from the package name (e.g.
75
+ // @typecad/framework-arduino → arduino) when neither the option nor the
76
+ // target supplied one. KNOWN_TARGETS embedded boards no longer hardcode it.
77
+ const frameworkId = framework ?? frameworkPackage.replace(/^@typecad\/framework-/, '');
56
78
  const projectName = options.projectName || 'my-project';
57
- const isEspIdf = frameworkPackage === '@typecad/framework-esp32' || framework === 'esp32';
58
- const idfTarget = target.frameworkData?.target
59
- ?? target.architecture
60
- ?? 'esp32';
61
79
  const result = scaffoldProject({
62
80
  projectName,
63
81
  targetId: target.id,
@@ -66,19 +84,14 @@ async function handleCreate(options) {
66
84
  architecture: target.architecture,
67
85
  boardPackage: target.boardPackage,
68
86
  frameworkPackage,
69
- framework,
70
- buildTarget: isEspIdf ? idfTarget : target.buildTarget,
87
+ framework: frameworkId,
88
+ buildTarget: target.buildTarget,
71
89
  mcu: target.mcu,
72
90
  baudRate: target.isNative ? undefined : (options.baud ?? 9600),
73
91
  includeSketch: !options.noSketch,
74
- ...(isEspIdf
75
- ? {
76
- toolchainType: 'idf',
77
- frameworkData: target.frameworkData ?? { target: idfTarget, buildTarget: idfTarget },
78
- }
79
- : target.frameworkData
80
- ? { frameworkData: target.frameworkData }
81
- : {}),
92
+ ...(target.frameworkData
93
+ ? { frameworkData: target.frameworkData }
94
+ : {}),
82
95
  }, options.outDir);
83
96
  console.log(`\n${chalk.green("✓")} Created project files:`);
84
97
  for (const file of result.createdFiles) {
@@ -124,62 +137,6 @@ async function handleBoardAdd(options) {
124
137
  }
125
138
  console.log(generateFrameworkChecklist(spec));
126
139
  }
127
- /**
128
- * `cuttlefish doctor` — verify arduino-cli is installed and the board's core
129
- * (derived from the FQBN in cuttlefish.config.ts) is present. Exits 0 if the
130
- * environment is OK, non-zero otherwise. Reuses checkArduinoEnv so the
131
- * detection logic is shared with the build/test gates.
132
- */
133
- function runDoctor() {
134
- ui.printHeader();
135
- ui.printStep("Checking arduino-cli environment...");
136
- const config = loadCuttlefishConfig(process.cwd());
137
- const fqbn = config?.buildTarget;
138
- const result = checkArduinoEnv(fqbn);
139
- const check = result.check;
140
- // arduino-cli presence line
141
- if (check.arduinoCliInstalled) {
142
- ui.printInfo(`arduino-cli .... ${check.arduinoCliVersion ?? "unknown"} ✓`);
143
- }
144
- else if (!result.ok && result.reason === "arduino-cli-not-found") {
145
- ui.printError(`arduino-cli .... NOT FOUND on PATH`);
146
- }
147
- else {
148
- ui.printError(`arduino-cli .... found but unresponsive`);
149
- }
150
- // core presence line (only meaningful if we have an FQBN)
151
- if (fqbn) {
152
- if (check.requiredCore) {
153
- const status = check.requiredCoreInstalled ? "installed ✓" : "NOT installed ✗";
154
- const line = `${check.requiredCore} ....... ${status}`;
155
- if (check.requiredCoreInstalled) {
156
- ui.printInfo(line);
157
- }
158
- else {
159
- ui.printError(line);
160
- ui.printInfo(` → run: arduino-cli core install ${check.requiredCore}`);
161
- }
162
- }
163
- }
164
- else {
165
- ui.printInfo("(no buildTarget in cuttlefish.config.ts — skipping core check)");
166
- }
167
- // Exit code
168
- if (result.ok) {
169
- ui.printSuccess("Environment OK");
170
- return; // exitCode stays unset => 0
171
- }
172
- if (!result.ok) {
173
- for (const line of result.messages)
174
- ui.printInfo(line);
175
- process.exitCode = 1;
176
- }
177
- }
178
- /**
179
- * `cuttlefish licenses` dispatch — the presenter lives in licenses.ts so it is
180
- * unit-testable without importing this binary entry module (cli.ts has a
181
- * shebang and runs main() at import time). See runLicensesPresenter.
182
- */
183
140
  async function main() {
184
141
  try {
185
142
  const options = parseCommandLine(process.argv);
@@ -195,6 +152,10 @@ async function main() {
195
152
  await handleBoardAdd(options);
196
153
  return;
197
154
  }
155
+ if (options.command === "install") {
156
+ await handleInstall(options);
157
+ return;
158
+ }
198
159
  if (options.command === "preview") {
199
160
  await runPreviewServer({
200
161
  configPath: options.configPath,
@@ -227,12 +188,36 @@ async function main() {
227
188
  }
228
189
  return;
229
190
  }
230
- if (options.command === "doctor") {
231
- runDoctor();
232
- return;
233
- }
234
- if (options.command === "licenses") {
235
- runLicensesPresenter(options.strict ?? false, options.all ?? false);
191
+ if (options.command === "doctor" || options.command === "licenses") {
192
+ // These commands run standalone (often before a build), so the framework
193
+ // isn't loaded yet. Load it from the config's framework field so the
194
+ // framework-supplied doctor/licenses handlers are available.
195
+ if (!hasLoadedFramework()) {
196
+ const config = loadCuttlefishConfig(process.cwd());
197
+ if (config?.framework) {
198
+ try {
199
+ loadFrameworkPackage(config.framework, process.cwd());
200
+ }
201
+ catch {
202
+ // Framework package not resolvable — fall through to the
203
+ // no-support message below rather than crashing the command.
204
+ }
205
+ }
206
+ }
207
+ const fw = hasLoadedFramework() ? getLoadedFramework() : undefined;
208
+ if (options.command === "doctor") {
209
+ if (!fw?.doctor) {
210
+ ui.printInfo("This framework provides no doctor support.");
211
+ return;
212
+ }
213
+ fw.doctor();
214
+ return;
215
+ }
216
+ if (!fw?.licenses) {
217
+ ui.printInfo("This framework provides no licenses support.");
218
+ return;
219
+ }
220
+ fw.licenses(options.strict ?? false, options.all ?? false);
236
221
  return;
237
222
  }
238
223
  // ── Handle build command — entry point comes from config ──────────
@@ -280,21 +265,11 @@ async function main() {
280
265
  const managedNames = managedSpecs.map((spec) => spec.replace("/", "__"));
281
266
  const localPaths = (componentsNode.local ?? []).map((p) => path.isAbsolute(p) ? p : path.resolve(componentsDir, p));
282
267
  const builtinNames = componentsNode.builtin ?? [];
283
- // Built-in components resolve against an ESP-IDF install. The CLI
284
- // uses minimal discovery (env var + well-known paths). If no install
285
- // is found, builtins are skipped with a notice — the framework-esp32
286
- // compile path performs richer discovery and is the primary entry
287
- // point for builtin gen-decls.
288
- const idfRoot = discoverIdfRootForGenDecls();
289
- if (builtinNames.length > 0 && !idfRoot) {
290
- ui.printInfo('components.builtin declared but no ESP-IDF install was found; skipping builtin headers. ' +
291
- 'Run via `cuttlefish build --compile` (which performs full IDF discovery) or set $IDF_PATH.');
292
- }
293
268
  const created = generateComponentDeclsForProject(componentsDir, {
294
269
  managed: managedNames,
295
270
  local: localPaths,
296
- builtin: idfRoot ? builtinNames : [],
297
- idfRoot,
271
+ builtin: [],
272
+ idfRoot: undefined,
298
273
  });
299
274
  if (created.length === 0) {
300
275
  ui.printInfo("No component declaration files created.");
@@ -377,6 +352,15 @@ async function main() {
377
352
  // ── Load cuttlefish.config.ts (config wins over CLI flags) ──────────
378
353
  const inputDir = path.dirname(path.resolve(options.inputFile));
379
354
  const config = loadCuttlefishConfig(inputDir);
355
+ // ── Contract-based board narrowing ──────────────────────────────────
356
+ // If the config names a TypeCAD contract (*.contract.json from typecad.net),
357
+ // generate the narrowed `.cuttlefish/board.ts` BEFORE env.d.ts is written
358
+ // (it emits `export * from './board.js'` for this case) and before the
359
+ // board package is resolved downstream. The generated board re-exports
360
+ // only the pins/peripherals the PCB actually wires.
361
+ if (config?.contract) {
362
+ await generateContractBoard(config);
363
+ }
380
364
  // ── Pre-transpile: generate component .d.ts stubs ───────────────────
381
365
  // The transpile type-check needs the .d.ts imports in main.ts to resolve.
382
366
  // For ESP-IDF builtin/managed/local components, those .d.ts files are
@@ -398,17 +382,12 @@ async function main() {
398
382
  const outBase = config.outputOutDir
399
383
  ? path.resolve(inputDir, config.outputOutDir)
400
384
  : inputDir;
401
- // For builtins we need IDF_PATH. Use cuttlefish's minimal discovery
402
- // (env var + well-known paths) — full discovery is done by
403
- // framework-esp32 at --compile time. If no install is found,
404
- // builtins are skipped; --compile will regenerate them.
405
- const idfRoot = discoverIdfRootForGenDecls();
406
385
  try {
407
386
  generateComponentDeclsForProject(outBase, {
408
387
  managed: managedSpecs.map((s) => s.replace('/', '__')),
409
388
  local: localPaths,
410
- builtin: idfRoot ? builtinNames : [],
411
- idfRoot,
389
+ builtin: [],
390
+ idfRoot: undefined,
412
391
  });
413
392
  }
414
393
  catch {
@@ -420,6 +399,9 @@ async function main() {
420
399
  }
421
400
  // Load the UI engine (if @typecad/ui is installed) before any UI work.
422
401
  await loadUIEngine();
402
+ // Load the safety engine (if @typecad/safety is installed) before any
403
+ // safety work.
404
+ await loadSafetyEngine();
423
405
  let effectivePlatformContext = options.platformContext;
424
406
  let effectiveTarget = options.target;
425
407
  let effectiveOutDir = options.outDir;
@@ -440,16 +422,15 @@ async function main() {
440
422
  }
441
423
  let configBuildTarget = config.buildTarget;
442
424
  if (configBuildTarget) {
443
- // Reconstruct frameworkData from buildTarget, preserving other fields
444
- // (psram, components, etc.) from the resolved frameworkConfig so the
445
- // transpile-time diagnostics (e.g. scroll-canvas-memory PSRAM budget)
446
- // and the toolchain both see the full frameworkData, not just buildTarget.
447
- const fcPsram = config.frameworkConfig?.psram;
425
+ // Reconstruct frameworkData from buildTarget so the toolchain sees the
426
+ // full frameworkData, not just buildTarget.
448
427
  effectivePlatformContext = {
449
428
  architecture: configBuildTarget.split(":")?.[1]?.toLowerCase(),
450
429
  frameworkData: {
451
430
  buildTarget: configBuildTarget,
452
- ...(fcPsram ? { psram: fcPsram } : {}),
431
+ // Thread the PSRAM type through to the framework so it can emit the
432
+ // PSRAM-enabling Kconfig (Zephyr) / define + FQBN option (Arduino).
433
+ ...(config.psram ? { psram: config.psram } : {}),
453
434
  },
454
435
  };
455
436
  }
@@ -529,6 +510,8 @@ async function main() {
529
510
  diagnostics: options.diagnostics,
530
511
  display: displayConfigForTranspile(config),
531
512
  projectRoot: options.projectRoot,
513
+ autosar: options.autosar,
514
+ autosarArxml: options.autosarArxml,
532
515
  });
533
516
  printDiagnostics(result.diagnostics);
534
517
  ui.printTasks(result.asyncTaskNames ?? [], result.usesTimers ?? false);
@@ -540,7 +523,8 @@ async function main() {
540
523
  initialBuildOk = true;
541
524
  // Initial compile + upload if flags are set
542
525
  if (options.compile) {
543
- const buildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
526
+ const baseBuildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
527
+ const { buildTarget, defines: psramDefines } = applyPsramToArduinoBuild(baseBuildTarget, config?.outputDefines, config?.psram);
544
528
  const watchOpts = {
545
529
  outputDir: path.dirname(result.sourcePath),
546
530
  sourcePath: result.sourcePath,
@@ -549,8 +533,12 @@ async function main() {
549
533
  baud: options.baud ?? config?.console?.baudRate,
550
534
  optimize: config?.outputOptimize,
551
535
  extraFlags: config?.outputExtraFlags,
552
- defines: config?.outputDefines,
536
+ defines: psramDefines,
537
+ psram: config?.psram,
553
538
  frameworkConfig: config?.frameworkConfig,
539
+ zephyrConfig: config?.zephyrConfig,
540
+ display: displayConfigForTranspile(config),
541
+ debug: options.debug,
554
542
  };
555
543
  ui.printCompiling(buildTarget ?? "native");
556
544
  const compileResult = compileSource(watchOpts);
@@ -622,6 +610,8 @@ async function main() {
622
610
  diagnostics: options.diagnostics,
623
611
  display: displayConfigForTranspile(config),
624
612
  projectRoot: options.projectRoot,
613
+ autosar: options.autosar,
614
+ autosarArxml: options.autosarArxml,
625
615
  });
626
616
  printDiagnostics(rebuildResult.diagnostics);
627
617
  ui.printTasks(rebuildResult.asyncTaskNames ?? [], rebuildResult.usesTimers ?? false);
@@ -634,7 +624,8 @@ async function main() {
634
624
  else {
635
625
  ui.printSuccess();
636
626
  if (options.compile) {
637
- const buildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
627
+ const baseBuildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
628
+ const { buildTarget, defines: psramDefines } = applyPsramToArduinoBuild(baseBuildTarget, config?.outputDefines, config?.psram);
638
629
  const rebuildOpts = {
639
630
  outputDir: path.dirname(rebuildResult.sourcePath),
640
631
  sourcePath: rebuildResult.sourcePath,
@@ -643,8 +634,12 @@ async function main() {
643
634
  baud: options.baud ?? config?.console?.baudRate,
644
635
  optimize: config?.outputOptimize,
645
636
  extraFlags: config?.outputExtraFlags,
646
- defines: config?.outputDefines,
637
+ defines: psramDefines,
638
+ psram: config?.psram,
647
639
  frameworkConfig: config?.frameworkConfig,
640
+ zephyrConfig: config?.zephyrConfig,
641
+ display: displayConfigForTranspile(config),
642
+ debug: options.debug,
648
643
  };
649
644
  ui.printCompiling(buildTarget ?? "native");
650
645
  const compileResult = compileSource(rebuildOpts);
@@ -709,6 +704,8 @@ async function main() {
709
704
  diagnostics: options.diagnostics,
710
705
  display: displayConfigForTranspile(config) ?? options.display,
711
706
  projectRoot: options.projectRoot,
707
+ autosar: options.autosar,
708
+ autosarArxml: options.autosarArxml,
712
709
  });
713
710
  printDiagnostics(result.diagnostics);
714
711
  ui.printTasks(result.asyncTaskNames ?? [], result.usesTimers ?? false);
@@ -735,7 +732,8 @@ async function main() {
735
732
  return;
736
733
  }
737
734
  // --compile (delegates to the active framework's toolchain)
738
- const buildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
735
+ const baseBuildTarget = effectivePlatformContext?.frameworkData?.buildTarget ?? options.platformContext?.frameworkData?.buildTarget;
736
+ const { buildTarget, defines: psramDefines } = applyPsramToArduinoBuild(baseBuildTarget, config?.outputDefines, config?.psram);
739
737
  const toolchainOpts = {
740
738
  outputDir: path.dirname(result.sourcePath),
741
739
  sourcePath: result.sourcePath,
@@ -744,8 +742,12 @@ async function main() {
744
742
  baud: options.baud ?? config?.console?.baudRate,
745
743
  optimize: config?.outputOptimize,
746
744
  extraFlags: config?.outputExtraFlags,
747
- defines: config?.outputDefines,
745
+ defines: psramDefines,
746
+ psram: config?.psram,
748
747
  frameworkConfig: config?.frameworkConfig,
748
+ zephyrConfig: config?.zephyrConfig,
749
+ display: displayConfigForTranspile(config),
750
+ debug: options.debug,
749
751
  };
750
752
  ui.printCompiling(buildTarget ?? "native");
751
753
  let compileResult;
@@ -796,8 +798,10 @@ async function main() {
796
798
  return;
797
799
  }
798
800
  if (options.monitor) {
799
- // --monitor (blocks until Ctrl+C)
800
- ui.printMonitoring(port, options.baud);
801
+ // --monitor (blocks until Ctrl+C). Show the resolved baud (config ?? --baud)
802
+ // in the banner — toolchainOpts.baud is built the same way, so what the
803
+ // user sees is what the framework monitor opens the port at.
804
+ ui.printMonitoring(port, toolchainOpts.baud ?? 115200);
801
805
  monitorDevice(toolchainOpts);
802
806
  return;
803
807
  }
@@ -13,7 +13,7 @@ export interface ResolvedCuttlefishConfig {
13
13
  contract?: string;
14
14
  /** Build target identifier (e.g. FQBN for Arduino CLI). */
15
15
  buildTarget?: string;
16
- /** Output framework (e.g. 'arduino', 'platformio'). */
16
+ /** Output framework (e.g. 'arduino'). */
17
17
  outputFramework?: string;
18
18
  /** Optimization level. */
19
19
  outputOptimize?: string;
@@ -21,9 +21,12 @@ export interface ResolvedCuttlefishConfig {
21
21
  outputOutDir?: string;
22
22
  /**
23
23
  * Framework package for code generation strategy.
24
- * Can be '@typecad/framework-arduino', '@typecad/framework-avr', or a custom path.
24
+ * Can be '@typecad/framework-arduino', '@typecad/framework-zephyr',
25
+ * '@typecad/framework-native', or a custom path.
25
26
  */
26
27
  framework?: string;
28
+ /** ESP32 PSRAM type ('opi' | 'quad') when the target board has PSRAM. */
29
+ psram?: 'opi' | 'quad';
27
30
  /** Entry point TypeScript file (relative to config file directory). */
28
31
  entry?: string;
29
32
  /** Path to the config file that was loaded. */
@@ -39,6 +42,8 @@ export interface ResolvedCuttlefishConfig {
39
42
  outputDefines?: Record<string, string>;
40
43
  /** Framework-specific config (e.g. `native` section). */
41
44
  frameworkConfig?: Record<string, unknown>;
45
+ /** Zephyr-specific config (e.g. `zephyr` section). */
46
+ zephyrConfig?: Record<string, unknown>;
42
47
  /** Display profile config. */
43
48
  display?: import("./api/shared/display-profile.js").DisplayConfig;
44
49
  }