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

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 (222) hide show
  1. package/README.md +10 -10
  2. package/dist/api/board-types.d.ts +1 -1
  3. package/dist/api/config.d.ts +32 -0
  4. package/dist/api/index.d.ts +1 -1
  5. package/dist/api/schema/types.d.ts +15 -0
  6. package/dist/api/shared/async-runtime-static.d.ts +12 -1
  7. package/dist/api/shared/async-runtime-static.js +69 -24
  8. package/dist/api/shared/async-symbol-detector.d.ts +11 -0
  9. package/dist/api/shared/async-symbol-detector.js +140 -0
  10. package/dist/api/shared/async-types.d.ts +24 -0
  11. package/dist/api/shared/coop-scheduler.d.ts +60 -0
  12. package/dist/api/shared/coop-scheduler.js +149 -0
  13. package/dist/api/shared/display-adapter.d.ts +4 -3
  14. package/dist/api/shared/display-adapter.js +15 -118
  15. package/dist/api/shared/display-adapters/sdl.js +38 -31
  16. package/dist/api/shared/display-profile.d.ts +47 -25
  17. package/dist/api/shared/display-profile.js +31 -164
  18. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  19. package/dist/api/shared/framework-manifest-registry.js +27 -0
  20. package/dist/api/shared/framework-manifest.d.ts +595 -0
  21. package/dist/api/shared/framework-manifest.js +193 -0
  22. package/dist/api/shared/glcdfont.d.ts +12 -0
  23. package/dist/api/shared/glcdfont.js +124 -0
  24. package/dist/api/shared/graphics-strategy.d.ts +37 -0
  25. package/dist/api/shared/hal-op-ir.d.ts +764 -1
  26. package/dist/api/shared/hal-op-ir.js +129 -1
  27. package/dist/api/shared/index.d.ts +20 -2
  28. package/dist/api/shared/index.js +25 -1
  29. package/dist/api/shared/ir-core.d.ts +4 -0
  30. package/dist/api/shared/ir-declarations.d.ts +6 -0
  31. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  32. package/dist/api/shared/native-display-op-resolver.js +64 -0
  33. package/dist/api/shared/platform-strategy.d.ts +119 -2
  34. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  35. package/dist/api/shared/promise-runtime.d.ts +1 -1
  36. package/dist/api/shared/promise-runtime.js +95 -13
  37. package/dist/api/shared/toolchain-types.d.ts +17 -0
  38. package/dist/api/shared/types.d.ts +8 -0
  39. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  40. package/dist/api/shared/validate-framework-manifest.js +494 -0
  41. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  42. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  43. package/dist/api/shared/worker-runtime.d.ts +69 -0
  44. package/dist/api/shared/worker-runtime.js +163 -0
  45. package/dist/cli-utils.d.ts +1 -0
  46. package/dist/cli-utils.js +3 -1
  47. package/dist/cli.js +249 -32
  48. package/dist/config-loader.d.ts +7 -2
  49. package/dist/config-loader.js +23 -3
  50. package/dist/config-schema.d.ts +112 -70
  51. package/dist/config-schema.js +14 -0
  52. package/dist/contract/board-generator.d.ts +17 -0
  53. package/dist/contract/board-generator.js +57 -0
  54. package/dist/contract/contract-parser.d.ts +217 -0
  55. package/dist/contract/contract-parser.js +224 -0
  56. package/dist/contract/index.d.ts +22 -0
  57. package/dist/contract/index.js +68 -0
  58. package/dist/create/board-codegen.js +4 -4
  59. package/dist/create/board-generators.js +4 -5
  60. package/dist/create/board-spec.d.ts +72 -75
  61. package/dist/create/board-spec.js +0 -1
  62. package/dist/create/framework-catalog.d.ts +64 -0
  63. package/dist/create/framework-catalog.js +136 -0
  64. package/dist/create/index.d.ts +5 -1
  65. package/dist/create/index.js +3 -1
  66. package/dist/create/init-scaffold.d.ts +12 -3
  67. package/dist/create/init-scaffold.js +49 -14
  68. package/dist/create/init-templates.d.ts +4 -0
  69. package/dist/create/init-templates.js +263 -17
  70. package/dist/create/init-wizard.js +41 -27
  71. package/dist/create/install-deps.d.ts +32 -0
  72. package/dist/create/install-deps.js +46 -0
  73. package/dist/debug/preprocessor.js +178 -39
  74. package/dist/debug/types.d.ts +23 -0
  75. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  76. package/dist/diagnostics/mermaid-builder.js +34 -24
  77. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  78. package/dist/emit/compliance/arxml-writer.js +34 -0
  79. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  80. package/dist/emit/compliance/compliance-context.js +113 -0
  81. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  82. package/dist/emit/compliance/deviation-ledger.js +47 -0
  83. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  84. package/dist/emit/compliance/deviation-writer.js +37 -0
  85. package/dist/emit/compliance/index.d.ts +7 -0
  86. package/dist/emit/compliance/index.js +6 -0
  87. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  88. package/dist/emit/compliance/rule-engine.js +101 -0
  89. package/dist/emit/compliance/rules.d.ts +16 -0
  90. package/dist/emit/compliance/rules.js +191 -0
  91. package/dist/emit/compliance/types.d.ts +59 -0
  92. package/dist/emit/compliance/types.js +8 -0
  93. package/dist/emit/cpp-emitter.js +4 -3
  94. package/dist/emit/emitters/class-emitter.js +6 -1
  95. package/dist/emit/emitters/emitter-context.d.ts +28 -1
  96. package/dist/emit/emitters/function-emitter-impl.js +153 -53
  97. package/dist/emit/emitters/line-appender.js +19 -0
  98. package/dist/emit/emitters/line-marker.d.ts +38 -0
  99. package/dist/emit/emitters/line-marker.js +39 -0
  100. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  101. package/dist/emit/emitters/output-finalizer.js +101 -12
  102. package/dist/emit/emitters/setup.d.ts +17 -0
  103. package/dist/emit/emitters/setup.js +372 -30
  104. package/dist/emit/emitters/top-level-prep.js +8 -0
  105. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  106. package/dist/emit/emitters/ui-emitter.js +33 -9
  107. package/dist/emit/expression-renderer.d.ts +1 -1
  108. package/dist/emit/expression-renderer.js +91 -6
  109. package/dist/emit/route-hal-op.js +18 -5
  110. package/dist/emit/snprintf-helpers.js +15 -4
  111. package/dist/emit/statement-renderer.d.ts +10 -0
  112. package/dist/emit/statement-renderer.js +34 -5
  113. package/dist/emit/utils/async-state-machine.js +221 -125
  114. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  115. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  116. package/dist/framework-package.js +2 -0
  117. package/dist/framework-registry.d.ts +17 -0
  118. package/dist/install/framework-catalog.d.ts +53 -0
  119. package/dist/install/framework-catalog.js +107 -0
  120. package/dist/install/handle-install.d.ts +35 -0
  121. package/dist/install/handle-install.js +177 -0
  122. package/dist/install/index.d.ts +4 -0
  123. package/dist/install/index.js +3 -0
  124. package/dist/ir/adc-range-validation.js +40 -26
  125. package/dist/ir/build-ir-state.d.ts +1 -0
  126. package/dist/ir/build-ir-state.js +14 -0
  127. package/dist/ir/build-ir.js +9 -5
  128. package/dist/ir/call-graph.js +16 -0
  129. package/dist/ir/expression-to-ir.js +207 -6
  130. package/dist/ir/feature-registry.js +7 -25
  131. package/dist/ir/function-builder.js +22 -0
  132. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  133. package/dist/ir/hal/hal-emitter.js +50 -18
  134. package/dist/ir/hal/hal-parser.d.ts +6 -0
  135. package/dist/ir/hal/hal-parser.js +81 -0
  136. package/dist/ir/hal/hal-plugins.js +733 -1
  137. package/dist/ir/identifier-collector.js +35 -0
  138. package/dist/ir/interrupt-analysis.d.ts +5 -1
  139. package/dist/ir/interrupt-analysis.js +16 -19
  140. package/dist/ir/memory-budget-validation.js +1 -0
  141. package/dist/ir/network-validation.d.ts +4 -0
  142. package/dist/ir/network-validation.js +184 -0
  143. package/dist/ir/ownership-analysis.js +39 -1
  144. package/dist/ir/peripheral-ownership.js +5 -0
  145. package/dist/ir/peripheral-validation.d.ts +1 -1
  146. package/dist/ir/peripheral-validation.js +6 -3
  147. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  148. package/dist/ir/pin-alias-conflict.js +2 -1
  149. package/dist/ir/pin-capability-validation.js +71 -32
  150. package/dist/ir/pin-mode-validation.d.ts +2 -2
  151. package/dist/ir/pin-mode-validation.js +54 -18
  152. package/dist/ir/pin-safety.d.ts +1 -1
  153. package/dist/ir/pin-safety.js +2 -1
  154. package/dist/ir/program-analysis.d.ts +81 -0
  155. package/dist/ir/program-analysis.js +429 -0
  156. package/dist/ir/pulldown-validation.d.ts +1 -1
  157. package/dist/ir/pulldown-validation.js +2 -1
  158. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  159. package/dist/ir/pwm-timer-sharing.js +2 -1
  160. package/dist/ir/render-expr.js +11 -0
  161. package/dist/ir/resource-analysis.js +2 -0
  162. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  163. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  164. package/dist/ir/timing-validation.d.ts +6 -1
  165. package/dist/ir/timing-validation.js +61 -13
  166. package/dist/ir/transformers/call-statement.js +105 -0
  167. package/dist/ir/transformers/expressions.js +62 -0
  168. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  169. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  170. package/dist/ir/transformers/namespace-methods.js +17 -12
  171. package/dist/ir/transformers/ui-reactive.js +2 -2
  172. package/dist/ir/transformers/variables.js +137 -19
  173. package/dist/ir/try-catch-validation.js +2 -0
  174. package/dist/ir/type-resolution.js +20 -2
  175. package/dist/ir/ui-element-auto-wire.js +7 -5
  176. package/dist/ir/unit-suspicion-validation.js +9 -7
  177. package/dist/ir/utils/map-statements.d.ts +4 -0
  178. package/dist/ir/utils/map-statements.js +79 -0
  179. package/dist/ir/validation-orchestrator.js +15 -8
  180. package/dist/ir/worker-analysis.d.ts +10 -0
  181. package/dist/ir/worker-analysis.js +261 -0
  182. package/dist/libdef/c-to-decl.d.ts +27 -0
  183. package/dist/libdef/c-to-decl.js +397 -0
  184. package/dist/libdef/component-decls.d.ts +2 -0
  185. package/dist/libdef/component-decls.js +6 -0
  186. package/dist/libdef/component-discovery.d.ts +43 -0
  187. package/dist/libdef/component-discovery.js +83 -0
  188. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  189. package/dist/libdef/cpp-to-decl.js +72 -0
  190. package/dist/libdef/registry.js +5 -2
  191. package/dist/lint-cache.d.ts +59 -0
  192. package/dist/lint-cache.js +257 -0
  193. package/dist/orchestrator/graph-builder.js +14 -9
  194. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  195. package/dist/platform/coop-scheduler-runtime.js +41 -0
  196. package/dist/platform/generic-strategy.d.ts +15 -3
  197. package/dist/platform/generic-strategy.js +49 -4
  198. package/dist/safety/safety-bridge.d.ts +11 -0
  199. package/dist/safety/safety-bridge.js +48 -0
  200. package/dist/safety/sidecar-bridge.d.ts +5 -0
  201. package/dist/safety/sidecar-bridge.js +22 -0
  202. package/dist/safety-hook.d.ts +79 -0
  203. package/dist/safety-hook.js +35 -0
  204. package/dist/testing.d.ts +8 -2
  205. package/dist/testing.js +9 -2
  206. package/dist/transpile.d.ts +3 -0
  207. package/dist/transpile.js +191 -49
  208. package/dist/types.d.ts +22 -3
  209. package/dist/ui-hook.d.ts +17 -3
  210. package/dist/utils/cli.js +122 -2
  211. package/dist/utils/fs.d.ts +13 -0
  212. package/dist/utils/fs.js +50 -0
  213. package/dist/utils/ui.js +3 -1
  214. package/package.json +23 -3
  215. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  216. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  217. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  218. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  219. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  220. package/dist/api/shared/display-adapters/st7796.js +0 -132
  221. package/dist/ir/heap-array-validation.d.ts +0 -24
  222. package/dist/ir/heap-array-validation.js +0 -29
