@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 @@
1
+ {"version":3,"sources":["../../../src/lib/credentials/store.ts","../../../src/lib/errors.ts","../../../src/lib/credentials/clock.ts","../../../src/lib/credentials/path.ts","../../../src/lib/credentials/refresher.ts","../../../src/lib/credentials/schema.ts"],"sourcesContent":["/**\n * File-backed credential store.\n *\n * Token resolution precedence (highest first):\n * 1. `RATIO_API_TOKEN` from the injected env reader — returned as-is\n * with ZERO filesystem access and ZERO refresher involvement.\n * 2. The requested profile (default: \"default\") from the credentials\n * file, silently refreshed via the injected TokenRefresher when it\n * is within 5 minutes of expiry.\n *\n * Write contract: atomic tmp + rename in the SAME directory, tmp created\n * with mode 0600 at open time (POSIX), parent directory created 0700,\n * belt-and-braces chmod 0600 after rename. On Windows no chmod / mode\n * checks are ever performed. Concurrent writers are last-writer-wins by\n * design — the atomic rename guarantees no half-written file, which is\n * sufficient for a single-developer CLI.\n *\n * No secret (access token, refresh token, raw file contents) is ever\n * included in an error message or a log line.\n */\n\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport { dirname } from 'node:path';\nimport {\n NotLoggedInError,\n TokenExpiredError,\n ValidationError,\n} from '../errors.js';\nimport { SystemClock } from './clock.js';\nimport type { Clock } from './clock.js';\nimport { defaultCredentialsPath } from './path.js';\nimport { NotImplementedRefresher } from './refresher.js';\nimport type { TokenRefresher } from './refresher.js';\nimport { parseCredentials } from './schema.js';\nimport type { Credentials, DeveloperSession, Profile, TokenSet } from './types.js';\n\n/** Milliseconds before expiry at which a silent refresh is triggered. */\nconst REFRESH_WINDOW_MS = 300_000;\n\n/** Env var that short-circuits all file access when set to a non-empty string. */\nconst ENV_TOKEN_VAR = 'RATIO_API_TOKEN';\n\n/**\n * Env var that short-circuits developer-session file access when set to a\n * non-empty string. Strictly separate from RATIO_API_TOKEN: this one carries\n * the developer sign-in bearer used for app-management calls, never the\n * OAuth access token.\n */\nconst ENV_DEVELOPER_TOKEN_VAR = 'RATIO_DEVELOPER_TOKEN';\n\nexport interface CredentialStoreOptions {\n /** Defaults to defaultCredentialsPath(). */\n readonly filePath?: string;\n /** Defaults to new SystemClock(). */\n readonly clock?: Clock;\n /** Defaults to new NotImplementedRefresher(). */\n readonly refresher?: TokenRefresher;\n /** Defaults to () => process.env — injected for env-precedence tests. */\n readonly envReader?: () => Readonly<Record<string, string | undefined>>;\n /** Defaults to process.platform. */\n readonly platform?: NodeJS.Platform;\n}\n\nexport interface CredentialStore {\n /** Reads the full credentials file. Throws NotLoggedInError if missing. */\n load(): Promise<Credentials>;\n /** Atomic write with 0600 on POSIX. tmp + rename in same dir. */\n save(creds: Credentials): Promise<void>;\n /** Deletes the credentials file. Idempotent. */\n clear(): Promise<void>;\n /**\n * Env precedence: if envReader().RATIO_API_TOKEN is set, returns it\n * WITHOUT reading the file, WITHOUT touching the refresher, WITHOUT\n * any fs I/O. This is the auditable \"no disk access\" path.\n *\n * Otherwise: reads the file, validates, computes\n * msUntilExpiry = new Date(profile.expiresAt).getTime() - clock.now()\n * If msUntilExpiry < 300_000 (5 min), calls refresher.refresh(...),\n * persists the new TokenSet atomically, returns the new accessToken.\n * Otherwise returns profile.accessToken.\n */\n getValidAccessToken(profileName?: string): Promise<string>;\n /**\n * Env precedence: RATIO_DEVELOPER_TOKEN if set (no fs I/O).\n * Otherwise: load(); if creds.developer is absent or within 5 minutes of\n * expiry, throws NotLoggedInError ('Developer signin required.').\n * Otherwise returns creds.developer.jwt.\n * NO refresh attempt — the caller drives the re-prompt UX.\n */\n getValidDeveloperToken(): Promise<string>;\n /**\n * Merge a DeveloperSession into the loaded credentials (creating the file\n * if absent), atomic tmp+rename, 0600.\n */\n saveDeveloperSession(session: DeveloperSession): Promise<void>;\n}\n\n/** Narrow an unknown thrown value to a Node errno error with the given code. */\nfunction hasErrnoCode(err: unknown, code: string): boolean {\n return (\n err instanceof Error &&\n (err as NodeJS.ErrnoException).code === code\n );\n}\n\nexport class FileCredentialStore implements CredentialStore {\n private readonly filePath: string;\n private readonly clock: Clock;\n private readonly refresher: TokenRefresher;\n private readonly envReader: () => Readonly<\n Record<string, string | undefined>\n >;\n private readonly platform: NodeJS.Platform;\n\n constructor(opts?: CredentialStoreOptions) {\n this.filePath = opts?.filePath ?? defaultCredentialsPath();\n this.clock = opts?.clock ?? new SystemClock();\n this.refresher = opts?.refresher ?? new NotImplementedRefresher();\n this.envReader = opts?.envReader ?? (() => process.env);\n this.platform = opts?.platform ?? process.platform;\n }\n\n async load(): Promise<Credentials> {\n this.reapplyFileModeIfDrifted();\n\n let raw: string;\n try {\n raw = fs.readFileSync(this.filePath, 'utf8');\n } catch (err) {\n if (hasErrnoCode(err, 'ENOENT')) {\n throw new NotLoggedInError('You are not signed in.');\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Never echo the raw contents — they may hold a truncated token.\n throw new ValidationError(\n 'credentials file is not valid JSON — run \"ratio auth login\" to reset',\n );\n }\n\n return parseCredentials(parsed);\n }\n\n async save(creds: Credentials): Promise<void> {\n const dir = dirname(this.filePath);\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n if (this.platform !== 'win32') {\n // mkdirSync does not re-mode a pre-existing directory; re-apply.\n fs.chmodSync(dir, 0o700);\n }\n\n const tmpPath = `${this.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, JSON.stringify(creds, null, 2), {\n mode: 0o600,\n });\n fs.renameSync(tmpPath, this.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\n if (this.platform !== 'win32') {\n // Belt-and-braces: rename may preserve a prior destination mode.\n fs.chmodSync(this.filePath, 0o600);\n }\n }\n\n async clear(): Promise<void> {\n try {\n fs.unlinkSync(this.filePath);\n } catch (err) {\n if (hasErrnoCode(err, 'ENOENT')) {\n return; // Idempotent: already gone.\n }\n throw err;\n }\n }\n\n async getValidAccessToken(profileName?: string): Promise<string> {\n const envToken = this.envReader()[ENV_TOKEN_VAR];\n if (typeof envToken === 'string' && envToken.length > 0) {\n return envToken;\n }\n\n const creds = await this.load();\n const name = profileName ?? 'default';\n const profile = creds.profiles[name];\n if (profile === undefined) {\n throw new NotLoggedInError('You are not signed in.');\n }\n\n const msUntilExpiry =\n new Date(profile.expiresAt).getTime() - this.clock.now();\n if (msUntilExpiry < REFRESH_WINDOW_MS) {\n return this.refreshAndPersist(creds, name, profile);\n }\n\n return profile.accessToken;\n }\n\n async getValidDeveloperToken(): Promise<string> {\n const envToken = this.envReader()[ENV_DEVELOPER_TOKEN_VAR];\n if (typeof envToken === 'string' && envToken.length > 0) {\n return envToken;\n }\n\n let creds: Credentials;\n try {\n creds = await this.load();\n } catch (err) {\n if (err instanceof NotLoggedInError) {\n throw new NotLoggedInError('Developer signin required.');\n }\n throw err;\n }\n\n const session = creds.developer;\n if (session === undefined) {\n throw new NotLoggedInError('Developer signin required.');\n }\n\n const msUntilExpiry =\n new Date(session.expiresAt).getTime() - this.clock.now();\n if (msUntilExpiry < REFRESH_WINDOW_MS) {\n // No refresh endpoint exists for developer sessions — the caller\n // drives a fresh interactive sign-in instead.\n throw new NotLoggedInError('Developer signin required.');\n }\n\n return session.jwt;\n }\n\n async saveDeveloperSession(session: DeveloperSession): Promise<void> {\n let creds: Credentials;\n try {\n creds = await this.load();\n } catch (err) {\n if (err instanceof NotLoggedInError) {\n // Edge case: no prior credentials file. Create a fresh v1 shell so\n // the merged write below still validates on the next load().\n creds = { version: 1, server: '', clientId: '', profiles: {} };\n } else {\n throw err;\n }\n }\n\n const updated: Credentials = {\n ...creds,\n developer: session,\n };\n await this.save(updated);\n }\n\n /**\n * Exchange the profile's refresh token for a fresh token set, merge it\n * into the loaded credentials (preserving every other profile), persist\n * atomically, and return the new access token. A refresher failure is\n * wrapped in TokenExpiredError; the underlying message is deliberately\n * discarded so a secret can never leak through the error surface.\n */\n private async refreshAndPersist(\n creds: Credentials,\n name: string,\n profile: Profile,\n ): Promise<string> {\n let refreshed: TokenSet;\n try {\n refreshed = await this.refresher.refresh(profile.refreshToken);\n } catch {\n throw new TokenExpiredError(\n 'Your session has expired and could not be refreshed.',\n );\n }\n\n const updated: Credentials = {\n ...creds,\n profiles: {\n ...creds.profiles,\n [name]: {\n ...profile,\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken,\n expiresAt: refreshed.expiresAt,\n },\n },\n };\n await this.save(updated);\n return refreshed.accessToken;\n }\n\n /**\n * POSIX-only startup permission check: if the file exists with a mode\n * other than 0600, re-apply 0600 and emit a warning to stderr. Never\n * runs on Windows (stat modes there do not map to POSIX bits and chmod\n * must not be called). Missing file is NOT an error here — load()\n * reports it as NotLoggedInError.\n */\n private reapplyFileModeIfDrifted(): void {\n if (this.platform === 'win32') {\n return;\n }\n\n let stat: fs.Stats;\n try {\n stat = fs.statSync(this.filePath);\n } catch (err) {\n if (hasErrnoCode(err, 'ENOENT')) {\n return; // load() turns the missing file into NotLoggedInError.\n }\n throw err;\n }\n\n const mode = stat.mode & 0o777;\n if (mode !== 0o600) {\n fs.chmodSync(this.filePath, 0o600);\n process.stderr.write(\n `[warn] credentials file permissions were 0o${mode.toString(8)}; corrected to 0600.\\n`,\n );\n }\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 * Injectable wall-clock seam for the credential store.\n *\n * This is the ONLY file in the credentials module allowed to sample the\n * real wall clock. Every other file receives a {@link Clock} instance so\n * tests can drive expiry-window logic deterministically with a fake.\n */\n\n/** Injectable time source. */\nexport interface Clock {\n /** Milliseconds since Unix epoch. */\n now(): number;\n}\n\n/** Production {@link Clock} backed by the system wall clock. */\nexport class SystemClock implements Clock {\n now(): number {\n return Date.now();\n }\n}\n","/**\n * Pure path resolution for the credentials file.\n *\n * POSIX: `$XDG_CONFIG_HOME/ratio/credentials` when the override is a\n * non-empty string, otherwise `~/.config/ratio/credentials`.\n * Windows: `%APPDATA%\\ratio\\credentials`, falling back to\n * `<home>\\AppData\\Roaming\\ratio\\credentials` when APPDATA is unset.\n *\n * `path.win32.join` / `path.posix.join` are used explicitly (never bare\n * `path.join`) so the result is deterministic when a test running on a\n * POSIX host fakes `platform === 'win32'`.\n */\n\nimport os from 'node:os';\nimport path from 'node:path';\n\n/** The subset of environment variables the path algorithm consults. */\nexport interface PathEnv {\n readonly XDG_CONFIG_HOME?: string;\n readonly APPDATA?: string;\n}\n\n/** Snapshot the two relevant variables from the real process env. */\nfunction snapshotPathEnv(): PathEnv {\n const snapshot: { XDG_CONFIG_HOME?: string; APPDATA?: string } = {};\n const xdgConfigHome = process.env['XDG_CONFIG_HOME'];\n if (xdgConfigHome !== undefined) {\n snapshot.XDG_CONFIG_HOME = xdgConfigHome;\n }\n const appData = process.env['APPDATA'];\n if (appData !== undefined) {\n snapshot.APPDATA = appData;\n }\n return snapshot;\n}\n\n/**\n * Pure function. Returns the on-disk path for the credentials file.\n * platform defaults to process.platform, homedir defaults to os.homedir(),\n * env defaults to a snapshot of {XDG_CONFIG_HOME, APPDATA} from process.env.\n * All three are injectable so unit tests fake Windows without a Windows host.\n */\nexport function defaultCredentialsPath(\n platform?: NodeJS.Platform,\n homedir?: string,\n env?: PathEnv,\n): string {\n const resolvedPlatform = platform ?? process.platform;\n const resolvedHome = homedir ?? os.homedir();\n const resolvedEnv = env ?? snapshotPathEnv();\n\n if (resolvedPlatform === 'win32') {\n const appData = resolvedEnv.APPDATA;\n const base =\n appData !== undefined && appData !== ''\n ? appData\n : path.win32.join(resolvedHome, 'AppData', 'Roaming');\n return path.win32.join(base, 'ratio', 'credentials');\n }\n\n const xdgConfigHome = resolvedEnv.XDG_CONFIG_HOME;\n const base =\n typeof xdgConfigHome === 'string' && xdgConfigHome !== ''\n ? xdgConfigHome\n : path.posix.join(resolvedHome, '.config');\n return path.posix.join(base, 'ratio', 'credentials');\n}\n","/**\n * Injectable token-refresh seam for the credential store.\n *\n * The store never performs network I/O itself: when a stored token is\n * close to expiry the store calls the injected {@link TokenRefresher}\n * and persists whatever it returns. The concrete HTTP implementation is\n * wired in by the auth commands; this module only defines the seam plus\n * a throw-if-called placeholder used before sign-in support is wired.\n */\n\nimport { NotLoggedInError } from '../errors.js';\nimport type { TokenSet } from './types.js';\n\n/** Injectable seam that exchanges a refresh token for a fresh token set. */\nexport interface TokenRefresher {\n refresh(refreshToken: string): Promise<TokenSet>;\n}\n\n/**\n * v1 stub. Always rejects with a static-message NotLoggedInError; it\n * never inspects, echoes, or logs the token it was handed. The real\n * HTTP implementation is supplied by the auth commands.\n */\nexport class NotImplementedRefresher implements TokenRefresher {\n async refresh(refreshToken: string): Promise<TokenSet> {\n // Deliberately unused: this placeholder must never touch the token.\n void refreshToken;\n throw new NotLoggedInError(\n 'Token refresh is not available yet in this CLI version.',\n );\n }\n}\n","/**\n * Runtime validation for the on-disk credentials file.\n *\n * The parser is deliberately strict: unknown top-level keys, missing\n * fields, non-ISO timestamps, and any `version` other than `1` are all\n * rejected with a typed ValidationError. Error messages NEVER embed the\n * raw file contents — a malformed file may contain a truncated token.\n */\n\nimport { z } from 'zod';\nimport { ValidationError } from '../errors.js';\nimport type { Credentials } from './types.js';\n\nconst identitySchema = z\n .object({\n developerId: z.string(),\n email: z.string(),\n // Token-derived fields captured at login. Optional (additive) so files\n // written before these fields existed still validate as version 1.\n merchantId: z.string().optional(),\n scopes: z.array(z.string()).optional(),\n })\n .strict();\n\nconst profileSchema = z\n .object({\n accessToken: z.string(),\n refreshToken: z.string(),\n expiresAt: z.string().datetime({ offset: true }),\n identity: identitySchema,\n obtainedAt: z.string().datetime({ offset: true }),\n })\n .strict();\n\nconst developerSessionSchema = z\n .object({\n jwt: z.string(),\n developerId: z.string(),\n email: z.string(),\n expiresAt: z.string().datetime({ offset: true }),\n obtainedAt: z.string().datetime({ offset: true }),\n })\n .strict();\n\n/** Strict zod schema for the version-1 credentials file. */\nexport const credentialsSchema = z\n .object({\n version: z.literal(1),\n server: z.string(),\n clientId: z.string(),\n profiles: z.record(z.string(), profileSchema),\n // Optional cached developer session. Additive so files written before\n // this field existed still validate as version 1.\n developer: developerSessionSchema.optional(),\n })\n .strict();\n\n/**\n * Validate an already-JSON-parsed value against the version-1 shape.\n *\n * Throws ValidationError on any mismatch. A recognisable-but-unsupported\n * `version` gets a dedicated upgrade message; the version label is only\n * echoed when it is a number so arbitrary file contents can never leak\n * into an error message.\n */\nexport function parseCredentials(input: unknown): Credentials {\n if (typeof input === 'object' && input !== null && 'version' in input) {\n const version = (input as { version: unknown }).version;\n if (version !== 1) {\n const label = typeof version === 'number' ? String(version) : 'unknown';\n throw new ValidationError(\n `credentials file version ${label} is unsupported by this CLI — upgrade the CLI or sign in again`,\n );\n }\n }\n\n const result = credentialsSchema.safeParse(input);\n if (!result.success) {\n throw new ValidationError(\n 'credentials file schema mismatch — expected v1 shape',\n );\n }\n return result.data as Credentials;\n}\n"],"mappings":"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","names":["base"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Shared types for the credential store module.
3
+ *
4
+ * These interfaces describe the on-disk JSON shape of the credentials
5
+ * file (`Credentials` / `Profile` / `Identity`) and the return shape of
6
+ * a token-refresh round-trip (`TokenSet`). The on-disk shape is
7
+ * versioned: readers reject any file whose `version` is not `1`.
8
+ */
9
+ /** Who the stored tokens belong to. */
10
+ interface Identity {
11
+ developerId: string;
12
+ email: string;
13
+ /**
14
+ * Token-derived merchant id captured at login. Optional so files written
15
+ * before this field existed still validate (backward-compatible v1).
16
+ */
17
+ merchantId?: string;
18
+ /**
19
+ * Token-derived OAuth scopes captured at login. Optional so files written
20
+ * before this field existed still validate (backward-compatible v1).
21
+ */
22
+ scopes?: string[];
23
+ }
24
+ /** One named credential profile inside the credentials file. */
25
+ interface Profile {
26
+ accessToken: string;
27
+ refreshToken: string;
28
+ /** ISO-8601 timestamp (UTC). */
29
+ expiresAt: string;
30
+ identity: Identity;
31
+ /** ISO-8601 timestamp (UTC). */
32
+ obtainedAt: string;
33
+ }
34
+ /**
35
+ * A cached developer sign-in session (email/password flow). Lives at a
36
+ * single optional top-level key so files written before it existed still
37
+ * validate as version 1 (backward-compatible additive extension).
38
+ */
39
+ interface DeveloperSession {
40
+ jwt: string;
41
+ developerId: string;
42
+ email: string;
43
+ /** ISO-8601 timestamp (UTC) — computed as obtainedAt + 24h at write time. */
44
+ expiresAt: string;
45
+ /** ISO-8601 timestamp (UTC). */
46
+ obtainedAt: string;
47
+ }
48
+ /** The full on-disk credentials file shape (version 1). */
49
+ interface Credentials {
50
+ version: 1;
51
+ server: string;
52
+ clientId: string;
53
+ profiles: Record<string, Profile>;
54
+ /** Optional cached developer session (additive, backward-compatible v1). */
55
+ developer?: DeveloperSession;
56
+ }
57
+ /** Return shape of TokenRefresher.refresh(). */
58
+ interface TokenSet {
59
+ accessToken: string;
60
+ refreshToken: string;
61
+ /** ISO-8601 timestamp (UTC). */
62
+ expiresAt: string;
63
+ }
64
+
65
+ export type { Credentials, DeveloperSession, Identity, Profile, TokenSet };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Extension seam for future development-store gating.
3
+ *
4
+ * The Ratio platform has a forthcoming capability that will restrict certain
5
+ * operations to development stores only. Today the CLI does not enforce any
6
+ * such restriction — every operation is permitted regardless of store type.
7
+ * When the platform gains that capability, one class swap in this module
8
+ * (returning a different implementation from {@link getDevStoreGate}) will
9
+ * activate enforcement without touching any consumer.
10
+ *
11
+ * Developers integrating with the CLI's internals do not need to interact
12
+ * with this seam in v1.
13
+ *
14
+ * Rules:
15
+ * - No imports from node:fs, node:net, node:http, db, fetch, or any other
16
+ * I/O module. This module must remain pure/sync.
17
+ * - No Date.now() / new Date() / Math.random().
18
+ * - The only mutable module-level state is the singleton slot below.
19
+ */
20
+ /**
21
+ * Context passed by a caller invoking a would-be-gated operation.
22
+ *
23
+ * `command` is the only required field so that the seam is callable from
24
+ * unauthenticated flows (e.g. login) as well as authenticated ones. `appId`
25
+ * and `developerId` are optional because they are not yet known at every
26
+ * potential call-site.
27
+ */
28
+ interface DevStoreGateContext {
29
+ /** The invoking oclif command id (e.g. "app link"). Required. */
30
+ readonly command: string;
31
+ /** The current app being operated on, if any. */
32
+ readonly appId?: string;
33
+ /** The current developer, if authenticated. */
34
+ readonly developerId?: string;
35
+ }
36
+ /** Extension point for future development-store gating enforcement. */
37
+ interface DevStoreGate {
38
+ /**
39
+ * Assert the current context is allowed to invoke a gated operation.
40
+ *
41
+ * v1 implementations MAY be a no-op. Future implementations will throw
42
+ * when the operation is not permitted for the current context. Callers
43
+ * cannot silently ignore a denial: the method returns `void`, so the
44
+ * only signalling channel is an exception.
45
+ */
46
+ assertAllowed(context: DevStoreGateContext): void;
47
+ }
48
+ /**
49
+ * v1 no-op implementation. Permits every operation unconditionally by
50
+ * simply returning. Replaced by a future implementation when the platform
51
+ * capability lights up.
52
+ */
53
+ declare class NoopDevStoreGate implements DevStoreGate {
54
+ assertAllowed(_context: DevStoreGateContext): void;
55
+ }
56
+ /**
57
+ * Return the current {@link DevStoreGate} binding.
58
+ *
59
+ * v1 returns a singleton {@link NoopDevStoreGate}. When the platform
60
+ * capability lights up, this factory is the single point that will
61
+ * return a different implementation. No other code path is expected
62
+ * to instantiate a {@link DevStoreGate}.
63
+ */
64
+ declare function getDevStoreGate(): DevStoreGate;
65
+
66
+ export { type DevStoreGate, type DevStoreGateContext, NoopDevStoreGate, getDevStoreGate };
@@ -0,0 +1,15 @@
1
+ // src/lib/dev-store-gate.ts
2
+ var NoopDevStoreGate = class {
3
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
+ assertAllowed(_context) {
5
+ }
6
+ };
7
+ var _defaultInstance = new NoopDevStoreGate();
8
+ function getDevStoreGate() {
9
+ return _defaultInstance;
10
+ }
11
+ export {
12
+ NoopDevStoreGate,
13
+ getDevStoreGate
14
+ };
15
+ //# sourceMappingURL=dev-store-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/dev-store-gate.ts"],"sourcesContent":["/**\n * Extension seam for future development-store gating.\n *\n * The Ratio platform has a forthcoming capability that will restrict certain\n * operations to development stores only. Today the CLI does not enforce any\n * such restriction — every operation is permitted regardless of store type.\n * When the platform gains that capability, one class swap in this module\n * (returning a different implementation from {@link getDevStoreGate}) will\n * activate enforcement without touching any consumer.\n *\n * Developers integrating with the CLI's internals do not need to interact\n * with this seam in v1.\n *\n * Rules:\n * - No imports from node:fs, node:net, node:http, db, fetch, or any other\n * I/O module. This module must remain pure/sync.\n * - No Date.now() / new Date() / Math.random().\n * - The only mutable module-level state is the singleton slot below.\n */\n\n// ---------------------------------------------------------------------------\n// Public interface\n// ---------------------------------------------------------------------------\n\n/**\n * Context passed by a caller invoking a would-be-gated operation.\n *\n * `command` is the only required field so that the seam is callable from\n * unauthenticated flows (e.g. login) as well as authenticated ones. `appId`\n * and `developerId` are optional because they are not yet known at every\n * potential call-site.\n */\nexport interface DevStoreGateContext {\n /** The invoking oclif command id (e.g. \"app link\"). Required. */\n readonly command: string;\n /** The current app being operated on, if any. */\n readonly appId?: string;\n /** The current developer, if authenticated. */\n readonly developerId?: string;\n}\n\n/** Extension point for future development-store gating enforcement. */\nexport interface DevStoreGate {\n /**\n * Assert the current context is allowed to invoke a gated operation.\n *\n * v1 implementations MAY be a no-op. Future implementations will throw\n * when the operation is not permitted for the current context. Callers\n * cannot silently ignore a denial: the method returns `void`, so the\n * only signalling channel is an exception.\n */\n assertAllowed(context: DevStoreGateContext): void;\n}\n\n// ---------------------------------------------------------------------------\n// NoopDevStoreGate — v1 no-op implementation (shipping default)\n// ---------------------------------------------------------------------------\n\n/**\n * v1 no-op implementation. Permits every operation unconditionally by\n * simply returning. Replaced by a future implementation when the platform\n * capability lights up.\n */\nexport class NoopDevStoreGate implements DevStoreGate {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n public assertAllowed(_context: DevStoreGateContext): void {\n // No-op by design. See module JSDoc.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module-level singleton factory\n// ---------------------------------------------------------------------------\n\n// Stateless — a single instance is sound for the whole process lifetime.\nconst _defaultInstance: DevStoreGate = new NoopDevStoreGate();\n\n/**\n * Return the current {@link DevStoreGate} binding.\n *\n * v1 returns a singleton {@link NoopDevStoreGate}. When the platform\n * capability lights up, this factory is the single point that will\n * return a different implementation. No other code path is expected\n * to instantiate a {@link DevStoreGate}.\n */\nexport function getDevStoreGate(): DevStoreGate {\n return _defaultInstance;\n}\n"],"mappings":";AA+DO,IAAM,mBAAN,MAA+C;AAAA;AAAA,EAE7C,cAAc,UAAqC;AAAA,EAE1D;AACF;AAOA,IAAM,mBAAiC,IAAI,iBAAiB;AAUrD,SAAS,kBAAgC;AAC9C,SAAO;AACT;","names":[]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Error hierarchy for the Ratio CLI. Every error thrown from a command's
3
+ * run() is expected to be an instance of RatioCLIError so the global
4
+ * render-error handler can map it to a deterministic stderr line and exit
5
+ * code. Uncaught Error subclasses render as "INTERNAL: <msg>" with exit 1.
6
+ *
7
+ * No import from @oclif/core — this file is dependency-free so tests
8
+ * can import it without oclif overhead.
9
+ */
10
+ declare class RatioCLIError extends Error {
11
+ readonly code: string;
12
+ readonly exitCode: number;
13
+ readonly hint?: string;
14
+ readonly docsUrl?: string;
15
+ constructor(opts: {
16
+ code: string;
17
+ exitCode: number;
18
+ message: string;
19
+ hint?: string;
20
+ docsUrl?: string;
21
+ });
22
+ }
23
+ /** Coming-soon stub UX. Renders on stdout with exit 0. */
24
+ declare class ComingSoonError extends RatioCLIError {
25
+ constructor(message: string);
26
+ }
27
+ /** User has no credentials and command requires auth. */
28
+ declare class NotLoggedInError extends RatioCLIError {
29
+ constructor(message: string, hint?: string);
30
+ }
31
+ declare class TokenExpiredError extends RatioCLIError {
32
+ constructor(message: string, hint?: string);
33
+ }
34
+ declare class ScopeDeniedError extends RatioCLIError {
35
+ constructor(message: string, hint?: string);
36
+ }
37
+ declare class NetworkError extends RatioCLIError {
38
+ constructor(message: string, hint?: string);
39
+ }
40
+ declare class RelayConnectError extends RatioCLIError {
41
+ constructor(message: string, hint?: string);
42
+ }
43
+ declare class TemplateExistsError extends RatioCLIError {
44
+ constructor(message: string, hint?: string);
45
+ }
46
+ declare class ValidationError extends RatioCLIError {
47
+ constructor(message: string, hint?: string);
48
+ }
49
+ declare class RelayResumeGapError extends RatioCLIError {
50
+ constructor(message: string, hint?: string);
51
+ }
52
+ declare class OrchestratorSpawnError extends RatioCLIError {
53
+ constructor(message: string, hint?: string);
54
+ }
55
+ declare class OrchestratorDrainTimeoutError extends RatioCLIError {
56
+ constructor(message: string, hint?: string);
57
+ }
58
+ declare function isRatioCLIError(err: unknown): err is RatioCLIError;
59
+
60
+ export { ComingSoonError, NetworkError, NotLoggedInError, OrchestratorDrainTimeoutError, OrchestratorSpawnError, RatioCLIError, RelayConnectError, RelayResumeGapError, ScopeDeniedError, TemplateExistsError, TokenExpiredError, ValidationError, isRatioCLIError };
@@ -0,0 +1,101 @@
1
+ // src/lib/errors.ts
2
+ var RatioCLIError = class extends Error {
3
+ code;
4
+ exitCode;
5
+ hint;
6
+ docsUrl;
7
+ constructor(opts) {
8
+ super(opts.message);
9
+ this.name = new.target.name;
10
+ this.code = opts.code;
11
+ this.exitCode = opts.exitCode;
12
+ if (opts.hint !== void 0) this.hint = opts.hint;
13
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
14
+ }
15
+ };
16
+ var ComingSoonError = class extends RatioCLIError {
17
+ constructor(message) {
18
+ super({ code: "COMING_SOON", exitCode: 0, message });
19
+ }
20
+ };
21
+ var NotLoggedInError = class extends RatioCLIError {
22
+ constructor(message, hint = "Run 'ratio auth login' to sign in.") {
23
+ super({ code: "NOT_LOGGED_IN", exitCode: 3, message, hint });
24
+ }
25
+ };
26
+ var TokenExpiredError = class extends RatioCLIError {
27
+ constructor(message, hint = "Run 'ratio auth login' to refresh your session.") {
28
+ super({ code: "TOKEN_EXPIRED", exitCode: 3, message, hint });
29
+ }
30
+ };
31
+ var ScopeDeniedError = class extends RatioCLIError {
32
+ constructor(message, hint) {
33
+ const opts = {
34
+ code: "SCOPE_DENIED",
35
+ exitCode: 4,
36
+ message
37
+ };
38
+ if (hint !== void 0) opts.hint = hint;
39
+ super(opts);
40
+ }
41
+ };
42
+ var NetworkError = class extends RatioCLIError {
43
+ constructor(message, hint = "Check your network and retry.") {
44
+ super({ code: "NETWORK_ERROR", exitCode: 5, message, hint });
45
+ }
46
+ };
47
+ var RelayConnectError = class extends RatioCLIError {
48
+ constructor(message, hint = "Verify the tunnel URL and that the auth-server is reachable.") {
49
+ super({ code: "RELAY_CONNECT", exitCode: 6, message, hint });
50
+ }
51
+ };
52
+ var TemplateExistsError = class extends RatioCLIError {
53
+ constructor(message, hint = "Choose a different name or remove the existing directory.") {
54
+ super({ code: "TEMPLATE_EXISTS", exitCode: 7, message, hint });
55
+ }
56
+ };
57
+ var ValidationError = class extends RatioCLIError {
58
+ constructor(message, hint) {
59
+ const opts = {
60
+ code: "VALIDATION_ERROR",
61
+ exitCode: 8,
62
+ message
63
+ };
64
+ if (hint !== void 0) opts.hint = hint;
65
+ super(opts);
66
+ }
67
+ };
68
+ var RelayResumeGapError = class extends RatioCLIError {
69
+ constructor(message, hint = "Reconnect from the current cursor; older events are outside the buffer window.") {
70
+ super({ code: "RELAY_RESUME_GAP", exitCode: 9, message, hint });
71
+ }
72
+ };
73
+ var OrchestratorSpawnError = class extends RatioCLIError {
74
+ constructor(message, hint = "Could not start your app's dev command \u2014 check that the command in ratio.config.jsonc runs from this directory.") {
75
+ super({ code: "ORCHESTRATOR_SPAWN", exitCode: 10, message, hint });
76
+ }
77
+ };
78
+ var OrchestratorDrainTimeoutError = class extends RatioCLIError {
79
+ constructor(message, hint = "Your dev command did not exit cleanly within 5 seconds \u2014 it was forcibly terminated.") {
80
+ super({ code: "ORCHESTRATOR_DRAIN", exitCode: 11, message, hint });
81
+ }
82
+ };
83
+ function isRatioCLIError(err) {
84
+ return err instanceof RatioCLIError;
85
+ }
86
+ export {
87
+ ComingSoonError,
88
+ NetworkError,
89
+ NotLoggedInError,
90
+ OrchestratorDrainTimeoutError,
91
+ OrchestratorSpawnError,
92
+ RatioCLIError,
93
+ RelayConnectError,
94
+ RelayResumeGapError,
95
+ ScopeDeniedError,
96
+ TemplateExistsError,
97
+ TokenExpiredError,
98
+ ValidationError,
99
+ isRatioCLIError
100
+ };
101
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/errors.ts"],"sourcesContent":["/**\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":";AAUO,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;AAGO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EACjD,YAAY,SAAiB;AAC3B,UAAM,EAAE,MAAM,eAAe,UAAU,GAAG,QAAQ,CAAC;AAAA,EACrD;AACF;AAGO,IAAM,mBAAN,cAA+B,cAAc;AAAA,EAClD,YACE,SACA,OAAe,sCACf;AACA,UAAM,EAAE,MAAM,iBAAiB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC7D;AACF;AAEO,IAAM,oBAAN,cAAgC,cAAc;AAAA,EACnD,YACE,SACA,OAAe,mDACf;AACA,UAAM,EAAE,MAAM,iBAAiB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC7D;AACF;AAEO,IAAM,mBAAN,cAA+B,cAAc;AAAA,EAClD,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;AAEO,IAAM,eAAN,cAA2B,cAAc;AAAA,EAC9C,YAAY,SAAiB,OAAe,iCAAiC;AAC3E,UAAM,EAAE,MAAM,iBAAiB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC7D;AACF;AAEO,IAAM,oBAAN,cAAgC,cAAc;AAAA,EACnD,YACE,SACA,OAAe,gEACf;AACA,UAAM,EAAE,MAAM,iBAAiB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC7D;AACF;AAEO,IAAM,sBAAN,cAAkC,cAAc;AAAA,EACrD,YACE,SACA,OAAe,6DACf;AACA,UAAM,EAAE,MAAM,mBAAmB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC/D;AACF;AAEO,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;AAEO,IAAM,sBAAN,cAAkC,cAAc;AAAA,EACrD,YACE,SACA,OAAe,kFACf;AACA,UAAM,EAAE,MAAM,oBAAoB,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAChE;AACF;AAEO,IAAM,yBAAN,cAAqC,cAAc;AAAA,EACxD,YACE,SACA,OAAe,wHACf;AACA,UAAM,EAAE,MAAM,sBAAsB,UAAU,IAAI,SAAS,KAAK,CAAC;AAAA,EACnE;AACF;AAEO,IAAM,gCAAN,cAA4C,cAAc;AAAA,EAC/D,YACE,SACA,OAAe,6FACf;AACA,UAAM,EAAE,MAAM,sBAAsB,UAAU,IAAI,SAAS,KAAK,CAAC;AAAA,EACnE;AACF;AAEO,SAAS,gBAAgB,KAAoC;AAClE,SAAO,eAAe;AACxB;","names":[]}
@@ -0,0 +1,2 @@
1
+ export { MANIFEST_FILE_NAME, WriteClientIdResult, writeClientId } from './write-client-id.js';
2
+ export { DEV_COMMAND_HINT, ManifestView, readManifest } from './read-manifest.js';
@@ -0,0 +1,144 @@
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
+
91
+ // src/lib/manifest/read-manifest.ts
92
+ import fs2 from "fs";
93
+ import path2 from "path";
94
+ var DEV_COMMAND_ROW = /^\s*"devCommand"\s*:\s*"([^"]*)"/m;
95
+ var DEV_SERVER_PORT_ROW = /^\s*"devServerPort"\s*:\s*(\d+)/m;
96
+ var DEV_SERVER_PORT_KEY = /^\s*"devServerPort"\s*:/m;
97
+ var APP_NAME_ROW2 = /^\s*"appName"\s*:\s*"([^"]*)"/m;
98
+ var CLIENT_ID_READ_ROW = /^\s*"clientId"\s*:\s*"([^"]*)"/m;
99
+ var DEV_COMMAND_HINT = 'Add a "devCommand" entry to ratio.config.jsonc (e.g. "npm run dev") and re-run.';
100
+ function readManifest(cwd) {
101
+ const filePath = path2.join(cwd, MANIFEST_FILE_NAME);
102
+ let content;
103
+ try {
104
+ content = fs2.readFileSync(filePath, "utf8");
105
+ } catch {
106
+ throw new ValidationError(
107
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
108
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
109
+ );
110
+ }
111
+ const devCommand = DEV_COMMAND_ROW.exec(content)?.[1];
112
+ if (devCommand === void 0 || devCommand.trim() === "") {
113
+ throw new ValidationError(
114
+ `${MANIFEST_FILE_NAME} has no usable "devCommand" entry.`,
115
+ DEV_COMMAND_HINT
116
+ );
117
+ }
118
+ let devServerPort;
119
+ if (DEV_SERVER_PORT_KEY.test(content)) {
120
+ const rawPort = DEV_SERVER_PORT_ROW.exec(content)?.[1];
121
+ const port = rawPort === void 0 ? Number.NaN : Number.parseInt(rawPort, 10);
122
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
123
+ throw new ValidationError(
124
+ `${MANIFEST_FILE_NAME} has an invalid "devServerPort" value.`,
125
+ '"devServerPort" must be an unquoted integer between 1 and 65535 (e.g. "devServerPort": 3001).'
126
+ );
127
+ }
128
+ devServerPort = port;
129
+ }
130
+ return {
131
+ path: filePath,
132
+ devCommand,
133
+ devServerPort,
134
+ appName: APP_NAME_ROW2.exec(content)?.[1],
135
+ clientId: CLIENT_ID_READ_ROW.exec(content)?.[1]
136
+ };
137
+ }
138
+ export {
139
+ DEV_COMMAND_HINT,
140
+ MANIFEST_FILE_NAME,
141
+ readManifest,
142
+ writeClientId
143
+ };
144
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/manifest/write-client-id.ts","../../../src/lib/errors.ts","../../../src/lib/manifest/read-manifest.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","/**\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"],"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;;;AE1GA,OAAOA,SAAQ;AACf,OAAOC,WAAU;AAKjB,IAAM,kBAAkB;AAGxB,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAMC,gBAAe;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,SAASF,cAAa,KAAK,OAAO,IAAI,CAAC;AAAA,IACvC,UAAU,mBAAmB,KAAK,OAAO,IAAI,CAAC;AAAA,EAChD;AACF;","names":["fs","path","APP_NAME_ROW","path","fs"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Read-only view over the app manifest (ratio.config.jsonc).
3
+ *
4
+ * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is
5
+ * involved, so comments and unknown keys are irrelevant to the read and
6
+ * survive untouched on disk. Each field is extracted with one targeted
7
+ * regex, mirroring the write-side idiom in write-client-id.ts. This module
8
+ * never writes the file.
9
+ */
10
+ /** Hint shown whenever the dev command entry is missing or unusable. */
11
+ declare const DEV_COMMAND_HINT = "Add a \"devCommand\" entry to ratio.config.jsonc (e.g. \"npm run dev\") and re-run.";
12
+ interface ManifestView {
13
+ /** Absolute path to the manifest file that was read. */
14
+ path: string;
15
+ /** The command used to start the app's own dev server. Always present. */
16
+ devCommand: string;
17
+ /** Optional local dev-server port (1-65535); undefined when absent. */
18
+ devServerPort: number | undefined;
19
+ /** Parsed from the manifest's appName row, when present. */
20
+ appName: string | undefined;
21
+ /** Parsed from the manifest's clientId row, when present. */
22
+ clientId: string | undefined;
23
+ }
24
+ /**
25
+ * Locate `ratio.config.jsonc` under `cwd` and extract the fields the dev
26
+ * orchestrator needs. Fails fast with a typed error when the file is
27
+ * missing, when `devCommand` is absent/empty, or when `devServerPort` is
28
+ * present but not an integer in 1-65535.
29
+ */
30
+ declare function readManifest(cwd: string): ManifestView;
31
+
32
+ export { DEV_COMMAND_HINT, type ManifestView, readManifest };