@ratio-app/cli 0.1.1

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 (182) hide show
  1. package/README.md +504 -0
  2. package/bin/run.mjs +5 -0
  3. package/dist/commands/__test__/throw.d.ts +25 -0
  4. package/dist/commands/__test__/throw.js +238 -0
  5. package/dist/commands/__test__/throw.js.map +1 -0
  6. package/dist/commands/app/create.d.ts +55 -0
  7. package/dist/commands/app/create.js +609 -0
  8. package/dist/commands/app/create.js.map +1 -0
  9. package/dist/commands/app/deploy.d.ts +14 -0
  10. package/dist/commands/app/deploy.js +179 -0
  11. package/dist/commands/app/deploy.js.map +1 -0
  12. package/dist/commands/app/dev.d.ts +28 -0
  13. package/dist/commands/app/dev.js +701 -0
  14. package/dist/commands/app/dev.js.map +1 -0
  15. package/dist/commands/app/generate.d.ts +14 -0
  16. package/dist/commands/app/generate.js +179 -0
  17. package/dist/commands/app/generate.js.map +1 -0
  18. package/dist/commands/app/info.d.ts +14 -0
  19. package/dist/commands/app/info.js +179 -0
  20. package/dist/commands/app/info.js.map +1 -0
  21. package/dist/commands/app/link.d.ts +118 -0
  22. package/dist/commands/app/link.js +1120 -0
  23. package/dist/commands/app/link.js.map +1 -0
  24. package/dist/commands/auth/login.d.ts +43 -0
  25. package/dist/commands/auth/login.js +987 -0
  26. package/dist/commands/auth/login.js.map +1 -0
  27. package/dist/commands/auth/logout.d.ts +15 -0
  28. package/dist/commands/auth/logout.js +486 -0
  29. package/dist/commands/auth/logout.js.map +1 -0
  30. package/dist/commands/auth/whoami.d.ts +16 -0
  31. package/dist/commands/auth/whoami.js +531 -0
  32. package/dist/commands/auth/whoami.js.map +1 -0
  33. package/dist/commands/dev/listen.d.ts +84 -0
  34. package/dist/commands/dev/listen.js +1187 -0
  35. package/dist/commands/dev/listen.js.map +1 -0
  36. package/dist/commands/dev/trigger.d.ts +88 -0
  37. package/dist/commands/dev/trigger.js +729 -0
  38. package/dist/commands/dev/trigger.js.map +1 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.js +617 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
  43. package/dist/lib/auth/authorize-code-exchange.js +114 -0
  44. package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
  45. package/dist/lib/auth/authorize-url.d.ts +42 -0
  46. package/dist/lib/auth/authorize-url.js +35 -0
  47. package/dist/lib/auth/authorize-url.js.map +1 -0
  48. package/dist/lib/auth/browser.d.ts +16 -0
  49. package/dist/lib/auth/browser.js +48 -0
  50. package/dist/lib/auth/browser.js.map +1 -0
  51. package/dist/lib/auth/config.d.ts +41 -0
  52. package/dist/lib/auth/config.js +21 -0
  53. package/dist/lib/auth/config.js.map +1 -0
  54. package/dist/lib/auth/http-refresher.d.ts +30 -0
  55. package/dist/lib/auth/http-refresher.js +103 -0
  56. package/dist/lib/auth/http-refresher.js.map +1 -0
  57. package/dist/lib/auth/identity.d.ts +26 -0
  58. package/dist/lib/auth/identity.js +10 -0
  59. package/dist/lib/auth/identity.js.map +1 -0
  60. package/dist/lib/auth/index.d.ts +13 -0
  61. package/dist/lib/auth/index.js +527 -0
  62. package/dist/lib/auth/index.js.map +1 -0
  63. package/dist/lib/auth/loopback-server.d.ts +65 -0
  64. package/dist/lib/auth/loopback-server.js +245 -0
  65. package/dist/lib/auth/loopback-server.js.map +1 -0
  66. package/dist/lib/auth/manual-prompt.d.ts +17 -0
  67. package/dist/lib/auth/manual-prompt.js +20 -0
  68. package/dist/lib/auth/manual-prompt.js.map +1 -0
  69. package/dist/lib/auth/pkce.d.ts +24 -0
  70. package/dist/lib/auth/pkce.js +15 -0
  71. package/dist/lib/auth/pkce.js.map +1 -0
  72. package/dist/lib/auth/state.d.ts +20 -0
  73. package/dist/lib/auth/state.js +19 -0
  74. package/dist/lib/auth/state.js.map +1 -0
  75. package/dist/lib/auth/verbose-trace.d.ts +25 -0
  76. package/dist/lib/auth/verbose-trace.js +17 -0
  77. package/dist/lib/auth/verbose-trace.js.map +1 -0
  78. package/dist/lib/credentials/clock.d.ts +18 -0
  79. package/dist/lib/credentials/clock.js +10 -0
  80. package/dist/lib/credentials/clock.js.map +1 -0
  81. package/dist/lib/credentials/index.d.ts +5 -0
  82. package/dist/lib/credentials/index.js +340 -0
  83. package/dist/lib/credentials/index.js.map +1 -0
  84. package/dist/lib/credentials/path.d.ts +26 -0
  85. package/dist/lib/credentials/path.js +32 -0
  86. package/dist/lib/credentials/path.js.map +1 -0
  87. package/dist/lib/credentials/refresher.d.ts +26 -0
  88. package/dist/lib/credentials/refresher.js +34 -0
  89. package/dist/lib/credentials/refresher.js.map +1 -0
  90. package/dist/lib/credentials/schema.d.ts +138 -0
  91. package/dist/lib/credentials/schema.js +85 -0
  92. package/dist/lib/credentials/schema.js.map +1 -0
  93. package/dist/lib/credentials/store.d.ts +102 -0
  94. package/dist/lib/credentials/store.js +337 -0
  95. package/dist/lib/credentials/store.js.map +1 -0
  96. package/dist/lib/credentials/types.d.ts +65 -0
  97. package/dist/lib/credentials/types.js +1 -0
  98. package/dist/lib/credentials/types.js.map +1 -0
  99. package/dist/lib/dev-store-gate.d.ts +66 -0
  100. package/dist/lib/dev-store-gate.js +15 -0
  101. package/dist/lib/dev-store-gate.js.map +1 -0
  102. package/dist/lib/errors.d.ts +60 -0
  103. package/dist/lib/errors.js +101 -0
  104. package/dist/lib/errors.js.map +1 -0
  105. package/dist/lib/manifest/index.d.ts +2 -0
  106. package/dist/lib/manifest/index.js +144 -0
  107. package/dist/lib/manifest/index.js.map +1 -0
  108. package/dist/lib/manifest/read-manifest.d.ts +32 -0
  109. package/dist/lib/manifest/read-manifest.js +87 -0
  110. package/dist/lib/manifest/read-manifest.js.map +1 -0
  111. package/dist/lib/manifest/write-client-id.d.ts +33 -0
  112. package/dist/lib/manifest/write-client-id.js +94 -0
  113. package/dist/lib/manifest/write-client-id.js.map +1 -0
  114. package/dist/lib/messages.d.ts +15 -0
  115. package/dist/lib/messages.js +16 -0
  116. package/dist/lib/messages.js.map +1 -0
  117. package/dist/lib/orchestrator/child-runner.d.ts +140 -0
  118. package/dist/lib/orchestrator/child-runner.js +471 -0
  119. package/dist/lib/orchestrator/child-runner.js.map +1 -0
  120. package/dist/lib/preview/index.d.ts +11 -0
  121. package/dist/lib/preview/index.js +17 -0
  122. package/dist/lib/preview/index.js.map +1 -0
  123. package/dist/lib/preview/types.d.ts +16 -0
  124. package/dist/lib/preview/types.js +11 -0
  125. package/dist/lib/preview/types.js.map +1 -0
  126. package/dist/lib/ratio-command.d.ts +52 -0
  127. package/dist/lib/ratio-command.js +141 -0
  128. package/dist/lib/ratio-command.js.map +1 -0
  129. package/dist/lib/relay/index.d.ts +7 -0
  130. package/dist/lib/relay/index.js +362 -0
  131. package/dist/lib/relay/index.js.map +1 -0
  132. package/dist/lib/relay/relay-client.d.ts +91 -0
  133. package/dist/lib/relay/relay-client.js +362 -0
  134. package/dist/lib/relay/relay-client.js.map +1 -0
  135. package/dist/lib/relay/types.d.ts +71 -0
  136. package/dist/lib/relay/types.js +1 -0
  137. package/dist/lib/relay/types.js.map +1 -0
  138. package/dist/lib/render-error.d.ts +35 -0
  139. package/dist/lib/render-error.js +115 -0
  140. package/dist/lib/render-error.js.map +1 -0
  141. package/dist/lib/verbose-flag.d.ts +12 -0
  142. package/dist/lib/verbose-flag.js +17 -0
  143. package/dist/lib/verbose-flag.js.map +1 -0
  144. package/dist/render-CrHGqTPH.d.ts +115 -0
  145. package/dist/templates/.gitkeep +0 -0
  146. package/dist/templates/minimal/.env.example.tmpl +4 -0
  147. package/dist/templates/minimal/README.md.tmpl +26 -0
  148. package/dist/templates/minimal/gitignore +5 -0
  149. package/dist/templates/minimal/manifest.json +5 -0
  150. package/dist/templates/minimal/package.json.tmpl +25 -0
  151. package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
  152. package/dist/templates/minimal/src/index.ts.tmpl +26 -0
  153. package/dist/templates/minimal/src/install.ts.tmpl +37 -0
  154. package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
  155. package/dist/templates/minimal/tsconfig.json +17 -0
  156. package/dist/templates/serverless/README.md.tmpl +28 -0
  157. package/dist/templates/serverless/gitignore +6 -0
  158. package/dist/templates/serverless/manifest.json +5 -0
  159. package/dist/templates/serverless/package.json.tmpl +22 -0
  160. package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
  161. package/dist/templates/serverless/src/index.ts.tmpl +20 -0
  162. package/dist/templates/serverless/src/install.ts.tmpl +20 -0
  163. package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
  164. package/dist/templates/serverless/tsconfig.json +18 -0
  165. package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
  166. package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
  167. package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
  168. package/dist/templates/with-admin-ui/gitignore +6 -0
  169. package/dist/templates/with-admin-ui/manifest.json +5 -0
  170. package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
  171. package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
  172. package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
  173. package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
  174. package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
  175. package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
  176. package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
  177. package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
  178. package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
  179. package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
  180. package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
  181. package/dist/templates/with-admin-ui/tsconfig.json +17 -0
  182. package/package.json +78 -0