@@ -0,0 +1,107 @@
1
+ // Framework catalog + board→framework compatibility for `cuttlefish install`.
2
+ //
3
+ // A "framework" is a `@typecad/framework-<id>` npm package. This module is the
4
+ // single source of truth for which frameworks exist as installable packages and
5
+ // which are compatible with a given board architecture. It is deliberately
6
+ // free of side effects (no fs/process beyond lockfile/package.json reads in
7
+ // detectPackageManager) so it unit-tests cleanly.
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+ /**
11
+ * The installable frameworks. Kept aligned with the packages/ directory:
12
+ * framework-arduino, framework-native, framework-zephyr all ship real packages.
13
+ * esp-idf is intentionally absent (no published package in this repo).
14
+ */
15
+ export const FRAMEWORK_CATALOG = [
16
+ { id: "arduino", packageName: "@typecad/framework-arduino", label: "Arduino (digitalWrite, Wire, SPI)", installable: true },
17
+ { id: "zephyr", packageName: "@typecad/framework-zephyr", label: "Zephyr RTOS", installable: true },
18
+ { id: "native", packageName: "@typecad/framework-native", label: "Native (Windows/Linux executable)", installable: true },
19
+ ];
20
+ /**
21
+ * Architecture → compatible framework ids. Derived from each framework
22
+ * package's framework.manifest.ts `profile.targets`:
23
+ * - arduino: avr, esp32 family, rp2040/rp2350, samd, stm32
24
+ * - zephyr: nrf52 (xiao_ble), esp32, esp32s3
25
+ * - native: desktop only
26
+ * Unknown embedded architectures fall back to [arduino] (the broadest core).
27
+ */
28
+ const ARCHITECTURE_FRAMEWORKS = {
29
+ avr: ["arduino"],
30
+ esp32: ["arduino", "zephyr"],
31
+ esp32s2: ["arduino"],
32
+ esp32s3: ["arduino", "zephyr"],
33
+ esp32c3: ["arduino"],
34
+ esp32c6: ["arduino"],
35
+ rp2040: ["arduino"],
36
+ rp2350: ["arduino"],
37
+ samd: ["arduino"],
38
+ stm32: ["arduino"],
39
+ nrf52: ["zephyr"],
40
+ };
41
+ const FALLBACK_FRAMEWORKS = ["arduino"];
42
+ /** Look up a catalog entry by framework id (e.g. "arduino"). */
43
+ export function frameworkCatalogEntry(id) {
44
+ return FRAMEWORK_CATALOG.find((f) => f.id === id);
45
+ }
46
+ /**
47
+ * The framework ids compatible with a board. Native boards map to ["native"];
48
+ * embedded boards map via ARCHITECTURE_FRAMEWORKS (falling back to arduino for
49
+ * unknown architectures). Order is preserved as the catalog order so the most
50
+ * common framework is offered first in the prompt.
51
+ */
52
+ export function frameworksForTarget(target) {
53
+ let ids;
54
+ if (target.isNative) {
55
+ ids = ["native"];
56
+ }
57
+ else {
58
+ ids = ARCHITECTURE_FRAMEWORKS[target.architecture ?? ""] ?? FALLBACK_FRAMEWORKS;
59
+ }
60
+ // Re-map ids → catalog entries in catalog order (stable ordering), dropping
61
+ // any id that has no catalog entry (defensive — keeps the prompt clean).
62
+ return FRAMEWORK_CATALOG.filter((entry) => ids.includes(entry.id));
63
+ }
64
+ /**
65
+ * Detect the package manager for a directory. Priority:
66
+ * 1. package.json#packageManager field (the strongest signal, Corepack-style)
67
+ * 2. lockfile presence (pnpm-lock.yaml / yarn.lock → otherwise npm)
68
+ * 3. default "npm"
69
+ * Never throws — unreadable/missing files fall through to the next signal.
70
+ */
71
+ export function detectPackageManager(cwd) {
72
+ try {
73
+ const pkgPath = path.join(cwd, "package.json");
74
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
75
+ const pm = typeof pkg.packageManager === "string" ? pkg.packageManager : "";
76
+ if (pm.startsWith("pnpm"))
77
+ return "pnpm";
78
+ if (pm.startsWith("yarn"))
79
+ return "yarn";
80
+ if (pm.startsWith("npm"))
81
+ return "npm";
82
+ }
83
+ catch {
84
+ // no package.json or unparseable JSON — fall through to lockfile detection
85
+ }
86
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml")))
87
+ return "pnpm";
88
+ if (fs.existsSync(path.join(cwd, "yarn.lock")))
89
+ return "yarn";
90
+ // package-lock.json implies npm; absence also defaults to npm.
91
+ return "npm";
92
+ }
93
+ /**
94
+ * Build the package-manager invocation that installs `packageName` into the
95
+ * current project (project-local, never global — per the install command spec).
96
+ */
97
+ export function buildInstallCommand(pm, packageName) {
98
+ switch (pm) {
99
+ case "pnpm":
100
+ return { bin: "pnpm", args: ["add", packageName] };
101
+ case "yarn":
102
+ return { bin: "yarn", args: ["add", packageName] };
103
+ case "npm":
104
+ default:
105
+ return { bin: "npm", args: ["install", packageName] };
106
+ }
107
+ }
@@ -0,0 +1,35 @@
1
+ import type { InstallCommandOptions } from "../types.js";
2
+ import { type FrameworkCatalogEntry, type PackageManager } from "./framework-catalog.js";
3
+ interface InstallCommand {
4
+ bin: string;
5
+ args: string[];
6
+ cwd: string;
7
+ }
8
+ interface InstallRunResult {
9
+ /** Exit status; null when the process could not be launched (ENOENT, etc.). */
10
+ status: number | null;
11
+ /** Populated only when the binary could not be launched. */
12
+ launchError?: string;
13
+ }
14
+ type InstallRunner = (cmd: InstallCommand) => InstallRunResult;
15
+ /**
16
+ * FOR TESTS ONLY. Replaces the real spawn-based installer with `runner`.
17
+ * Pass `undefined` to restore the real executor.
18
+ */
19
+ export declare function __setInstallRunnerForTest(runner: InstallRunner | undefined): void;
20
+ export interface InstallResult {
21
+ pm: PackageManager;
22
+ bin: string;
23
+ args: string[];
24
+ }
25
+ /**
26
+ * Resolve the package manager and run the install for `entry`'s package. In
27
+ * dry-run mode the command is printed but not executed. Throws on launch
28
+ * failure or non-zero exit so the CLI surfaces a clear error.
29
+ */
30
+ export declare function installFrameworkPackage(entry: FrameworkCatalogEntry, opts: {
31
+ cwd: string;
32
+ dryRun?: boolean;
33
+ }): InstallResult;
34
+ export declare function handleInstall(options: InstallCommandOptions): Promise<void>;
35
+ export {};
@@ -0,0 +1,177 @@
1
+ // `cuttlefish install` — installs a chosen @typecad/framework-* package into the
2
+ // current project. The flow asks which board to target first, then narrows the
3
+ // framework choices to those compatible with that board (see framework-catalog).
4
+ // A framework id may be passed directly (`cuttlefish install arduino`) to skip
5
+ // the prompts entirely, which also makes the command usable in CI.
6
+ import * as readline from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ import { spawnSync } from "node:child_process";
9
+ import chalk from "chalk";
10
+ import { KNOWN_TARGETS } from "../create/index.js";
11
+ import { FRAMEWORK_CATALOG, frameworkCatalogEntry, frameworksForTarget, detectPackageManager, buildInstallCommand, } from "./framework-catalog.js";
12
+ // ── prompts (mirror the create wizard's style for a consistent UX) ──────────
13
+ async function promptSelect(rl, prompt, options) {
14
+ console.log(`${chalk.cyan("?")} ${prompt}:`);
15
+ for (let i = 0; i < options.length; i++) {
16
+ console.log(` ${chalk.dim(`${i + 1})`)} ${options[i].label}`);
17
+ }
18
+ while (true) {
19
+ const answer = await rl.question(` Enter number (1-${options.length}): `);
20
+ const idx = parseInt(answer.trim(), 10) - 1;
21
+ if (idx >= 0 && idx < options.length) {
22
+ return options[idx].value;
23
+ }
24
+ console.log(` ${chalk.red("✗")} Please enter a number between 1 and ${options.length}.`);
25
+ }
26
+ }
27
+ async function selectBoardInteractively(rl) {
28
+ const options = KNOWN_TARGETS.map((t) => ({
29
+ label: t.isNative
30
+ ? `${t.displayName} (Windows/Linux executable)`
31
+ : `${t.displayName} (${t.architecture.toUpperCase()})`,
32
+ value: t.id,
33
+ }));
34
+ const selectedId = await promptSelect(rl, "Target board", options);
35
+ return KNOWN_TARGETS.find((t) => t.id === selectedId);
36
+ }
37
+ /**
38
+ * Pick a framework for `target`. When only one framework is compatible with the
39
+ * board it is auto-selected (mirrors the create wizard's single-option path) so
40
+ * the user isn't asked a question with one answer.
41
+ */
42
+ async function selectFrameworkInteractively(rl, target, compatible) {
43
+ if (compatible.length === 1) {
44
+ const only = compatible[0];
45
+ console.log(`${chalk.cyan("?")} Framework: ${chalk.white(only.label)} ${chalk.dim(`(only option for ${target.id})`)}`);
46
+ return only;
47
+ }
48
+ const selectedId = await promptSelect(rl, "Framework", compatible.map((f) => ({ label: f.label, value: f.id })));
49
+ return compatible.find((f) => f.id === selectedId);
50
+ }
51
+ let testRunner;
52
+ /**
53
+ * FOR TESTS ONLY. Replaces the real spawn-based installer with `runner`.
54
+ * Pass `undefined` to restore the real executor.
55
+ */
56
+ export function __setInstallRunnerForTest(runner) {
57
+ testRunner = runner;
58
+ }
59
+ function runRealInstall(cmd) {
60
+ // stdio: "inherit" streams the package manager's own output to the terminal
61
+ // (install progress, deprecation warnings, etc.). On failure the user has
62
+ // already seen the details above, so we only need to report the exit code.
63
+ const result = spawnSync(cmd.bin, cmd.args, {
64
+ cwd: cmd.cwd,
65
+ stdio: "inherit",
66
+ });
67
+ if (result.error) {
68
+ const errno = result.error.code;
69
+ return {
70
+ status: null,
71
+ launchError: errno === "ENOENT" ? `'${cmd.bin}' not found on PATH` : String(result.error),
72
+ };
73
+ }
74
+ return { status: result.status };
75
+ }
76
+ /**
77
+ * Resolve the package manager and run the install for `entry`'s package. In
78
+ * dry-run mode the command is printed but not executed. Throws on launch
79
+ * failure or non-zero exit so the CLI surfaces a clear error.
80
+ */
81
+ export function installFrameworkPackage(entry, opts) {
82
+ const pm = detectPackageManager(opts.cwd);
83
+ const { bin, args } = buildInstallCommand(pm, entry.packageName);
84
+ if (opts.dryRun) {
85
+ console.log(`${chalk.cyan("$")} ${bin} ${args.join(" ")}`);
86
+ return { pm, bin, args };
87
+ }
88
+ const cmd = { bin, args, cwd: opts.cwd };
89
+ const run = testRunner ?? runRealInstall;
90
+ const result = run(cmd);
91
+ if (result.launchError) {
92
+ throw new Error(`Failed to run '${bin}': ${result.launchError}`);
93
+ }
94
+ if (result.status !== 0) {
95
+ throw new Error(`'${bin} ${args.join(" ")}' exited with code ${result.status}. See the package manager output above for details.`);
96
+ }
97
+ return { pm, bin, args };
98
+ }
99
+ // ── command entry point ─────────────────────────────────────────────────────
100
+ export async function handleInstall(options) {
101
+ const cwd = process.cwd();
102
+ // 1. Resolve which framework to install.
103
+ let entry;
104
+ if (options.framework) {
105
+ const resolved = frameworkCatalogEntry(options.framework);
106
+ if (!resolved) {
107
+ const available = FRAMEWORK_CATALOG.filter((f) => f.installable).map((f) => f.id).join(", ");
108
+ throw new Error(`Unknown framework '${options.framework}'. Available: ${available}`);
109
+ }
110
+ if (!resolved.installable) {
111
+ throw new Error(`Framework '${options.framework}' has no published package yet and cannot be installed.`);
112
+ }
113
+ entry = resolved;
114
+ console.log(`${chalk.cyan("?")} Framework: ${chalk.white(entry.label)}`);
115
+ }
116
+ else {
117
+ // Resolve the target board. --board skips the board prompt; otherwise the
118
+ // board is chosen interactively (which requires a TTY — handled below).
119
+ let target;
120
+ if (options.board) {
121
+ const found = KNOWN_TARGETS.find((t) => t.id === options.board);
122
+ if (!found) {
123
+ const available = KNOWN_TARGETS.map((t) => t.id).join(", ");
124
+ throw new Error(`Unknown board '${options.board}'. Available: ${available}`);
125
+ }
126
+ target = found;
127
+ console.log(`${chalk.cyan("?")} Board: ${chalk.white(target.displayName)} (${chalk.dim(target.id)})`);
128
+ }
129
+ // Narrow to the frameworks compatible with that board.
130
+ let compatible;
131
+ if (target) {
132
+ compatible = frameworksForTarget(target).filter((f) => f.installable);
133
+ if (compatible.length === 0) {
134
+ throw new Error(`No installable frameworks are compatible with board '${target.id}'.`);
135
+ }
136
+ }
137
+ if (compatible && compatible.length === 1) {
138
+ // Only one framework fits this board — auto-select it (no prompt, so this
139
+ // path also works under CI / non-interactive stdin).
140
+ entry = compatible[0];
141
+ console.log(`${chalk.cyan("?")} Framework: ${chalk.white(entry.label)} ${chalk.dim(`(only option for ${target.id})`)}`);
142
+ }
143
+ else {
144
+ // Need to prompt — for the board (if --board wasn't given) and/or for the
145
+ // framework (when 2+ are compatible). Refuse to hang on a readline that
146
+ // can't be answered (e.g. piped stdin under CI).
147
+ if (!process.stdin.isTTY) {
148
+ throw new Error("No framework specified and stdin is not interactive. " +
149
+ "Pass a framework id (e.g. 'cuttlefish install arduino'), " +
150
+ "or a board with a single compatible framework (e.g. '--board arduino-uno').");
151
+ }
152
+ const rl = readline.createInterface({ input, output });
153
+ try {
154
+ if (!target) {
155
+ target = await selectBoardInteractively(rl);
156
+ compatible = frameworksForTarget(target).filter((f) => f.installable);
157
+ if (compatible.length === 0) {
158
+ throw new Error(`No installable frameworks are compatible with board '${target.id}'.`);
159
+ }
160
+ }
161
+ entry = await selectFrameworkInteractively(rl, target, compatible);
162
+ }
163
+ finally {
164
+ rl.close();
165
+ }
166
+ }
167
+ }
168
+ // 2. Install it.
169
+ console.log(`\n${chalk.cyan("⤳")} Installing ${chalk.white(entry.packageName)} into ${chalk.dim(cwd)}…`);
170
+ const result = installFrameworkPackage(entry, { cwd, dryRun: options.dryRun });
171
+ if (options.dryRun) {
172
+ console.log(chalk.dim("(dry-run — nothing was installed)"));
173
+ return;
174
+ }
175
+ console.log(`\n${chalk.green("✓")} Installed ${chalk.white(entry.packageName)} via ${result.pm}.`);
176
+ console.log(chalk.dim(`Next: run 'cuttlefish create' and pick ${entry.id}, or set framework: "${entry.packageName}" in cuttlefish.config.ts.`));
177
+ }
@@ -0,0 +1,4 @@
1
+ export { handleInstall, installFrameworkPackage, __setInstallRunnerForTest } from "./handle-install.js";
2
+ export type { InstallResult } from "./handle-install.js";
3
+ export { FRAMEWORK_CATALOG, frameworkCatalogEntry, frameworksForTarget, detectPackageManager, buildInstallCommand, } from "./framework-catalog.js";
4
+ export type { FrameworkCatalogEntry, BoardLike, PackageManager, } from "./framework-catalog.js";
@@ -0,0 +1,3 @@
1
+ // `cuttlefish install` command — install a @typecad/framework-* package.
2
+ export { handleInstall, installFrameworkPackage, __setInstallRunnerForTest } from "./handle-install.js";
3
+ export { FRAMEWORK_CATALOG, frameworkCatalogEntry, frameworksForTarget, detectPackageManager, buildInstallCommand, } from "./framework-catalog.js";
@@ -4,6 +4,7 @@
4
4
  // Detects comparisons against analogRead() values that exceed the board's
