mikrojs 0.0.7 → 0.0.8

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 (76) hide show
  1. package/dist/cli/cliWrapper.js +13 -0
  2. package/dist/cli/cliWrapper.js.map +1 -1
  3. package/dist/cli/commands/flash.js +1 -1
  4. package/dist/cli/commands/sim/dev.d.ts +12 -0
  5. package/dist/cli/commands/sim/dev.d.ts.map +1 -1
  6. package/dist/cli/commands/sim/dev.js +265 -139
  7. package/dist/cli/commands/sim/dev.js.map +1 -1
  8. package/dist/cli/commands/sim/profile.js +3 -1
  9. package/dist/cli/commands/sim/profile.js.map +1 -1
  10. package/dist/cli/commands/sim/test.js +1 -1
  11. package/dist/cli/commands/sim/test.js.map +1 -1
  12. package/dist/cli/commands/sim.d.ts +8 -0
  13. package/dist/cli/commands/sim.d.ts.map +1 -1
  14. package/dist/cli/commands/test.js +1 -1
  15. package/dist/cli/commands/test.js.map +1 -1
  16. package/dist/cli/lib/esptool.js +2 -2
  17. package/dist/cli/lib/firmware.d.ts.map +1 -1
  18. package/dist/cli/lib/firmware.js +56 -29
  19. package/dist/cli/lib/firmware.js.map +1 -1
  20. package/dist/cli/lib/serial/ReplConsole.js +1 -1
  21. package/dist/cli/lib/serial/ReplConsole.js.map +1 -1
  22. package/dist/cli/lib/serial/devSession.d.ts +4 -0
  23. package/dist/cli/lib/serial/devSession.d.ts.map +1 -1
  24. package/dist/cli/lib/serial/devSession.js +3 -3
  25. package/dist/cli/lib/serial/devSession.js.map +1 -1
  26. package/dist/cli/lib/serial/replStateMachine.d.ts.map +1 -1
  27. package/dist/cli/lib/serial/replStateMachine.js +9 -2
  28. package/dist/cli/lib/serial/replStateMachine.js.map +1 -1
  29. package/dist/cli/lib/simPid.d.ts.map +1 -1
  30. package/dist/cli/lib/simPid.js +12 -4
  31. package/dist/cli/lib/simPid.js.map +1 -1
  32. package/dist/simulator/builtins/ble.d.ts.map +1 -1
  33. package/dist/simulator/builtins/ble.js +11 -10
  34. package/dist/simulator/builtins/ble.js.map +1 -1
  35. package/dist/simulator/builtins/http.d.ts.map +1 -1
  36. package/dist/simulator/builtins/http.js +73 -15
  37. package/dist/simulator/builtins/http.js.map +1 -1
  38. package/dist/simulator/builtins/i2c.d.ts.map +1 -1
  39. package/dist/simulator/builtins/i2c.js +6 -5
  40. package/dist/simulator/builtins/i2c.js.map +1 -1
  41. package/dist/simulator/builtins/kv.d.ts.map +1 -1
  42. package/dist/simulator/builtins/kv.js +2 -1
  43. package/dist/simulator/builtins/kv.js.map +1 -1
  44. package/dist/simulator/builtins/neopixel.d.ts.map +1 -1
  45. package/dist/simulator/builtins/neopixel.js +7 -5
  46. package/dist/simulator/builtins/neopixel.js.map +1 -1
  47. package/dist/simulator/builtins/nvs-kv.d.ts.map +1 -1
  48. package/dist/simulator/builtins/nvs-kv.js +37 -2
  49. package/dist/simulator/builtins/nvs-kv.js.map +1 -1
  50. package/dist/simulator/builtins/pin.d.ts.map +1 -1
  51. package/dist/simulator/builtins/pin.js +5 -4
  52. package/dist/simulator/builtins/pin.js.map +1 -1
  53. package/dist/simulator/builtins/pwm.d.ts.map +1 -1
  54. package/dist/simulator/builtins/pwm.js +6 -4
  55. package/dist/simulator/builtins/pwm.js.map +1 -1
  56. package/dist/simulator/builtins/sleep.d.ts.map +1 -1
  57. package/dist/simulator/builtins/sleep.js +7 -6
  58. package/dist/simulator/builtins/sleep.js.map +1 -1
  59. package/dist/simulator/builtins/sntp.d.ts.map +1 -1
  60. package/dist/simulator/builtins/sntp.js +3 -1
  61. package/dist/simulator/builtins/sntp.js.map +1 -1
  62. package/dist/simulator/builtins/spi.d.ts.map +1 -1
  63. package/dist/simulator/builtins/spi.js +5 -4
  64. package/dist/simulator/builtins/spi.js.map +1 -1
  65. package/dist/simulator/builtins/uart.d.ts.map +1 -1
  66. package/dist/simulator/builtins/uart.js +6 -4
  67. package/dist/simulator/builtins/uart.js.map +1 -1
  68. package/dist/simulator/builtins/wifi.d.ts.map +1 -1
  69. package/dist/simulator/builtins/wifi.js +20 -19
  70. package/dist/simulator/builtins/wifi.js.map +1 -1
  71. package/dist/simulator/devRunner.d.ts.map +1 -1
  72. package/dist/simulator/devRunner.js +61 -0
  73. package/dist/simulator/devRunner.js.map +1 -1
  74. package/dist/simulator/simProcess.js +46 -52
  75. package/dist/simulator/simProcess.js.map +1 -1
  76. package/package.json +4 -4