@@ -0,0 +1,87 @@
1
+ // src/lib/manifest/read-manifest.ts
2
+ import fs2 from "fs";
3
+ import path2 from "path";
4
+
5
+ // src/lib/errors.ts
6
+ var RatioCLIError = class extends Error {
7
+ code;
8
+ exitCode;
9
+ hint;
10
+ docsUrl;
11
+ constructor(opts) {
12
+ super(opts.message);
13
+ this.name = new.target.name;
14
+ this.code = opts.code;
15
+ this.exitCode = opts.exitCode;
16
+ if (opts.hint !== void 0) this.hint = opts.hint;
17
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
18
+ }
19
+ };
20
+ var ValidationError = class extends RatioCLIError {
21
+ constructor(message, hint) {
22
+ const opts = {
23
+ code: "VALIDATION_ERROR",
24
+ exitCode: 8,
25
+ message
26
+ };
27
+ if (hint !== void 0) opts.hint = hint;
28
+ super(opts);
29
+ }
30
+ };
31
+
32
+ // src/lib/manifest/write-client-id.ts
33
+ import crypto from "crypto";
34
+ import fs from "fs";
35
+ import path from "path";
36
+ var MANIFEST_FILE_NAME = "ratio.config.jsonc";
37
+
38
+ // src/lib/manifest/read-manifest.ts
39
+ var DEV_COMMAND_ROW = /^\s*"devCommand"\s*:\s*"([^"]*)"/m;
40
+ var DEV_SERVER_PORT_ROW = /^\s*"devServerPort"\s*:\s*(\d+)/m;
41
+ var DEV_SERVER_PORT_KEY = /^\s*"devServerPort"\s*:/m;
42
+ var APP_NAME_ROW = /^\s*"appName"\s*:\s*"([^"]*)"/m;
43
+ var CLIENT_ID_READ_ROW = /^\s*"clientId"\s*:\s*"([^"]*)"/m;
44
+ var DEV_COMMAND_HINT = 'Add a "devCommand" entry to ratio.config.jsonc (e.g. "npm run dev") and re-run.';
45
+ function readManifest(cwd) {
46
+ const filePath = path2.join(cwd, MANIFEST_FILE_NAME);
47
+ let content;
48
+ try {
49
+ content = fs2.readFileSync(filePath, "utf8");
50
+ } catch {
51
+ throw new ValidationError(
52
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
53
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
54
+ );
55
+ }
56
+ const devCommand = DEV_COMMAND_ROW.exec(content)?.[1];
57
+ if (devCommand === void 0 || devCommand.trim() === "") {
58
+ throw new ValidationError(
59
+ `${MANIFEST_FILE_NAME} has no usable "devCommand" entry.`,
60
+ DEV_COMMAND_HINT
61
+ );
62
+ }
63
+ let devServerPort;
64
+ if (DEV_SERVER_PORT_KEY.test(content)) {
65
+ const rawPort = DEV_SERVER_PORT_ROW.exec(content)?.[1];
66
+ const port = rawPort === void 0 ? Number.NaN : Number.parseInt(rawPort, 10);
67
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
68
+ throw new ValidationError(
69
+ `${MANIFEST_FILE_NAME} has an invalid "devServerPort" value.`,
70
+ '"devServerPort" must be an unquoted integer between 1 and 65535 (e.g. "devServerPort": 3001).'
71
+ );
72
+ }
73
+ devServerPort = port;
74
+ }
75
+ return {
76
+ path: filePath,
77
+ devCommand,
78
+ devServerPort,
79
+ appName: APP_NAME_ROW.exec(content)?.[1],
80
+ clientId: CLIENT_ID_READ_ROW.exec(content)?.[1]
81
+ };
82
+ }
83
+ export {
84
+ DEV_COMMAND_HINT,
85
+ readManifest
86
+ };
87
+ //# sourceMappingURL=read-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/manifest/read-manifest.ts","../../../src/lib/errors.ts","../../../src/lib/manifest/write-client-id.ts"],"sourcesContent":["/**\n * Read-only view over the app manifest (ratio.config.jsonc).\n *\n * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is\n * involved, so comments and unknown keys are irrelevant to the read and\n * survive untouched on disk. Each field is extracted with one targeted\n * regex, mirroring the write-side idiom in write-client-id.ts. This module\n * never writes the file.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { ValidationError } from '../errors.js';\nimport { MANIFEST_FILE_NAME } from './write-client-id.js';\n\n/** Read-only extraction regex for the dev command line. */\nconst DEV_COMMAND_ROW = /^\\s*\"devCommand\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Read-only extraction regex for the dev-server port (integer, unquoted). */\nconst DEV_SERVER_PORT_ROW = /^\\s*\"devServerPort\"\\s*:\\s*(\\d+)/m;\n\n/** Key-presence probe: distinguishes \"absent\" from \"present but malformed\". */\nconst DEV_SERVER_PORT_KEY = /^\\s*\"devServerPort\"\\s*:/m;\n\n/** Read-only extraction regex for the appName echo. Never used to write. */\nconst APP_NAME_ROW = /^\\s*\"appName\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Read-only extraction regex for the clientId echo. Never used to write. */\nconst CLIENT_ID_READ_ROW = /^\\s*\"clientId\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Hint shown whenever the dev command entry is missing or unusable. */\nexport const DEV_COMMAND_HINT =\n 'Add a \"devCommand\" entry to ratio.config.jsonc (e.g. \"npm run dev\") and re-run.';\n\nexport interface ManifestView {\n /** Absolute path to the manifest file that was read. */\n path: string;\n /** The command used to start the app's own dev server. Always present. */\n devCommand: string;\n /** Optional local dev-server port (1-65535); undefined when absent. */\n devServerPort: number | undefined;\n /** Parsed from the manifest's appName row, when present. */\n appName: string | undefined;\n /** Parsed from the manifest's clientId row, when present. */\n clientId: string | undefined;\n}\n\n/**\n * Locate `ratio.config.jsonc` under `cwd` and extract the fields the dev\n * orchestrator needs. Fails fast with a typed error when the file is\n * missing, when `devCommand` is absent/empty, or when `devServerPort` is\n * present but not an integer in 1-65535.\n */\nexport function readManifest(cwd: string): ManifestView {\n const filePath = path.join(cwd, MANIFEST_FILE_NAME);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n );\n }\n\n const devCommand = DEV_COMMAND_ROW.exec(content)?.[1];\n if (devCommand === undefined || devCommand.trim() === '') {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has no usable \"devCommand\" entry.`,\n DEV_COMMAND_HINT,\n );\n }\n\n let devServerPort: number | undefined;\n if (DEV_SERVER_PORT_KEY.test(content)) {\n const rawPort = DEV_SERVER_PORT_ROW.exec(content)?.[1];\n const port = rawPort === undefined ? Number.NaN : Number.parseInt(rawPort, 10);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has an invalid \"devServerPort\" value.`,\n '\"devServerPort\" must be an unquoted integer between 1 and 65535 (e.g. \"devServerPort\": 3001).',\n );\n }\n devServerPort = port;\n }\n\n return {\n path: filePath,\n devCommand,\n devServerPort,\n appName: APP_NAME_ROW.exec(content)?.[1],\n clientId: CLIENT_ID_READ_ROW.exec(content)?.[1],\n };\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n","/**\n * Atomically rewrite the top-level \"clientId\" value in a ratio.config.jsonc\n * file. Preserves every comment, whitespace token, and unknown key.\n *\n * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is\n * involved, so comments survive verbatim. Exactly one targeted regex is\n * applied to the `\"clientId\": \"<value>\"` line; everything else is copied\n * byte-for-byte.\n *\n * Throws ValidationError if the file is not shaped like a canonical\n * ratio.config.jsonc (no top-level \"clientId\" key on its own line).\n */\n\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { ValidationError } from '../errors.js';\n\n/** Manifest file name expected in the app repo root. */\nexport const MANIFEST_FILE_NAME = 'ratio.config.jsonc';\n\n/** Targeted single-key rewrite regex for the clientId value. */\nconst CLIENT_ID_ROW = /^(\\s*\"clientId\"\\s*:\\s*\")([^\"]*)(\"\\s*,?)/m;\n\n/** Read-only extraction regex for the appName echo. Never used to write. */\nconst APP_NAME_ROW = /^\\s*\"appName\"\\s*:\\s*\"([^\"]*)\"/m;\n\nexport interface WriteClientIdResult {\n /** false when the file already had the same clientId. */\n written: boolean;\n /** Absolute path to the manifest file. */\n path: string;\n /** Parsed from the manifest's appName row, when present. */\n appName?: string;\n /** The value that was replaced, when it was non-empty and different. */\n previousClientId?: string;\n}\n\n/**\n * Locate `ratio.config.jsonc` under `cwd`, rewrite ONLY the top-level\n * clientId value, and persist via atomic tmp+rename with 0600 discipline\n * (mirrors the credential store's write contract). Same-value re-runs are\n * a no-op (`written: false`, file untouched).\n */\nexport async function writeClientId(\n cwd: string,\n clientId: string,\n): Promise<WriteClientIdResult> {\n const filePath = path.join(cwd, MANIFEST_FILE_NAME);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n );\n }\n\n const appNameMatch = APP_NAME_ROW.exec(content);\n const appName = appNameMatch?.[1];\n\n const rowMatch = CLIENT_ID_ROW.exec(content);\n if (rowMatch === null) {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has no top-level \"clientId\" entry.`,\n 'Restore the \"clientId\" line from the app template, then re-run.',\n );\n }\n\n const existing = rowMatch[2] ?? '';\n\n const base: WriteClientIdResult = {\n written: false,\n path: filePath,\n ...(appName !== undefined ? { appName } : {}),\n };\n\n if (existing === clientId) {\n return base;\n }\n\n // Function replacer: the new value is inserted verbatim, immune to `$`\n // pattern expansion in String.replace replacement strings.\n const updated = content.replace(\n CLIENT_ID_ROW,\n (_row, open: string, _old: string, close: string) =>\n `${open}${clientId}${close}`,\n );\n\n const tmpPath = `${filePath}.${process.pid}.${crypto\n .randomBytes(8)\n .toString('hex')}.tmp`;\n try {\n // mode at open time closes the create-then-chmod race window.\n fs.writeFileSync(tmpPath, updated, { mode: 0o600 });\n fs.renameSync(tmpPath, filePath);\n } catch (err) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n // Best-effort cleanup: the tmp file may never have been created.\n }\n throw err;\n }\n if (process.platform !== 'win32') {\n // Belt-and-braces: rename may preserve a prior destination mode.\n fs.chmodSync(filePath, 0o600);\n }\n\n return {\n ...base,\n written: true,\n ...(existing !== '' ? { previousClientId: existing } : {}),\n };\n}\n"],"mappings":";AAUA,OAAOA,SAAQ;AACf,OAAOC,WAAU;;;ACDV,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAMT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO,WAAW;AACvB,SAAK,OAAO,KAAK;AACjB,SAAK,WAAW,KAAK;AACrB,QAAI,KAAK,SAAS,OAAW,MAAK,OAAO,KAAK;AAC9C,QAAI,KAAK,YAAY,OAAW,MAAK,UAAU,KAAK;AAAA,EACtD;AACF;AAgEO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EACjD,YAAY,SAAiB,MAAe;AAC1C,UAAM,OAA2E;AAAA,MAC/E,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AACA,QAAI,SAAS,OAAW,MAAK,OAAO;AACpC,UAAM,IAAI;AAAA,EACZ;AACF;;;AC3FA,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;AAIV,IAAM,qBAAqB;;;AFHlC,IAAM,kBAAkB;AAGxB,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,eAAe;AAGrB,IAAM,qBAAqB;AAGpB,IAAM,mBACX;AAqBK,SAAS,aAAa,KAA2B;AACtD,QAAM,WAAWC,MAAK,KAAK,KAAK,kBAAkB;AAElD,MAAI;AACJ,MAAI;AACF,cAAUC,IAAG,aAAa,UAAU,MAAM;AAAA,EAC5C,QAAQ;AACN,UAAM,IAAI;AAAA,MACR,GAAG,kBAAkB,iBAAiB,GAAG;AAAA,MACzC,8CAA8C,kBAAkB,iBAAiB,GAAG;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,aAAa,gBAAgB,KAAK,OAAO,IAAI,CAAC;AACpD,MAAI,eAAe,UAAa,WAAW,KAAK,MAAM,IAAI;AACxD,UAAM,IAAI;AAAA,MACR,GAAG,kBAAkB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,oBAAoB,KAAK,OAAO,GAAG;AACrC,UAAM,UAAU,oBAAoB,KAAK,OAAO,IAAI,CAAC;AACrD,UAAM,OAAO,YAAY,SAAY,OAAO,MAAM,OAAO,SAAS,SAAS,EAAE;AAC7E,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAO;AACvD,YAAM,IAAI;AAAA,QACR,GAAG,kBAAkB;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AACA,oBAAgB;AAAA,EAClB;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,aAAa,KAAK,OAAO,IAAI,CAAC;AAAA,IACvC,UAAU,mBAAmB,KAAK,OAAO,IAAI,CAAC;AAAA,EAChD;AACF;","names":["fs","path","path","fs"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Atomically rewrite the top-level "clientId" value in a ratio.config.jsonc
3
+ * file. Preserves every comment, whitespace token, and unknown key.
4
+ *
5
+ * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is
6
+ * involved, so comments survive verbatim. Exactly one targeted regex is
7
+ * applied to the `"clientId": "<value>"` line; everything else is copied
8
+ * byte-for-byte.
9
+ *
10
+ * Throws ValidationError if the file is not shaped like a canonical
11
+ * ratio.config.jsonc (no top-level "clientId" key on its own line).
12
+ */
13
+ /** Manifest file name expected in the app repo root. */
14
+ declare const MANIFEST_FILE_NAME = "ratio.config.jsonc";
15
+ interface WriteClientIdResult {
16
+ /** false when the file already had the same clientId. */
17
+ written: boolean;
18
+ /** Absolute path to the manifest file. */
19
+ path: string;
20
+ /** Parsed from the manifest's appName row, when present. */
21
+ appName?: string;
22
+ /** The value that was replaced, when it was non-empty and different. */
23
+ previousClientId?: string;
24
+ }
25
+ /**
26
+ * Locate `ratio.config.jsonc` under `cwd`, rewrite ONLY the top-level
27
+ * clientId value, and persist via atomic tmp+rename with 0600 discipline
28
+ * (mirrors the credential store's write contract). Same-value re-runs are
29
+ * a no-op (`written: false`, file untouched).
30
+ */
31
+ declare function writeClientId(cwd: string, clientId: string): Promise<WriteClientIdResult>;
32
+
33
+ export { MANIFEST_FILE_NAME, type WriteClientIdResult, writeClientId };
@@ -0,0 +1,94 @@
1
+ // src/lib/manifest/write-client-id.ts
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ // src/lib/errors.ts
7
+ var RatioCLIError = class extends Error {
8
+ code;
9
+ exitCode;
10
+ hint;
11
+ docsUrl;
12
+ constructor(opts) {
13
+ super(opts.message);
14
+ this.name = new.target.name;
15
+ this.code = opts.code;
16
+ this.exitCode = opts.exitCode;
17
+ if (opts.hint !== void 0) this.hint = opts.hint;
18
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
19
+ }
20
+ };
21
+ var ValidationError = class extends RatioCLIError {
22
+ constructor(message, hint) {
23
+ const opts = {
24
+ code: "VALIDATION_ERROR",
25
+ exitCode: 8,
26
+ message
27
+ };
28
+ if (hint !== void 0) opts.hint = hint;
29
+ super(opts);
30
+ }
31
+ };
32
+
33
+ // src/lib/manifest/write-client-id.ts
34
+ var MANIFEST_FILE_NAME = "ratio.config.jsonc";
35
+ var CLIENT_ID_ROW = /^(\s*"clientId"\s*:\s*")([^"]*)("\s*,?)/m;
36
+ var APP_NAME_ROW = /^\s*"appName"\s*:\s*"([^"]*)"/m;
37
+ async function writeClientId(cwd, clientId) {
38
+ const filePath = path.join(cwd, MANIFEST_FILE_NAME);
39
+ let content;
40
+ try {
41
+ content = fs.readFileSync(filePath, "utf8");
42
+ } catch {
43
+ throw new ValidationError(
44
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
45
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
46
+ );
47
+ }
48
+ const appNameMatch = APP_NAME_ROW.exec(content);
49
+ const appName = appNameMatch?.[1];
50
+ const rowMatch = CLIENT_ID_ROW.exec(content);
51
+ if (rowMatch === null) {
52
+ throw new ValidationError(
53
+ `${MANIFEST_FILE_NAME} has no top-level "clientId" entry.`,
54
+ 'Restore the "clientId" line from the app template, then re-run.'
55
+ );
56
+ }
57
+ const existing = rowMatch[2] ?? "";
58
+ const base = {
59
+ written: false,
60
+ path: filePath,
61
+ ...appName !== void 0 ? { appName } : {}
62
+ };
63
+ if (existing === clientId) {
64
+ return base;
65
+ }
66
+ const updated = content.replace(
67
+ CLIENT_ID_ROW,
68
+ (_row, open, _old, close) => `${open}${clientId}${close}`
69
+ );
70
+ const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
71
+ try {
72
+ fs.writeFileSync(tmpPath, updated, { mode: 384 });
73
+ fs.renameSync(tmpPath, filePath);
74
+ } catch (err) {
75
+ try {
76
+ fs.unlinkSync(tmpPath);
77
+ } catch {
78
+ }
79
+ throw err;
80
+ }
81
+ if (process.platform !== "win32") {
82
+ fs.chmodSync(filePath, 384);
83
+ }
84
+ return {
85
+ ...base,
86
+ written: true,
87
+ ...existing !== "" ? { previousClientId: existing } : {}
88
+ };
89
+ }
90
+ export {
91
+ MANIFEST_FILE_NAME,
92
+ writeClientId
93
+ };
94
+ //# sourceMappingURL=write-client-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/manifest/write-client-id.ts","../../../src/lib/errors.ts"],"sourcesContent":["/**\n * Atomically rewrite the top-level \"clientId\" value in a ratio.config.jsonc\n * file. Preserves every comment, whitespace token, and unknown key.\n *\n * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is\n * involved, so comments survive verbatim. Exactly one targeted regex is\n * applied to the `\"clientId\": \"<value>\"` line; everything else is copied\n * byte-for-byte.\n *\n * Throws ValidationError if the file is not shaped like a canonical\n * ratio.config.jsonc (no top-level \"clientId\" key on its own line).\n */\n\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { ValidationError } from '../errors.js';\n\n/** Manifest file name expected in the app repo root. */\nexport const MANIFEST_FILE_NAME = 'ratio.config.jsonc';\n\n/** Targeted single-key rewrite regex for the clientId value. */\nconst CLIENT_ID_ROW = /^(\\s*\"clientId\"\\s*:\\s*\")([^\"]*)(\"\\s*,?)/m;\n\n/** Read-only extraction regex for the appName echo. Never used to write. */\nconst APP_NAME_ROW = /^\\s*\"appName\"\\s*:\\s*\"([^\"]*)\"/m;\n\nexport interface WriteClientIdResult {\n /** false when the file already had the same clientId. */\n written: boolean;\n /** Absolute path to the manifest file. */\n path: string;\n /** Parsed from the manifest's appName row, when present. */\n appName?: string;\n /** The value that was replaced, when it was non-empty and different. */\n previousClientId?: string;\n}\n\n/**\n * Locate `ratio.config.jsonc` under `cwd`, rewrite ONLY the top-level\n * clientId value, and persist via atomic tmp+rename with 0600 discipline\n * (mirrors the credential store's write contract). Same-value re-runs are\n * a no-op (`written: false`, file untouched).\n */\nexport async function writeClientId(\n cwd: string,\n clientId: string,\n): Promise<WriteClientIdResult> {\n const filePath = path.join(cwd, MANIFEST_FILE_NAME);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n );\n }\n\n const appNameMatch = APP_NAME_ROW.exec(content);\n const appName = appNameMatch?.[1];\n\n const rowMatch = CLIENT_ID_ROW.exec(content);\n if (rowMatch === null) {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has no top-level \"clientId\" entry.`,\n 'Restore the \"clientId\" line from the app template, then re-run.',\n );\n }\n\n const existing = rowMatch[2] ?? '';\n\n const base: WriteClientIdResult = {\n written: false,\n path: filePath,\n ...(appName !== undefined ? { appName } : {}),\n };\n\n if (existing === clientId) {\n return base;\n }\n\n // Function replacer: the new value is inserted verbatim, immune to `$`\n // pattern expansion in String.replace replacement strings.\n const updated = content.replace(\n CLIENT_ID_ROW,\n (_row, open: string, _old: string, close: string) =>\n `${open}${clientId}${close}`,\n );\n\n const tmpPath = `${filePath}.${process.pid}.${crypto\n .randomBytes(8)\n .toString('hex')}.tmp`;\n try {\n // mode at open time closes the create-then-chmod race window.\n fs.writeFileSync(tmpPath, updated, { mode: 0o600 });\n fs.renameSync(tmpPath, filePath);\n } catch (err) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n // Best-effort cleanup: the tmp file may never have been created.\n }\n throw err;\n }\n if (process.platform !== 'win32') {\n // Belt-and-braces: rename may preserve a prior destination mode.\n fs.chmodSync(filePath, 0o600);\n }\n\n return {\n ...base,\n written: true,\n ...(existing !== '' ? { previousClientId: existing } : {}),\n };\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n"],"mappings":";AAaA,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;;;ACLV,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAMT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO,WAAW;AACvB,SAAK,OAAO,KAAK;AACjB,SAAK,WAAW,KAAK;AACrB,QAAI,KAAK,SAAS,OAAW,MAAK,OAAO,KAAK;AAC9C,QAAI,KAAK,YAAY,OAAW,MAAK,UAAU,KAAK;AAAA,EACtD;AACF;AAgEO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EACjD,YAAY,SAAiB,MAAe;AAC1C,UAAM,OAA2E;AAAA,MAC/E,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AACA,QAAI,SAAS,OAAW,MAAK,OAAO;AACpC,UAAM,IAAI;AAAA,EACZ;AACF;;;ADrFO,IAAM,qBAAqB;AAGlC,IAAM,gBAAgB;AAGtB,IAAM,eAAe;AAmBrB,eAAsB,cACpB,KACA,UAC8B;AAC9B,QAAM,WAAW,KAAK,KAAK,KAAK,kBAAkB;AAElD,MAAI;AACJ,MAAI;AACF,cAAU,GAAG,aAAa,UAAU,MAAM;AAAA,EAC5C,QAAQ;AACN,UAAM,IAAI;AAAA,MACR,GAAG,kBAAkB,iBAAiB,GAAG;AAAA,MACzC,8CAA8C,kBAAkB,iBAAiB,GAAG;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,eAAe,aAAa,KAAK,OAAO;AAC9C,QAAM,UAAU,eAAe,CAAC;AAEhC,QAAM,WAAW,cAAc,KAAK,OAAO;AAC3C,MAAI,aAAa,MAAM;AACrB,UAAM,IAAI;AAAA,MACR,GAAG,kBAAkB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,SAAS,CAAC,KAAK;AAEhC,QAAM,OAA4B;AAAA,IAChC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7C;AAEA,MAAI,aAAa,UAAU;AACzB,WAAO;AAAA,EACT;AAIA,QAAM,UAAU,QAAQ;AAAA,IACtB;AAAA,IACA,CAAC,MAAM,MAAc,MAAc,UACjC,GAAG,IAAI,GAAG,QAAQ,GAAG,KAAK;AAAA,EAC9B;AAEA,QAAM,UAAU,GAAG,QAAQ,IAAI,QAAQ,GAAG,IAAI,OAC3C,YAAY,CAAC,EACb,SAAS,KAAK,CAAC;AAClB,MAAI;AAEF,OAAG,cAAc,SAAS,SAAS,EAAE,MAAM,IAAM,CAAC;AAClD,OAAG,WAAW,SAAS,QAAQ;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI;AACF,SAAG,WAAW,OAAO;AAAA,IACvB,QAAQ;AAAA,IAER;AACA,UAAM;AAAA,EACR;AACA,MAAI,QAAQ,aAAa,SAAS;AAEhC,OAAG,UAAU,UAAU,GAAK;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS;AAAA,IACT,GAAI,aAAa,KAAK,EAAE,kBAAkB,SAAS,IAAI,CAAC;AAAA,EAC1D;AACF;","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * User-facing placeholder-message helpers used by every command
3
+ * whose real behaviour is not yet available. Kept in one module so
4
+ * wording stays consistent and a future edit is one place.
5
+ */
6
+ /** Message for command classes registered as "Coming soon" stubs. */
7
+ declare function comingSoonMessage(command: string): string;
8
+ /**
9
+ * Message for real command surfaces whose behaviour is being built.
10
+ * @param command display name, e.g. 'ratio auth login'
11
+ * @param hint one short sentence naming what's being finalised
12
+ */
13
+ declare function notYetAvailableMessage(command: string, hint: string): string;
14
+
15
+ export { comingSoonMessage, notYetAvailableMessage };
@@ -0,0 +1,16 @@
1
+ // src/lib/messages.ts
2
+ function comingSoonMessage(command) {
3
+ return `${command} \u2014 coming soon.
4
+ This command will be available in an upcoming Ratio CLI release.
5
+ Run 'ratio --help' after your next update to see what's new.`;
6
+ }
7
+ function notYetAvailableMessage(command, hint) {
8
+ return `${command} \u2014 not yet available.
9
+ ${hint}
10
+ Run 'ratio --help' after your next update to see when it lands.`;
11
+ }
12
+ export {
13
+ comingSoonMessage,
14
+ notYetAvailableMessage
15
+ };
16
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/messages.ts"],"sourcesContent":["/**\n * User-facing placeholder-message helpers used by every command\n * whose real behaviour is not yet available. Kept in one module so\n * wording stays consistent and a future edit is one place.\n */\n\n/** Message for command classes registered as \"Coming soon\" stubs. */\nexport function comingSoonMessage(command: string): string {\n return (\n `${command} — coming soon.\\n` +\n `This command will be available in an upcoming Ratio CLI release.\\n` +\n `Run 'ratio --help' after your next update to see what's new.`\n );\n}\n\n/**\n * Message for real command surfaces whose behaviour is being built.\n * @param command display name, e.g. 'ratio auth login'\n * @param hint one short sentence naming what's being finalised\n */\nexport function notYetAvailableMessage(command: string, hint: string): string {\n return (\n `${command} — not yet available.\\n` +\n `${hint}\\n` +\n `Run 'ratio --help' after your next update to see when it lands.`\n );\n}\n"],"mappings":";AAOO,SAAS,kBAAkB,SAAyB;AACzD,SACE,GAAG,OAAO;AAAA;AAAA;AAId;AAOO,SAAS,uBAAuB,SAAiB,MAAsB;AAC5E,SACE,GAAG,OAAO;AAAA,EACP,IAAI;AAAA;AAGX;","names":[]}
@@ -0,0 +1,140 @@
1
+ import { PreviewController } from '../preview/types.js';
2
+
3
+ /**
4
+ * Child-process runner for `ratio app dev`.
5
+ *
6
+ * Spawns two children — the app's own dev command and the webhook
7
+ * listener — multiplexes their output onto stdout with per-child line
8
+ * prefixes, and arbitrates a single deterministic exit code out of child
9
+ * crashes and delivery of SIGINT/SIGTERM to the orchestrator.
10
+ *
11
+ * Everything process-shaped is injectable (spawn functions, signal
12
+ * source, output sinks, hard-exit) so the full lifecycle is testable
13
+ * without real dev servers or real signals.
14
+ */
15
+
16
+ /** SIGTERM → SIGKILL grace window during shutdown. */
17
+ declare const DRAIN_MS = 5000;
18
+ /**
19
+ * Window within which "both children crashed" is arbitrated to the
20
+ * first-observed exit. The arbiter is deterministic for every ordering
21
+ * (first observed terminating cause always wins), which satisfies the
22
+ * race rule for this window and beyond.
23
+ */
24
+ declare const RACE_WINDOW_MS = 200;
25
+ /** A second interrupt inside this window forces an immediate exit (130). */
26
+ declare const SIGNAL_FORCE_EXIT_WINDOW_MS = 2000;
27
+ /** Per-child line-buffer bound; overflow flushes a partial line. */
28
+ declare const CHILD_BUFFER_MAX_BYTES: number;
29
+ /** Hint shown when the dev command cannot be tokenized safely. */
30
+ declare const DEV_COMMAND_TOKENIZE_HINT = "devCommand must be a simple whitespace-separated command; complex shell pipelines are not supported.";
31
+ /** Default forwarded-to port for the listener when the manifest has none. */
32
+ declare const DEFAULT_FORWARD_PORT = 3001;
33
+ /**
34
+ * Split a dev command string into an argv array: whitespace-separated,
35
+ * with single- or double-quoted segments kept together. Shell operators
36
+ * (pipes, redirects, substitutions) are rejected outright — the command
37
+ * is spawned without a shell, so they could never mean what the author
38
+ * intended.
39
+ */
40
+ declare function tokenizeDevCommand(raw: string): string[];
41
+ /**
42
+ * Per-stream line buffer that emits `<prefix><line>\n` only on newline
43
+ * boundaries, so chunks split mid-line (or mid-UTF-8-character) never
44
+ * corrupt the prefixed output. Bounded to CHILD_BUFFER_MAX_BYTES: on
45
+ * overflow, the partial line is flushed with the prefix and a newline.
46
+ */
47
+ declare class LinePrefixer {
48
+ private readonly prefix;
49
+ private readonly write;
50
+ private buffer;
51
+ constructor(prefix: string, write: (chunk: string) => void);
52
+ push(chunk: Buffer | string): void;
53
+ /** Emit any buffered partial line (with prefix + newline), then reset. */
54
+ flush(): void;
55
+ private drainCompleteLines;
56
+ }
57
+ /** Map a child's (code, signal) exit pair to a single numeric exit code. */
58
+ declare function exitCodeFrom(code: number | null, signal: NodeJS.Signals | null): number;
59
+ /**
60
+ * Deterministic exit-code arbiter: the FIRST observed terminating cause
61
+ * (a child exit, or a signal delivered to the orchestrator) decides the
62
+ * final code. Exits observed after shutdown started are the expected
63
+ * result of our own SIGTERM and never change the outcome.
64
+ */
65
+ declare class ExitArbiter {
66
+ private decidingChildCode;
67
+ private signalCode;
68
+ get shutdownInitiated(): boolean;
69
+ observeChildExit(code: number | null, signal: NodeJS.Signals | null): void;
70
+ observeSignal(sig: 'SIGINT' | 'SIGTERM'): void;
71
+ finalCode(): number;
72
+ }
73
+ /** The exact spawn options the orchestrator uses for both children. */
74
+ interface SpawnChildOptions {
75
+ cwd: string;
76
+ env: NodeJS.ProcessEnv;
77
+ stdio: ['ignore', 'pipe', 'pipe'];
78
+ shell: false;
79
+ }
80
+ /** Structural subset of ChildProcess the orchestrator relies on. */
81
+ interface ChildLike {
82
+ readonly pid?: number | undefined;
83
+ readonly stdout: NodeJS.ReadableStream | null;
84
+ readonly stderr: NodeJS.ReadableStream | null;
85
+ kill(signal?: NodeJS.Signals): boolean;
86
+ once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
87
+ once(event: 'error', listener: (err: Error) => void): unknown;
88
+ }
89
+ type SpawnFn = (command: string, args: readonly string[], options: SpawnChildOptions) => ChildLike;
90
+ /** Minimal process-signal surface, injectable so tests never send real signals. */
91
+ interface SignalSource {
92
+ on(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
93
+ removeListener(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
94
+ }
95
+ /** How the listener child is invoked: always the current node executable. */
96
+ interface ListenInvocation {
97
+ command: string;
98
+ args: string[];
99
+ }
100
+ /**
101
+ * Build the listener child's invocation. By default the listener runs as
102
+ * a sub-command of this same binary. The RATIO_DEV_LISTEN_BIN environment
103
+ * variable substitutes a different script for the listener (a test seam;
104
+ * unset in normal use, in which case the default binary path is used).
105
+ */
106
+ declare function buildListenInvocation(env: NodeJS.ProcessEnv, devServerPort: number | undefined, argv1: string | undefined): ListenInvocation;
107
+ interface OrchestratorIo {
108
+ writeOut(chunk: string): void;
109
+ writeErr(chunk: string): void;
110
+ }
111
+ interface OrchestrateOptions {
112
+ /** Tokenized dev command; index 0 is the executable. */
113
+ appArgv: string[];
114
+ /** Optional port hint exposed to the app child as PORT. */
115
+ devServerPort: number | undefined;
116
+ cwd: string;
117
+ env: NodeJS.ProcessEnv;
118
+ preview: PreviewController;
119
+ verbose: boolean;
120
+ io: OrchestratorIo;
121
+ signalSource: SignalSource;
122
+ /** Immediate process exit for the second-interrupt fast path. */
123
+ hardExit: (code: number) => void;
124
+ /** Test seam: spawner for the app child. */
125
+ spawnApp?: SpawnFn;
126
+ /** Test seam: spawner for the listener child. */
127
+ spawnListen?: SpawnFn;
128
+ /** Test seam: shrink the SIGTERM → SIGKILL grace window. */
129
+ drainMs?: number;
130
+ /** Test seam: stand-in for the CLI's own script path. */
131
+ argv1?: string;
132
+ }
133
+ /**
134
+ * Run both children to completion and resolve the final exit code.
135
+ * Rejects with a typed error when a child cannot be spawned (exit 10) or
136
+ * when an otherwise-clean shutdown needed a SIGKILL escalation (exit 11).
137
+ */
138
+ declare function orchestrate(options: OrchestrateOptions): Promise<number>;
139
+
140
+ export { CHILD_BUFFER_MAX_BYTES, type ChildLike, DEFAULT_FORWARD_PORT, DEV_COMMAND_TOKENIZE_HINT, DRAIN_MS, ExitArbiter, LinePrefixer, type ListenInvocation, type OrchestrateOptions, type OrchestratorIo, RACE_WINDOW_MS, SIGNAL_FORCE_EXIT_WINDOW_MS, type SignalSource, type SpawnChildOptions, type SpawnFn, buildListenInvocation, exitCodeFrom, orchestrate, tokenizeDevCommand };