5
5
  // ADC resolution (e.g., comparing to > 1023 on a 10-bit Arduino Uno).
6
6
  // ---------------------------------------------------------------------------
7
+ import { hasLoadedFramework, getLoadedFramework } from '../framework-registry.js';
7
8
  /**
8
9
  * Get ADC configuration from board constants.
9
10
  */
@@ -41,7 +42,19 @@ function isAnalogRead(expr) {
41
42
  return op === 'adc.read' || op === 'adc.read_voltage';
42
43
  }
43
44
  if (expr.kind === 'raw') {
44
- return /analogRead\s*\(/.test(expr.value);
45
+ const value = expr.value;
46
+ // Ask the loaded framework which call names produce an ADC read; fall back
47
+ // to the Wiring-derived analogRead when no framework is loaded.
48
+ const frameworkNames = hasLoadedFramework()
49
+ ? getLoadedFramework().strategy.analogReadCallNames?.()
50
+ : undefined;
51
+ const names = frameworkNames ?? new Set(['analogRead']);
52
+ for (const name of names) {
53
+ const re = new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`);
54
+ if (re.test(value))
55
+ return true;
56
+ }
57
+ return false;
45
58
  }
46
59
  return false;
47
60
  }
@@ -59,7 +72,7 @@ function extractNumericValue(expr) {
59
72
  /**
60
73
  * Check binary comparison expressions for ADC range issues.
61
74
  */
62
- function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostics) {
75
+ function checkComparisonForADCRange(left, operator, right, adcConfig, filePath, diagnostics) {
63
76
  // Check if one side is an analog read and the other is a literal
64
77
  let analogReadSide = null;
65
78
  let literalValue = null;
@@ -83,6 +96,7 @@ function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostic
83
96
  severity: 'info',
84
97
  message: `Comparison ${comparisonDesc} may never be true. ADC resolution is ${adcConfig.resolution}-bit (max ${adcConfig.maxValue}) on this board.`,
85
98
  code: 'adc-range-warning',
99
+ filePath,
86
100
  source: 'adc-range-validation',
87
101
  });
88
102
  }
@@ -90,7 +104,7 @@ function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostic
90
104
  /**
91
105
  * Scan an expression for ADC range issues.
92
106
  */
93
- function scanExpressionForADCRange(expr, adcConfig, diagnostics) {
107
+ function scanExpressionForADCRange(expr, adcConfig, filePath, diagnostics) {
94
108
  if (!expr || typeof expr !== 'object')
95
109
  return;
96
110
  // Check binary comparisons
@@ -98,59 +112,59 @@ function scanExpressionForADCRange(expr, adcConfig, diagnostics) {
98
112
  const bin = expr;
99
113
  const comparisonOps = ['>', '>=', '<', '<=', '===', '==', '!==', '!='];
100
114
  if (comparisonOps.includes(bin.operator)) {
101
- checkComparisonForADCRange(bin.left, bin.operator, bin.right, adcConfig, diagnostics);
115
+ checkComparisonForADCRange(bin.left, bin.operator, bin.right, adcConfig, filePath, diagnostics);
102
116
  }
103
117
  // Recursively scan both sides
104
- scanExpressionForADCRange(bin.left, adcConfig, diagnostics);
105
- scanExpressionForADCRange(bin.right, adcConfig, diagnostics);
118
+ scanExpressionForADCRange(bin.left, adcConfig, filePath, diagnostics);
119
+ scanExpressionForADCRange(bin.right, adcConfig, filePath, diagnostics);
106
120
  }
107
121
  // Check ternary conditions
108
122
  if (expr.kind === 'ternary') {
109
123
  const ternary = expr;
110
- scanExpressionForADCRange(ternary.condition, adcConfig, diagnostics);
111
- scanExpressionForADCRange(ternary.whenTrue, adcConfig, diagnostics);
112
- scanExpressionForADCRange(ternary.whenFalse, adcConfig, diagnostics);
124
+ scanExpressionForADCRange(ternary.condition, adcConfig, filePath, diagnostics);
125
+ scanExpressionForADCRange(ternary.whenTrue, adcConfig, filePath, diagnostics);
126
+ scanExpressionForADCRange(ternary.whenFalse, adcConfig, filePath, diagnostics);
113
127
  }
114
128
  // Check property access
115
129
  if (expr.kind === 'property-access') {
116
130
  const pa = expr;
117
- scanExpressionForADCRange(pa.object, adcConfig, diagnostics);
131
+ scanExpressionForADCRange(pa.object, adcConfig, filePath, diagnostics);
118
132
  }
119
133
  }
120
134
  /**
121
135
  * Scan a statement for ADC range issues.
122
136
  */
123
- function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
137
+ function scanStatementForADCRange(stmt, adcConfig, filePath, diagnostics) {
124
138
  if (!stmt || typeof stmt !== 'object')
125
139
  return;
126
140
  switch (stmt.kind) {
127
141
  case 'var_decl': {
128
142
  const varDecl = stmt;
129
143
  if (varDecl.initializer) {
130
- scanExpressionForADCRange(varDecl.initializer, adcConfig, diagnostics);
144
+ scanExpressionForADCRange(varDecl.initializer, adcConfig, filePath, diagnostics);
131
145
  }
132
146
  break;
133
147
  }
134
148
  case 'assign': {
135
149
  const assign = stmt;
136
150
  if (assign.value) {
137
- scanExpressionForADCRange(assign.value, adcConfig, diagnostics);
151
+ scanExpressionForADCRange(assign.value, adcConfig, filePath, diagnostics);
138
152
  }
139
153
  break;
140
154
  }
141
155
  case 'if': {
142
156
  const ifStmt = stmt;
143
157
  if (ifStmt.condition) {
144
- scanExpressionForADCRange(ifStmt.condition, adcConfig, diagnostics);
158
+ scanExpressionForADCRange(ifStmt.condition, adcConfig, filePath, diagnostics);
145
159
  }
146
160
  if (ifStmt.thenBranch) {
147
161
  for (const s of ifStmt.thenBranch) {
148
- scanStatementForADCRange(s, adcConfig, diagnostics);
162
+ scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
149
163
  }
150
164
  }
151
165
  if (ifStmt.elseBranch) {
152
166
  for (const s of ifStmt.elseBranch) {
153
- scanStatementForADCRange(s, adcConfig, diagnostics);
167
+ scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
154
168
  }
155
169
  }
156
170
  break;
@@ -158,11 +172,11 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
158
172
  case 'while': {
159
173
  const whileStmt = stmt;
160
174
  if (whileStmt.condition) {
161
- scanExpressionForADCRange(whileStmt.condition, adcConfig, diagnostics);
175
+ scanExpressionForADCRange(whileStmt.condition, adcConfig, filePath, diagnostics);
162
176
  }
163
177
  if (whileStmt.body) {
164
178
  for (const s of whileStmt.body) {
165
- scanStatementForADCRange(s, adcConfig, diagnostics);
179
+ scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
166
180
  }
167
181
  }
168
182
  break;
@@ -170,11 +184,11 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
170
184
  case 'for': {
171
185
  const forStmt = stmt;
172
186
  if (forStmt.condition) {
173
- scanExpressionForADCRange(forStmt.condition, adcConfig, diagnostics);
187
+ scanExpressionForADCRange(forStmt.condition, adcConfig, filePath, diagnostics);
174
188
  }
175
189
  if (forStmt.body) {
176
190
  for (const s of forStmt.body) {
177
- scanStatementForADCRange(s, adcConfig, diagnostics);
191
+ scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
178
192
  }
179
193
  }
180
194
  break;
@@ -182,7 +196,7 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
182
196
  case 'return': {
183
197
  const retStmt = stmt;
184
198
  if (retStmt.value) {
185
- scanExpressionForADCRange(retStmt.value, adcConfig, diagnostics);
199
+ scanExpressionForADCRange(retStmt.value, adcConfig, filePath, diagnostics);
186
200
  }
187
201
  break;
188
202
  }
@@ -190,7 +204,7 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
190
204
  const call = stmt;
191
205
  if (call.args) {
192
206
  for (const arg of call.args) {
193
- scanExpressionForADCRange(arg, adcConfig, diagnostics);
207
+ scanExpressionForADCRange(arg, adcConfig, filePath, diagnostics);
194
208
  }
195
209
  }
196
210
  break;
@@ -214,7 +228,7 @@ export function validateADCRange(program, boardConstants) {
214
228
  // Scan top-level statements
215
229
  if (program.topLevelStatements) {
216
230
  for (const stmt of program.topLevelStatements) {
217
- scanStatementForADCRange(stmt, adcConfig, diagnostics);
231
+ scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
218
232
  }
219
233
  }
220
234
  // Scan function bodies
@@ -222,7 +236,7 @@ export function validateADCRange(program, boardConstants) {
222
236
  for (const fn of program.functions) {
223
237
  if (fn.statements) {
224
238
  for (const stmt of fn.statements) {
225
- scanStatementForADCRange(stmt, adcConfig, diagnostics);
239
+ scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
226
240
  }
227
241
  }
228
242
  }
@@ -234,14 +248,14 @@ export function validateADCRange(program, boardConstants) {
234
248
  for (const method of cls.methods) {
235
249
  if (method.statements) {
236
250
  for (const stmt of method.statements) {
237
- scanStatementForADCRange(stmt, adcConfig, diagnostics);
251
+ scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
238
252
  }
239
253
  }
240
254
  }
241
255
  }
242
256
  if (cls.constructor?.statements) {
243
257
  for (const stmt of cls.constructor.statements) {
244
- scanStatementForADCRange(stmt, adcConfig, diagnostics);
258
+ scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
245
259
  }
246
260
  }
247
261
  }
@@ -78,6 +78,7 @@ export declare class CompilationContext {
78
78
  floatVariables: Set<string>;
79
79
  snprintfCounter: number;
80
80
  callbackPlaceholderCounter: number;
81
+ bleCharCounter: number;
81
82
  activeStrategy: PlatformStrategy | null;
82
83
  restParamFunctions: Map<string, string>;
83
84
  /**
@@ -65,6 +65,12 @@ export class CompilationContext {
65
65
  this.floatVariables = new Set();
66
66
  this.snprintfCounter = 0;
67
67
  this.callbackPlaceholderCounter = 0;
68
+ // BLE characteristic index counter — persists across separate Ble.server()
69
+ // calls so a multi-characteristic server (one server() per char, the common
70
+ // ble-demo pattern) gets unique sequential indices instead of every char
71
+ // clobbering slot 0. Read by the server()/characteristic() resolver branches
72
+ // in hal-parser.ts. Reset per file in hal-emitter.ts with the other counters.
73
+ this.bleCharCounter = 0;
68
74
  this.activeStrategy = null;
69
75
  this.restParamFunctions = new Map();
70
76
  /**
@@ -246,6 +252,14 @@ export function setCurrentBoardConstants(v) {
246
252
  pinAliasMap.set(name, num);
247
253
  mcuPinForwardMap.set(name, num);
248
254
  mcuPinReverseMap.set(num, name);
255
+ // Register an identifier-safe variant so that pins whose canonical
256
+ // name is not a legal JS identifier (e.g. nRF52840 "P0.28") can be
257
+ // imported under their underscore form (P0_28). See "Pin Naming
258
+ // Conventions" in the root AGENTS.md.
259
+ const identName = name.replace(/[.\s-]/g, "_");
260
+ if (identName !== name) {
261
+ pinAliasMap.set(identName, num);
262
+ }
249
263
  }
250
264
  }
251
265
  }
@@ -144,7 +144,11 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
144
144
  const parentStrategy = getContext().activeStrategy;
145
145
  return contextStorage.run(new CompilationContext(), () => {
146
146
  getContext().activeStrategy = parentStrategy;
147
- loadHALModules(true); // Parse HAL source files (force reload to pick up changes)
147
+ // Ensure HAL modules are loaded. transpile.ts warms the HAL registry once
148
+ // per transpile run (before the per-file IR build), so this is normally a
149
+ // cheap no-op. The non-forced call is a safety net for direct/test callers
150
+ // of buildProgramIR that didn't warm the registry first.
151
+ loadHALModules();
148
152
  const normalizedSourceText = normalizeEntrypointSyntax(sourceText);
149
153
  const source = parseSource(fileName, normalizedSourceText);
150
154
  const diagnostics = [];
@@ -439,14 +443,14 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
439
443
  }
440
444
  }
441
445
  }
442
- // Track HAL instances imported from board packages and framework stubs
443
- // so the HAL resolver can resolve them to Arduino C++ names.
446
+ // Track HAL instances imported from board packages and framework HAL
447
+ // subpaths so the HAL resolver can resolve them to framework C++ names.
444
448
  // The `@typecad/board` virtual import resolves to the board package, so it
445
449
  // also registers pin aliases (LED, etc.). Case-insensitive on the scope.
446
450
  const lowerSpecifier = moduleSpecifier.toLowerCase();
447
451
  const isHALSource = lowerSpecifier.startsWith('@typecad/board-')
448
- || lowerSpecifier === '@typecad/framework-arduino/arduino'
449
- || lowerSpecifier === '@typecad/board';
452
+ || lowerSpecifier === '@typecad/board'
453
+ || /^@typecad\/framework-[a-z0-9-]+\/(arduino|hal|gpio)$/.test(lowerSpecifier);
450
454
  // UI authoring namespace: `import { ui } from "@typecad/ui"`. The `ui`
451
455
  // value is a compile-time construct (its calls are intercepted by
452
456
  // tryResolveUICall); it must NOT be emitted as a C++ struct/value.
@@ -159,6 +159,22 @@ export function buildCallGraph(program) {
159
159
  }
160
160
  }
161
161
  }
162
+ // Registered callbacks from the HAL resolver (e.g. the arrow function passed
163
+ // to `Ble.characteristic(...).onRead(() => readTemp())`) are NOT present in
164
+ // top-level statements — they ride in `program.registeredCallbacks` and are
165
+ // later hoisted to file-scope `*_isr_*` functions by top-level-prep. The
166
+ // placeholder name (`__CALLBACK_N__`) is what surfaces in top-level deps,
167
+ // not the identifiers the callback body references (e.g. a free function
168
+ // like `readTemp`). Without this scan those references are invisible to the
169
+ // call graph: the function survives the hoist (its call sits inside the
170
+ // emitted ISR) but is tree-shaken as unreachable, and g++ later reports it
171
+ // "not declared in this scope". Attach their body identifiers to
172
+ // __top_level__ since the callbacks execute from the top-level entry path.
173
+ for (const rc of (program.registeredCallbacks ?? [])) {
174
+ for (const id of collectExpressionIdentifiers(rc.callbackIR)) {
175
+ topLevelDependencies.add(id);
176
+ }
177
+ }
162
178
  nodes.set("__top_level__", {
163
179
  name: "__top_level__",
164
180
  kind: "variable",