@@ -4,6 +4,9 @@ import * as path from 'node:path';
4
4
  const cliPath = path.join(import.meta.dirname, '/cli.js');
5
5
  function run() {
6
6
  const child = fork(cliPath, process.argv.slice(2));
7
+ // Track whether we're tearing down to start a fresh child (reload) so we
8
+ // can suppress the wrapper's own exit on the dying child.
9
+ let restarting = false;
7
10
  const onMessage = (message) => {
8
11
  if (message === 'exit') {
9
12
  exit('SIGTERM', false);
@@ -13,6 +16,7 @@ function run() {
13
16
  }
14
17
  };
15
18
  function exit(signal, restart) {
19
+ restarting = restart;
16
20
  child.kill('SIGTERM');
17
21
  child.removeAllListeners('message');
18
22
  // eslint-disable-next-line no-console
@@ -21,6 +25,15 @@ function run() {
21
25
  run();
22
26
  }
23
27
  }
28
+ // Forward the child's exit code to the wrapper. Without this, a child that
29
+ // calls process.exit(1) (e.g. on a fatal CLI error like a missing env file)
30
+ // would leave the wrapper exiting 0, masking the failure from shell scripts
31
+ // and CI. Suppressed during reload so the new child's lifetime drives exit.
32
+ child.on('exit', (code, signal) => {
33
+ if (restarting)
34
+ return;
35
+ process.exit(code ?? (signal ? 1 : 0));
36
+ });
24
37
  child.on('message', onMessage);
25
38
  }
26
39
  run();
@@ -1 +1 @@
1
- {"version":3,"file":"cliWrapper.js","sourceRoot":"","sources":["../../src/cli/cliWrapper.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAA;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;AAEzD,SAAS,GAAG;IACV,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAElD,MAAM,SAAS,GAAG,CAAC,OAAe,EAAE,EAAE;QACpC,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QACxB,CAAC;QACD,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QACtB,CAAC;IACH,CAAC,CAAA;IACD,SAAS,IAAI,CAAC,MAA4B,EAAE,OAAgB;QAC1D,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACrB,KAAK,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAA;QACnC,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,cAAc,EAAE,CAAC,CAAA;QACnF,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,EAAE,CAAA;QACP,CAAC;IACH,CAAC;IACD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAChC,CAAC;AAED,GAAG,EAAE,CAAA"}
1
+ {"version":3,"file":"cliWrapper.js","sourceRoot":"","sources":["../../src/cli/cliWrapper.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAA;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;AAEzD,SAAS,GAAG;IACV,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAClD,yEAAyE;IACzE,0DAA0D;IAC1D,IAAI,UAAU,GAAG,KAAK,CAAA;IAEtB,MAAM,SAAS,GAAG,CAAC,OAAe,EAAE,EAAE;QACpC,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QACxB,CAAC;QACD,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QACtB,CAAC;IACH,CAAC,CAAA;IACD,SAAS,IAAI,CAAC,MAA4B,EAAE,OAAgB;QAC1D,UAAU,GAAG,OAAO,CAAA;QACpB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACrB,KAAK,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAA;QACnC,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,cAAc,EAAE,CAAC,CAAA;QACnF,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,EAAE,CAAA;QACP,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;QAChC,IAAI,UAAU;YAAE,OAAM;QACtB,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IACF,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAChC,CAAC;AAED,GAAG,EAAE,CAAA"}
@@ -157,7 +157,7 @@ export default function FlashCmd(props) {
157
157
  if (!hasPrebuiltFirmware(resolvedChip)) {
158
158
  throw new Error(`No bundled firmware for ${resolvedChip}. ` +
159
159
  `Build a custom firmware locally and flash with --build-dir, ` +
160
- `or fetch a CI artifact with --from=mikrojs/mikrojs:<sha>.`);
160
+ `or fetch a CI artifact with --from=mikrojs/mikrojs@<sha>.`);
161
161
  }
162
162
  const flasherArgs = await readFlasherArgs(prebuiltFirmwareDir(resolvedChip));
163
163
  setInitState({ status: 'ready', flasherArgs, esptoolPath });
@@ -1,35 +1,47 @@
1
1
  export declare const args: import("@optique/core").Parser<"sync", {
2
2
  readonly subcommand: "dev";
3
3
  readonly entry: string | undefined;
4
+ readonly forceDeploy: true | undefined;
4
5
  readonly noMinify: true | undefined;
5
6
  readonly minifier: string | undefined;
6
7
  readonly minifyLevel: string | undefined;
7
8
  readonly noBytecode: true | undefined;
9
+ readonly noHooks: true | undefined;
10
+ readonly logLevel: string | undefined;
8
11
  readonly env: string | undefined;
9
12
  readonly secrets: string | undefined;
10
13
  readonly noEnvFile: true | undefined;
14
+ readonly noWatch: true | undefined;
11
15
  readonly agent: true | undefined;
12
16
  }, ["matched", string] | ["parsing", {
13
17
  readonly subcommand: "dev";
14
18
  readonly entry: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
19
+ readonly forceDeploy: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
15
20
  readonly noMinify: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
16
21
  readonly minifier: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
17
22
  readonly minifyLevel: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
18
23
  readonly noBytecode: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
24
+ readonly noHooks: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
25
+ readonly logLevel: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
19
26
  readonly env: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
20
27
  readonly secrets: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
21
28
  readonly noEnvFile: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
29
+ readonly noWatch: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
22
30
  readonly agent: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
23
31
  }] | undefined>;
24
32
  interface DevConfig {
25
33
  entry?: string;
34
+ forceDeploy?: boolean;
26
35
  noMinify?: boolean;
27
36
  minifier?: string;
28
37
  minifyLevel?: string;
29
38
  noBytecode?: boolean;
39
+ noHooks?: boolean;
40
+ logLevel?: string;
30
41
  env?: string;
31
42
  secrets?: string;
32
43
  noEnvFile?: boolean;
44
+ noWatch?: boolean;
33
45
  agent?: boolean;
34
46
  }
35
47
  export declare function run(config: DevConfig): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/sim/dev.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;eAmChB,CAAA;AAED,UAAU,SAAS;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,wBAAsB,GAAG,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA+G1D;AAaD,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,KAAK,EAAE;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,2CAetD"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/sim/dev.tsx"],"names":[],"mappings":"AA8BA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAqDhB,CAAA;AAED,UAAU,SAAS;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AA4BD,wBAAsB,GAAG,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAgJ1D;AAQD,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,KAAK,EAAE;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,2CAEtD"}
@@ -6,24 +6,22 @@ import { argument, flag, option } from '@optique/core/primitives';
6
6
  import { string } from '@optique/core/valueparser';
7
7
  import { path } from '@optique/run';
8
8
  import spinners from 'cli-spinners';
9
- import { Text } from 'ink';
10
- import React from 'react';
11
- import { catchError, defer, EMPTY, exhaustMap, lastValueFrom } from 'rxjs';
12
- import { agentEmit, agentError, agentResult, createAgentStdinReader } from '../../lib/agent.js';
13
- import { build } from '../../lib/build.js';
14
- import { collectFiles, loadEnvFiles } from '../../lib/deploy.js';
9
+ import { Text, useApp, useInput } from 'ink';
10
+ import { useCallback, useEffect, useState } from 'react';
11
+ import { filter, firstValueFrom, map, Subject } from 'rxjs';
12
+ import { agentEmit, agentError, agentResult, createAgentStdinReader, forwardSessionToAgent, } from '../../lib/agent.js';
15
13
  import { openSim } from '../../lib/openSim.js';
16
- import { parseMinifier, parseMinifyLevel } from '../../lib/parseMinifier.js';
17
- import { getMikroDir, resolveProjectRoot } from '../../lib/projectRoot.js';
14
+ import { parseLogLevel, parseMinifier, parseMinifyLevel } from '../../lib/parseMinifier.js';
18
15
  import { resolveEntry } from '../../lib/resolveEntry.js';
19
- import { ReplConsole } from '../../lib/serial/ReplConsole.js';
16
+ import { createDevSession } from '../../lib/serial/devSession.js';
17
+ import { InkReplConsole } from '../../lib/serial/InkReplConsole.js';
20
18
  import { createRepl } from '../../lib/serial/replStateMachine.js';
21
19
  import { SimAlreadyRunningError } from '../../lib/simPid.js';
22
20
  import { Spinner } from '../../lib/Spinner.js';
23
- import { createWatcher } from '../../lib/watcher.js';
24
21
  export const args = command('dev', object({
25
22
  subcommand: constant('dev'),
26
23
  entry: optional(argument(path({ metavar: 'ENTRY', mustExist: true, type: 'file' }))),
24
+ forceDeploy: optional(flag('--force-deploy', { description: message `Force full deploy, ignoring cached checksums` })),
27
25
  noMinify: optional(flag('--no-minify', { description: message `Skip minification` })),
28
26
  minifier: optional(option('--minifier', string({ metavar: 'NAME' }), {
29
27
  description: message `Minifier: esbuild, terser, or swc (default: esbuild)`,
@@ -32,6 +30,12 @@ export const args = command('dev', object({
32
30
  description: message `Minify level: default or max`,
33
31
  })),
34
32
  noBytecode: optional(flag('--no-bytecode', { description: message `Skip bytecode compilation` })),
33
+ noHooks: optional(flag('--no-hooks', {
34
+ description: message `Skip mikrojs.predeploy hooks from package.json`,
35
+ })),
36
+ logLevel: optional(option('--loglevel', string({ metavar: 'LEVEL' }), {
37
+ description: message `Log level: none, error, warn, info, debug. Console calls below this level are eliminated at build time.`,
38
+ })),
35
39
  env: optional(option('--env', string({ metavar: 'FILE' }), {
36
40
  description: message `Path to .env file with environment variables`,
37
41
  })),
@@ -41,20 +45,43 @@ export const args = command('dev', object({
41
45
  noEnvFile: optional(flag('--no-env-file', {
42
46
  description: message `Skip auto-loading of .env and .env.simulator from the project root`,
43
47
  })),
48
+ noWatch: optional(flag('--no-watch', {
49
+ description: message `Deploy once, then drop into the REPL without watching for file changes`,
50
+ })),
44
51
  agent: optional(flag('--agent', { description: message `NDJSON agent protocol over stdio` })),
45
52
  }), { description: message `Watch + build + deploy to simulator with REPL` });
53
+ /**
54
+ * Resolve the user's CLI args into the shape `createDevSession` expects.
55
+ * `mode: 'simulator'` drives `MIKRO_ENV` and the auto-loaded `.env.simulator`
56
+ * file, distinguishing sim runs from `mikro dev` (which defaults to
57
+ * `'development'`).
58
+ */
59
+ function devSessionOptions(config) {
60
+ return {
61
+ entry: resolveEntry(config.entry),
62
+ forceDeploy: config.forceDeploy === true,
63
+ minify: !config.noMinify,
64
+ bytecode: !config.noBytecode,
65
+ watch: config.noWatch !== true,
66
+ noHooks: config.noHooks === true,
67
+ minifier: parseMinifier(config.minifier),
68
+ minifyLevel: parseMinifyLevel(config.minifyLevel),
69
+ logLevel: parseLogLevel(config.logLevel),
70
+ envFile: config.env,
71
+ secretsFile: config.secrets,
72
+ noEnvFile: config.noEnvFile === true,
73
+ mode: 'simulator',
74
+ };
75
+ }
76
+ // ── Agent mode ─────────────────────────────────────────────────────
46
77
  export async function run(config) {
47
- const entry = resolveEntry(config.entry);
48
- const buildDir = pathlib.join(getMikroDir(), 'build');
49
- const watchDir = process.cwd();
50
- const minify = !config.noMinify;
51
- const minifier = parseMinifier(config.minifier);
52
- const minifyLevel = parseMinifyLevel(config.minifyLevel);
53
- const bytecode = !config.noBytecode;
54
78
  let session;
55
79
  let transport;
56
80
  try {
57
- const conn = await openSim({ claim: true, startDir: pathlib.dirname(entry) });
81
+ const conn = await openSim({
82
+ claim: true,
83
+ startDir: pathlib.dirname(resolveEntry(config.entry)),
84
+ });
58
85
  session = conn.session;
59
86
  transport = conn.transport;
60
87
  }
@@ -65,59 +92,97 @@ export async function run(config) {
65
92
  }
66
93
  throw err;
67
94
  }
68
- session.messages$.subscribe((event) => {
69
- switch (event.type) {
70
- case 'ready':
71
- agentEmit({ type: 'ready', chip: event.chip ?? null, id: event.id ?? null });
95
+ forwardSessionToAgent(session);
96
+ const opts = devSessionOptions(config);
97
+ const deploys$ = new Subject();
98
+ const dev = createDevSession({
99
+ session,
100
+ entry: opts.entry,
101
+ forceDeploy: opts.forceDeploy,
102
+ minify: opts.minify,
103
+ bytecode: opts.bytecode,
104
+ watch: opts.watch,
105
+ noHooks: opts.noHooks,
106
+ minifier: opts.minifier,
107
+ minifyLevel: opts.minifyLevel,
108
+ logLevel: opts.logLevel,
109
+ envFile: opts.envFile,
110
+ secretsFile: opts.secretsFile,
111
+ noEnvFile: opts.noEnvFile,
112
+ externalDeploys$: deploys$.asObservable(),
113
+ mode: opts.mode,
114
+ });
115
+ // Translate DevSessionState transitions into agent NDJSON events. Mirrors
116
+ // mikro dev's run() so consumers (CI, scripts, AI agents) see the same
117
+ // event stream regardless of target.
118
+ const idleStatus = opts.watch ? 'watching' : 'idle';
119
+ let stateSub = dev.state$.subscribe((state) => {
120
+ switch (state.status.type) {
121
+ case 'checking':
122
+ agentEmit({ type: 'status', status: 'checking', command: state.status.command });
123
+ break;
124
+ case 'building':
125
+ case 'rebuilding':
126
+ agentEmit({ type: 'status', status: 'building' });
72
127
  break;
73
- case 'prompt':
74
- case 'disconnect':
128
+ case 'deploying': {
129
+ const event = state.status.event;
130
+ if (event.type === 'uploading' || event.type === 'checking') {
131
+ agentEmit({
132
+ type: `deploy_${event.type}`,
133
+ file: event.file,
134
+ index: event.index,
135
+ total: event.total,
136
+ });
137
+ }
138
+ else if (event.type === 'connecting') {
139
+ agentEmit({ type: 'status', status: 'deploying' });
140
+ }
141
+ break;
142
+ }
143
+ case 'watching':
144
+ agentEmit({ type: 'status', status: idleStatus });
75
145
  break;
76
- default:
77
- if ('text' in event)
78
- agentEmit({ type: event.type, text: event.text });
146
+ case 'error':
147
+ agentEmit({ type: 'status', status: 'error', error: state.status.message });
79
148
  break;
80
149
  }
81
150
  });
82
- async function doBuildAndDeploy() {
83
- agentEmit({ type: 'status', status: 'building' });
84
- await lastValueFrom(build(entry, buildDir, { minify, bytecode, minifier, minifyLevel }), {
85
- defaultValue: undefined,
86
- });
87
- const allFiles = await collectFiles(buildDir);
88
- const envVars = [
89
- { key: 'MIKRO_ENV', value: 'simulator', secret: false },
90
- ...(await loadEnvFiles({
91
- cwd: resolveProjectRoot(),
92
- mode: 'simulator',
93
- envFile: config.env,
94
- secretsFile: config.secrets,
95
- noEnvFile: config.noEnvFile === true,
96
- })),
97
- ];
98
- agentEmit({ type: 'status', status: 'deploying' });
99
- await lastValueFrom(session.deploy({ files: allFiles, envVars, restart: true }));
100
- agentEmit({ type: 'status', status: 'watching' });
101
- }
151
+ const dispose = () => {
152
+ stateSub?.unsubscribe();
153
+ stateSub = null;
154
+ dev.close();
155
+ try {
156
+ session.close();
157
+ }
158
+ catch {
159
+ // already closed
160
+ }
161
+ try {
162
+ transport.close();
163
+ }
164
+ catch {
165
+ // already closed
166
+ }
167
+ };
168
+ process.on('exit', dispose);
169
+ process.on('SIGINT', () => process.exit(0));
170
+ process.on('SIGTERM', () => process.exit(0));
171
+ // Await the first non-building state to detect initial-deploy failure.
172
+ // Match mikro dev's contract: error on the initial path is fatal, exits
173
+ // with non-zero so CI/scripts see a failure.
102
174
  try {
103
- await doBuildAndDeploy();
175
+ await firstValueFrom(dev.state$.pipe(filter((s) => s.status.type === 'watching' || s.status.type === 'error'), map((s) => {
176
+ if (s.status.type === 'error')
177
+ throw new Error(s.status.message);
178
+ return s;
179
+ })));
104
180
  }
105
181
  catch (err) {
106
182
  const msg = err instanceof Error ? err.message : String(err);
107
183
  agentError('sim dev', msg, { fix: 'Check entry file and try again' });
108
184
  process.exit(1);
109
185
  }
110
- process.on('exit', () => session.close());
111
- process.on('SIGINT', () => process.exit(0));
112
- process.on('SIGTERM', () => process.exit(0));
113
- const { changes$, close: closeWatcher } = createWatcher(watchDir);
114
- changes$
115
- .pipe(exhaustMap(() => defer(() => doBuildAndDeploy()).pipe(catchError((err) => {
116
- const msg = err instanceof Error ? err.message : String(err);
117
- agentEmit({ type: 'status', status: 'error', error: msg });
118
- return EMPTY;
119
- }))))
120
- .subscribe();
121
186
  for await (const cmd of createAgentStdinReader()) {
122
187
  switch (cmd.type) {
123
188
  case 'eval':
@@ -135,120 +200,181 @@ export async function run(config) {
135
200
  case 'restart':
136
201
  session.restart();
137
202
  break;
203
+ case 'deploy':
204
+ deploys$.next({ force: cmd.force === true });
205
+ break;
138
206
  case 'exit':
139
207
  session.exit();
140
- session.close();
141
- closeWatcher();
142
- transport.close();
208
+ dispose();
143
209
  agentResult('sim dev', null, []);
144
210
  process.exit(0);
145
211
  }
146
212
  }
213
+ // stdin closed without explicit 'exit' — clean shutdown.
214
+ dispose();
215
+ agentResult('sim dev', null, []);
216
+ process.exit(0);
147
217
  }
148
218
  export default function SimDev(props) {
149
- const { args: cfg } = props;
150
- const entry = resolveEntry(cfg.entry);
151
- return (_jsx(SimDevMode, { entry: entry, minify: !cfg.noMinify, bytecode: !cfg.noBytecode, minifier: parseMinifier(cfg.minifier), minifyLevel: parseMinifyLevel(cfg.minifyLevel), envFile: cfg.env, secretsFile: cfg.secrets, noEnvFile: cfg.noEnvFile === true }));
219
+ return _jsx(SimDevMode, { config: props.args });
152
220
  }
221
+ /**
222
+ * TUI counterpart of `run()`. Connects the simulator session, instantiates
223
+ * `createDevSession` (the same state machine `mikro dev` uses), and renders
224
+ * the REPL once the initial deploy completes.
225
+ *
226
+ * Initial-setup errors (sim subprocess fails to start, first build/deploy
227
+ * fails) are fatal — there's nothing for the watcher to fall back to.
228
+ * Watch-mode rebuild errors stay in the TUI so the user can fix the file
229
+ * and let the watcher retry.
230
+ */
153
231
  function SimDevMode(props) {
154
- const { entry, minify, bytecode, minifier, minifyLevel, envFile, secretsFile, noEnvFile } = props;
155
- const buildDir = pathlib.join(getMikroDir(), 'build');
156
- const watchDir = process.cwd();
157
- const replRef = React.useRef(null);
158
- const [repl, setRepl] = React.useState(null);
159
- const [error, setError] = React.useState(null);
160
- React.useEffect(() => {
232
+ const { config } = props;
233
+ const opts = devSessionOptions(config);
234
+ // We mount the REPL UI as soon as the sim session+repl handle exist, BEFORE
235
+ // the first deploy completes. This matches the device-side InkReplMode flow
236
+ // and is required for correctness: the REPL state machine's awaitReady$
237
+ // / connectionTimeout$ only fire when `repl.state$` has a subscriber. If
238
+ // the REPL UI doesn't mount until after the first deploy, the state
239
+ // machine never gets to poll CMD_HELLO during the deploy window and
240
+ // ends up timing out the moment it does finally mount (because by then
241
+ // multiple restarts have happened).
242
+ const [conn, setConn] = useState(null);
243
+ const [error, setError] = useState(null);
244
+ const fatal = useCallback((err) => {
245
+ const msg = err instanceof Error ? err.message : String(err);
246
+ process.stderr.write(`Error: ${msg}\n`);
247
+ process.exit(1);
248
+ }, []);
249
+ const handleEnd = useCallback(() => {
250
+ process.stdout.write('\x1b[<u');
251
+ process.exit(0);
252
+ }, []);
253
+ // Ink's `exitOnCtrlC` is disabled at the cli.ts render call; until the REPL
254
+ // is mounted (or while an error screen is showing), nothing else handles
255
+ // input — without this, ctrl-c is a black hole. Matches DevicePicker /
256
+ // InkReplMode / deploy convention (ctrl-c or ctrl-q).
257
+ const { exit: exitInk } = useApp();
258
+ useInput((input, key) => {
259
+ if (key.ctrl && (input === 'c' || input === 'q')) {
260
+ exitInk();
261
+ process.exit(error ? 1 : 0);
262
+ }
263
+ }, { isActive: !conn });
264
+ useEffect(() => {
161
265
  let disposed = false;
162
266
  let cleanup = null;
163
267
  void (async () => {
164
268
  let session;
165
269
  let transport;
166
270
  try {
167
- const conn = await openSim({ claim: true, startDir: pathlib.dirname(entry) });
168
- session = conn.session;
169
- transport = conn.transport;
271
+ const handle = await openSim({
272
+ claim: true,
273
+ startDir: pathlib.dirname(opts.entry),
274
+ });
275
+ session = handle.session;
276
+ transport = handle.transport;
170
277
  }
171
278
  catch (err) {
172
279
  if (!disposed)
173
- setError(err instanceof Error ? err : new Error(String(err)));
280
+ fatal(err);
174
281
  return;
175
282
  }
176
- async function buildAndDeploy() {
177
- if (disposed)
178
- return;
179
- if (replRef.current)
180
- replRef.current.setDisabled(true);
181
- setError(null);
182
- const envVars = [
183
- { key: 'MIKRO_ENV', value: 'simulator', secret: false },
184
- ...(await loadEnvFiles({
185
- cwd: resolveProjectRoot(),
186
- mode: 'simulator',
187
- envFile,
188
- secretsFile,
189
- noEnvFile,
190
- })),
191
- ];
192
- await lastValueFrom(build(entry, buildDir, { minify, bytecode, minifier, minifyLevel }), {
193
- defaultValue: undefined,
194
- });
195
- if (disposed)
196
- return;
197
- const allFiles = await collectFiles(buildDir);
198
- const handle = createRepl({
199
- session,
200
- port: 'simulator',
201
- onEnd: () => process.exit(0),
202
- });
203
- handle.state$.subscribe();
204
- await lastValueFrom(session.deploy({ files: allFiles, envVars, restart: true }));
205
- if (disposed)
206
- return;
207
- replRef.current = handle;
208
- if (!disposed)
209
- setRepl({ handle, config: session.config });
283
+ if (disposed) {
284
+ try {
285
+ session.close();
286
+ transport.close();
287
+ }
288
+ catch {
289
+ // already closed
290
+ }
291
+ return;
210
292
  }
211
- buildAndDeploy().catch((err) => {
212
- if (!disposed)
213
- setError(err instanceof Error ? err : new Error(String(err)));
293
+ const repl = createRepl({
294
+ session,
295
+ port: 'simulator',
296
+ onEnd: handleEnd,
297
+ deployEnabled: true,
298
+ });
299
+ const dev = createDevSession({
300
+ session,
301
+ repl,
302
+ entry: opts.entry,
303
+ forceDeploy: opts.forceDeploy,
304
+ minify: opts.minify,
305
+ bytecode: opts.bytecode,
306
+ watch: opts.watch,
307
+ noHooks: opts.noHooks,
308
+ minifier: opts.minifier,
309
+ minifyLevel: opts.minifyLevel,
310
+ logLevel: opts.logLevel,
311
+ envFile: opts.envFile,
312
+ secretsFile: opts.secretsFile,
313
+ noEnvFile: opts.noEnvFile,
314
+ mode: opts.mode,
315
+ });
316
+ // Mount the REPL UI immediately so the state machine starts polling
317
+ // CMD_HELLO and the `connectionTimeout$` can be cancelled by the first
318
+ // MSG_READY. createDevSession's run is kept alive by the stateSub
319
+ // subscription below; rendering the REPL is purely a UI concern.
320
+ setConn({ session, transport, repl, dev });
321
+ const stateSub = dev.state$.subscribe((state) => {
322
+ if (state.status.type === 'error') {
323
+ // Surface in TUI; the watcher can recover by re-running on next
324
+ // file change, so we keep the session alive.
325
+ if (!disposed)
326
+ setError(new Error(state.status.message));
327
+ }
328
+ else if (state.status.type === 'watching' || state.status.type === 'deploying') {
329
+ if (!disposed)
330
+ setError(null);
331
+ }
214
332
  });
215
- const { changes$, close } = createWatcher(watchDir);
216
- const sub = changes$
217
- .pipe(exhaustMap(() => defer(() => buildAndDeploy()).pipe(catchError((err) => {
218
- if (!disposed)
219
- setError(err instanceof Error ? err : new Error(String(err)));
220
- return EMPTY;
221
- }))))
222
- .subscribe();
223
333
  cleanup = () => {
224
- sub.unsubscribe();
225
- close();
226
- session.close();
227
- transport.close();
334
+ stateSub.unsubscribe();
335
+ dev.close();
336
+ try {
337
+ session.close();
338
+ }
339
+ catch {
340
+ // already closed
341
+ }
342
+ try {
343
+ transport.close();
344
+ }
345
+ catch {
346
+ // already closed
347
+ }
228
348
  };
229
349
  })();
230
350
  return () => {
231
351
  disposed = true;
232
352
  cleanup?.();
233
353
  };
354
+ // opts is recomputed each render but its values are stable for a given
355
+ // config (which itself is stable for a session). Deps captured so React
356
+ // doesn't complain; effective re-runs only happen on real config change.
357
+ // eslint-disable-next-line react-hooks/exhaustive-deps
234
358
  }, [
235
- entry,
236
- buildDir,
237
- minify,
238
- bytecode,
239
- minifier,
240
- minifyLevel,
241
- envFile,
242
- secretsFile,
243
- noEnvFile,
244
- watchDir,
359
+ opts.entry,
360
+ opts.forceDeploy,
361
+ opts.minify,
362
+ opts.bytecode,
363
+ opts.watch,
364
+ opts.noHooks,
365
+ opts.minifier,
366
+ opts.minifyLevel,
367
+ opts.logLevel,
368
+ opts.envFile,
369
+ opts.secretsFile,
370
+ opts.noEnvFile,
245
371
  ]);
246
- if (error) {
372
+ if (error && !conn) {
247
373
  return _jsxs(Text, { color: "red", children: ["Error: ", error.message] });
248
374
  }
249
- if (!repl) {
250
- return (_jsxs(Text, { children: [_jsx(Spinner, { spinner: spinners.dots }), " Building\u2026"] }));
375
+ if (!conn) {
376
+ return (_jsxs(Text, { children: [_jsx(Spinner, { spinner: spinners.dots }), " Starting simulator\u2026"] }));
251
377
  }
252
- return _jsx(ReplConsole, { repl: repl.handle, config: repl.config });
378
+ return (_jsx(InkReplConsole, { session: conn.session, devicePath: "simulator", logLevel: opts.logLevel, repl: conn.repl, watch: opts.watch }));
253
379
  }
254
380
  //# sourceMappingURL=dev.js.map