@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,1120 @@
1
+ // src/commands/app/link.ts
2
+ import { createInterface } from "readline/promises";
3
+ import fs3 from "fs";
4
+ import { Flags as Flags2 } from "@oclif/core";
5
+ import { z as z2 } from "zod";
6
+
7
+ // src/lib/credentials/clock.ts
8
+ var SystemClock = class {
9
+ now() {
10
+ return Date.now();
11
+ }
12
+ };
13
+
14
+ // src/lib/credentials/path.ts
15
+ import os from "os";
16
+ import path from "path";
17
+ function snapshotPathEnv() {
18
+ const snapshot = {};
19
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
20
+ if (xdgConfigHome !== void 0) {
21
+ snapshot.XDG_CONFIG_HOME = xdgConfigHome;
22
+ }
23
+ const appData = process.env["APPDATA"];
24
+ if (appData !== void 0) {
25
+ snapshot.APPDATA = appData;
26
+ }
27
+ return snapshot;
28
+ }
29
+ function defaultCredentialsPath(platform, homedir, env) {
30
+ const resolvedPlatform = platform ?? process.platform;
31
+ const resolvedHome = homedir ?? os.homedir();
32
+ const resolvedEnv = env ?? snapshotPathEnv();
33
+ if (resolvedPlatform === "win32") {
34
+ const appData = resolvedEnv.APPDATA;
35
+ const base2 = appData !== void 0 && appData !== "" ? appData : path.win32.join(resolvedHome, "AppData", "Roaming");
36
+ return path.win32.join(base2, "ratio", "credentials");
37
+ }
38
+ const xdgConfigHome = resolvedEnv.XDG_CONFIG_HOME;
39
+ const base = typeof xdgConfigHome === "string" && xdgConfigHome !== "" ? xdgConfigHome : path.posix.join(resolvedHome, ".config");
40
+ return path.posix.join(base, "ratio", "credentials");
41
+ }
42
+
43
+ // src/lib/errors.ts
44
+ var RatioCLIError = class extends Error {
45
+ code;
46
+ exitCode;
47
+ hint;
48
+ docsUrl;
49
+ constructor(opts) {
50
+ super(opts.message);
51
+ this.name = new.target.name;
52
+ this.code = opts.code;
53
+ this.exitCode = opts.exitCode;
54
+ if (opts.hint !== void 0) this.hint = opts.hint;
55
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
56
+ }
57
+ };
58
+ var NotLoggedInError = class extends RatioCLIError {
59
+ constructor(message, hint = "Run 'ratio auth login' to sign in.") {
60
+ super({ code: "NOT_LOGGED_IN", exitCode: 3, message, hint });
61
+ }
62
+ };
63
+ var TokenExpiredError = class extends RatioCLIError {
64
+ constructor(message, hint = "Run 'ratio auth login' to refresh your session.") {
65
+ super({ code: "TOKEN_EXPIRED", exitCode: 3, message, hint });
66
+ }
67
+ };
68
+ var NetworkError = class extends RatioCLIError {
69
+ constructor(message, hint = "Check your network and retry.") {
70
+ super({ code: "NETWORK_ERROR", exitCode: 5, message, hint });
71
+ }
72
+ };
73
+ var ValidationError = class extends RatioCLIError {
74
+ constructor(message, hint) {
75
+ const opts = {
76
+ code: "VALIDATION_ERROR",
77
+ exitCode: 8,
78
+ message
79
+ };
80
+ if (hint !== void 0) opts.hint = hint;
81
+ super(opts);
82
+ }
83
+ };
84
+ function isRatioCLIError(err) {
85
+ return err instanceof RatioCLIError;
86
+ }
87
+
88
+ // src/lib/credentials/refresher.ts
89
+ var NotImplementedRefresher = class {
90
+ async refresh(refreshToken) {
91
+ void refreshToken;
92
+ throw new NotLoggedInError(
93
+ "Token refresh is not available yet in this CLI version."
94
+ );
95
+ }
96
+ };
97
+
98
+ // src/lib/credentials/store.ts
99
+ import crypto from "crypto";
100
+ import fs from "fs";
101
+ import { dirname } from "path";
102
+
103
+ // src/lib/credentials/schema.ts
104
+ import { z } from "zod";
105
+ var identitySchema = z.object({
106
+ developerId: z.string(),
107
+ email: z.string(),
108
+ // Token-derived fields captured at login. Optional (additive) so files
109
+ // written before these fields existed still validate as version 1.
110
+ merchantId: z.string().optional(),
111
+ scopes: z.array(z.string()).optional()
112
+ }).strict();
113
+ var profileSchema = z.object({
114
+ accessToken: z.string(),
115
+ refreshToken: z.string(),
116
+ expiresAt: z.string().datetime({ offset: true }),
117
+ identity: identitySchema,
118
+ obtainedAt: z.string().datetime({ offset: true })
119
+ }).strict();
120
+ var developerSessionSchema = z.object({
121
+ jwt: z.string(),
122
+ developerId: z.string(),
123
+ email: z.string(),
124
+ expiresAt: z.string().datetime({ offset: true }),
125
+ obtainedAt: z.string().datetime({ offset: true })
126
+ }).strict();
127
+ var credentialsSchema = z.object({
128
+ version: z.literal(1),
129
+ server: z.string(),
130
+ clientId: z.string(),
131
+ profiles: z.record(z.string(), profileSchema),
132
+ // Optional cached developer session. Additive so files written before
133
+ // this field existed still validate as version 1.
134
+ developer: developerSessionSchema.optional()
135
+ }).strict();
136
+ function parseCredentials(input) {
137
+ if (typeof input === "object" && input !== null && "version" in input) {
138
+ const version = input.version;
139
+ if (version !== 1) {
140
+ const label = typeof version === "number" ? String(version) : "unknown";
141
+ throw new ValidationError(
142
+ `credentials file version ${label} is unsupported by this CLI \u2014 upgrade the CLI or sign in again`
143
+ );
144
+ }
145
+ }
146
+ const result = credentialsSchema.safeParse(input);
147
+ if (!result.success) {
148
+ throw new ValidationError(
149
+ "credentials file schema mismatch \u2014 expected v1 shape"
150
+ );
151
+ }
152
+ return result.data;
153
+ }
154
+
155
+ // src/lib/credentials/store.ts
156
+ var REFRESH_WINDOW_MS = 3e5;
157
+ var ENV_TOKEN_VAR = "RATIO_API_TOKEN";
158
+ var ENV_DEVELOPER_TOKEN_VAR = "RATIO_DEVELOPER_TOKEN";
159
+ function hasErrnoCode(err, code) {
160
+ return err instanceof Error && err.code === code;
161
+ }
162
+ var FileCredentialStore = class {
163
+ filePath;
164
+ clock;
165
+ refresher;
166
+ envReader;
167
+ platform;
168
+ constructor(opts) {
169
+ this.filePath = opts?.filePath ?? defaultCredentialsPath();
170
+ this.clock = opts?.clock ?? new SystemClock();
171
+ this.refresher = opts?.refresher ?? new NotImplementedRefresher();
172
+ this.envReader = opts?.envReader ?? (() => process.env);
173
+ this.platform = opts?.platform ?? process.platform;
174
+ }
175
+ async load() {
176
+ this.reapplyFileModeIfDrifted();
177
+ let raw;
178
+ try {
179
+ raw = fs.readFileSync(this.filePath, "utf8");
180
+ } catch (err) {
181
+ if (hasErrnoCode(err, "ENOENT")) {
182
+ throw new NotLoggedInError("You are not signed in.");
183
+ }
184
+ throw err;
185
+ }
186
+ let parsed;
187
+ try {
188
+ parsed = JSON.parse(raw);
189
+ } catch {
190
+ throw new ValidationError(
191
+ 'credentials file is not valid JSON \u2014 run "ratio auth login" to reset'
192
+ );
193
+ }
194
+ return parseCredentials(parsed);
195
+ }
196
+ async save(creds) {
197
+ const dir = dirname(this.filePath);
198
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
199
+ if (this.platform !== "win32") {
200
+ fs.chmodSync(dir, 448);
201
+ }
202
+ const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
203
+ try {
204
+ fs.writeFileSync(tmpPath, JSON.stringify(creds, null, 2), {
205
+ mode: 384
206
+ });
207
+ fs.renameSync(tmpPath, this.filePath);
208
+ } catch (err) {
209
+ try {
210
+ fs.unlinkSync(tmpPath);
211
+ } catch {
212
+ }
213
+ throw err;
214
+ }
215
+ if (this.platform !== "win32") {
216
+ fs.chmodSync(this.filePath, 384);
217
+ }
218
+ }
219
+ async clear() {
220
+ try {
221
+ fs.unlinkSync(this.filePath);
222
+ } catch (err) {
223
+ if (hasErrnoCode(err, "ENOENT")) {
224
+ return;
225
+ }
226
+ throw err;
227
+ }
228
+ }
229
+ async getValidAccessToken(profileName) {
230
+ const envToken = this.envReader()[ENV_TOKEN_VAR];
231
+ if (typeof envToken === "string" && envToken.length > 0) {
232
+ return envToken;
233
+ }
234
+ const creds = await this.load();
235
+ const name = profileName ?? "default";
236
+ const profile = creds.profiles[name];
237
+ if (profile === void 0) {
238
+ throw new NotLoggedInError("You are not signed in.");
239
+ }
240
+ const msUntilExpiry = new Date(profile.expiresAt).getTime() - this.clock.now();
241
+ if (msUntilExpiry < REFRESH_WINDOW_MS) {
242
+ return this.refreshAndPersist(creds, name, profile);
243
+ }
244
+ return profile.accessToken;
245
+ }
246
+ async getValidDeveloperToken() {
247
+ const envToken = this.envReader()[ENV_DEVELOPER_TOKEN_VAR];
248
+ if (typeof envToken === "string" && envToken.length > 0) {
249
+ return envToken;
250
+ }
251
+ let creds;
252
+ try {
253
+ creds = await this.load();
254
+ } catch (err) {
255
+ if (err instanceof NotLoggedInError) {
256
+ throw new NotLoggedInError("Developer signin required.");
257
+ }
258
+ throw err;
259
+ }
260
+ const session = creds.developer;
261
+ if (session === void 0) {
262
+ throw new NotLoggedInError("Developer signin required.");
263
+ }
264
+ const msUntilExpiry = new Date(session.expiresAt).getTime() - this.clock.now();
265
+ if (msUntilExpiry < REFRESH_WINDOW_MS) {
266
+ throw new NotLoggedInError("Developer signin required.");
267
+ }
268
+ return session.jwt;
269
+ }
270
+ async saveDeveloperSession(session) {
271
+ let creds;
272
+ try {
273
+ creds = await this.load();
274
+ } catch (err) {
275
+ if (err instanceof NotLoggedInError) {
276
+ creds = { version: 1, server: "", clientId: "", profiles: {} };
277
+ } else {
278
+ throw err;
279
+ }
280
+ }
281
+ const updated = {
282
+ ...creds,
283
+ developer: session
284
+ };
285
+ await this.save(updated);
286
+ }
287
+ /**
288
+ * Exchange the profile's refresh token for a fresh token set, merge it
289
+ * into the loaded credentials (preserving every other profile), persist
290
+ * atomically, and return the new access token. A refresher failure is
291
+ * wrapped in TokenExpiredError; the underlying message is deliberately
292
+ * discarded so a secret can never leak through the error surface.
293
+ */
294
+ async refreshAndPersist(creds, name, profile) {
295
+ let refreshed;
296
+ try {
297
+ refreshed = await this.refresher.refresh(profile.refreshToken);
298
+ } catch {
299
+ throw new TokenExpiredError(
300
+ "Your session has expired and could not be refreshed."
301
+ );
302
+ }
303
+ const updated = {
304
+ ...creds,
305
+ profiles: {
306
+ ...creds.profiles,
307
+ [name]: {
308
+ ...profile,
309
+ accessToken: refreshed.accessToken,
310
+ refreshToken: refreshed.refreshToken,
311
+ expiresAt: refreshed.expiresAt
312
+ }
313
+ }
314
+ };
315
+ await this.save(updated);
316
+ return refreshed.accessToken;
317
+ }
318
+ /**
319
+ * POSIX-only startup permission check: if the file exists with a mode
320
+ * other than 0600, re-apply 0600 and emit a warning to stderr. Never
321
+ * runs on Windows (stat modes there do not map to POSIX bits and chmod
322
+ * must not be called). Missing file is NOT an error here — load()
323
+ * reports it as NotLoggedInError.
324
+ */
325
+ reapplyFileModeIfDrifted() {
326
+ if (this.platform === "win32") {
327
+ return;
328
+ }
329
+ let stat;
330
+ try {
331
+ stat = fs.statSync(this.filePath);
332
+ } catch (err) {
333
+ if (hasErrnoCode(err, "ENOENT")) {
334
+ return;
335
+ }
336
+ throw err;
337
+ }
338
+ const mode = stat.mode & 511;
339
+ if (mode !== 384) {
340
+ fs.chmodSync(this.filePath, 384);
341
+ process.stderr.write(
342
+ `[warn] credentials file permissions were 0o${mode.toString(8)}; corrected to 0600.
343
+ `
344
+ );
345
+ }
346
+ }
347
+ };
348
+
349
+ // src/lib/manifest/write-client-id.ts
350
+ import crypto2 from "crypto";
351
+ import fs2 from "fs";
352
+ import path2 from "path";
353
+ var MANIFEST_FILE_NAME = "ratio.config.jsonc";
354
+ var CLIENT_ID_ROW = /^(\s*"clientId"\s*:\s*")([^"]*)("\s*,?)/m;
355
+ var APP_NAME_ROW = /^\s*"appName"\s*:\s*"([^"]*)"/m;
356
+ async function writeClientId(cwd, clientId) {
357
+ const filePath = path2.join(cwd, MANIFEST_FILE_NAME);
358
+ let content;
359
+ try {
360
+ content = fs2.readFileSync(filePath, "utf8");
361
+ } catch {
362
+ throw new ValidationError(
363
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
364
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
365
+ );
366
+ }
367
+ const appNameMatch = APP_NAME_ROW.exec(content);
368
+ const appName = appNameMatch?.[1];
369
+ const rowMatch = CLIENT_ID_ROW.exec(content);
370
+ if (rowMatch === null) {
371
+ throw new ValidationError(
372
+ `${MANIFEST_FILE_NAME} has no top-level "clientId" entry.`,
373
+ 'Restore the "clientId" line from the app template, then re-run.'
374
+ );
375
+ }
376
+ const existing = rowMatch[2] ?? "";
377
+ const base = {
378
+ written: false,
379
+ path: filePath,
380
+ ...appName !== void 0 ? { appName } : {}
381
+ };
382
+ if (existing === clientId) {
383
+ return base;
384
+ }
385
+ const updated = content.replace(
386
+ CLIENT_ID_ROW,
387
+ (_row, open, _old, close) => `${open}${clientId}${close}`
388
+ );
389
+ const tmpPath = `${filePath}.${process.pid}.${crypto2.randomBytes(8).toString("hex")}.tmp`;
390
+ try {
391
+ fs2.writeFileSync(tmpPath, updated, { mode: 384 });
392
+ fs2.renameSync(tmpPath, filePath);
393
+ } catch (err) {
394
+ try {
395
+ fs2.unlinkSync(tmpPath);
396
+ } catch {
397
+ }
398
+ throw err;
399
+ }
400
+ if (process.platform !== "win32") {
401
+ fs2.chmodSync(filePath, 384);
402
+ }
403
+ return {
404
+ ...base,
405
+ written: true,
406
+ ...existing !== "" ? { previousClientId: existing } : {}
407
+ };
408
+ }
409
+
410
+ // src/lib/ratio-command.ts
411
+ import { Command, Errors } from "@oclif/core";
412
+
413
+ // src/lib/render-error.ts
414
+ function isOclifParserError(err) {
415
+ return err instanceof Error && "parse" in err && err.parse !== null && err.parse !== void 0;
416
+ }
417
+ function buildErrorEnvelope(code, message, hint, docsUrl) {
418
+ const obj = { ok: false, code, message };
419
+ if (hint !== void 0) obj["hint"] = hint;
420
+ if (docsUrl !== void 0) obj["docsUrl"] = docsUrl;
421
+ return JSON.stringify(obj) + "\n";
422
+ }
423
+ function renderError(input) {
424
+ const { err, verbose, json } = input;
425
+ if (isRatioCLIError(err)) {
426
+ const ratioErr = err;
427
+ if (ratioErr.exitCode === 0) {
428
+ if (json) {
429
+ return {
430
+ stdout: JSON.stringify({ ok: true, data: null }) + "\n",
431
+ stderr: "",
432
+ exitCode: 0
433
+ };
434
+ }
435
+ return {
436
+ stdout: ratioErr.message + "\n",
437
+ stderr: "",
438
+ exitCode: 0
439
+ };
440
+ }
441
+ let stderrLines = `${ratioErr.code}: ${ratioErr.message}
442
+ `;
443
+ if (ratioErr.hint !== void 0) stderrLines += `${ratioErr.hint}
444
+ `;
445
+ if (ratioErr.docsUrl !== void 0) stderrLines += `${ratioErr.docsUrl}
446
+ `;
447
+ if (json) {
448
+ return {
449
+ stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),
450
+ stderr: verbose && ratioErr.stack ? ratioErr.stack + "\n" : "",
451
+ exitCode: ratioErr.exitCode
452
+ };
453
+ }
454
+ if (verbose && ratioErr.stack) {
455
+ stderrLines += ratioErr.stack + "\n";
456
+ }
457
+ return { stdout: "", stderr: stderrLines, exitCode: ratioErr.exitCode };
458
+ }
459
+ if (isOclifParserError(err)) {
460
+ const parserErr = err;
461
+ const msg = parserErr.message || "Unknown parse error";
462
+ const coercedErr = new ValidationError(msg);
463
+ let stderrText2 = `${coercedErr.code}: ${msg}
464
+ `;
465
+ if (json) {
466
+ return {
467
+ stdout: buildErrorEnvelope(coercedErr.code, msg),
468
+ stderr: verbose && parserErr.stack ? parserErr.stack + "\n" : "",
469
+ exitCode: 8
470
+ };
471
+ }
472
+ if (verbose && parserErr.stack) {
473
+ stderrText2 += parserErr.stack + "\n";
474
+ }
475
+ return { stdout: "", stderr: stderrText2, exitCode: 8 };
476
+ }
477
+ const message = err instanceof Error ? err.message : String(err);
478
+ const stack = err instanceof Error ? err.stack : void 0;
479
+ const stderrPrefix = `INTERNAL: ${message}
480
+ `;
481
+ if (json) {
482
+ return {
483
+ stdout: buildErrorEnvelope("INTERNAL", message),
484
+ stderr: verbose && stack ? stack + "\n" : "",
485
+ exitCode: 1
486
+ };
487
+ }
488
+ let stderrText = stderrPrefix;
489
+ if (verbose && stack) {
490
+ stderrText += stack + "\n";
491
+ }
492
+ return { stdout: "", stderr: stderrText, exitCode: 1 };
493
+ }
494
+
495
+ // src/lib/ratio-command.ts
496
+ var RatioCommand = class extends Command {
497
+ _getRenderFlags() {
498
+ const argv = this.argv;
499
+ return {
500
+ verbose: argv.includes("--verbose"),
501
+ json: argv.includes("--json")
502
+ };
503
+ }
504
+ async catch(err) {
505
+ const { verbose, json } = this._getRenderFlags();
506
+ const output = renderError({ err, verbose, json });
507
+ if (output.stdout) process.stdout.write(output.stdout);
508
+ if (output.stderr) process.stderr.write(output.stderr);
509
+ if (output.exitCode === 0) {
510
+ throw new Errors.ExitError(0);
511
+ }
512
+ err.skipOclifErrorHandling = true;
513
+ err.oclif = { exit: output.exitCode };
514
+ throw err;
515
+ }
516
+ };
517
+
518
+ // src/lib/verbose-flag.ts
519
+ import { Flags } from "@oclif/core";
520
+ var verboseFlag = {
521
+ verbose: Flags.boolean({
522
+ description: "Show diagnostic detail: HTTP status/timing lines for auth requests (stderr) and stack traces for internal errors."
523
+ })
524
+ };
525
+ var jsonFlag = {
526
+ json: Flags.boolean({
527
+ description: "Emit machine-readable JSON on stdout."
528
+ })
529
+ };
530
+
531
+ // src/commands/app/link.ts
532
+ var REQUEST_TIMEOUT_MS = 1e4;
533
+ var DEFAULT_LINK_SERVER = "https://api.ratio.app";
534
+ var SERVER_ENV_VAR = "RATIO_SERVER";
535
+ var DEVELOPER_TOKEN_ENV_VAR = "RATIO_DEVELOPER_TOKEN";
536
+ var DEVELOPER_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
537
+ var DEFAULT_APP_DESCRIPTION = "A Ratio app scaffolded via ratio-cli.";
538
+ var DEFAULT_CATEGORY_SLUG = "general";
539
+ var MANIFEST_APP_NAME_ROW = /^\s*"appName"\s*:\s*"([^"]*)"/m;
540
+ var MANIFEST_CLIENT_ID_ROW = /^(\s*"clientId"\s*:\s*")([^"]*)("\s*,?)/m;
541
+ var signinResponseSchema = z2.object({
542
+ "accessToken": z2.string().min(1),
543
+ "developer": z2.object({
544
+ "id": z2.string().min(1),
545
+ "email": z2.string()
546
+ }).passthrough()
547
+ });
548
+ var applicationSummarySchema = z2.object({
549
+ "id": z2.string().min(1),
550
+ "name": z2.string(),
551
+ "clientId": z2.string().nullish()
552
+ }).passthrough();
553
+ var listApplicationsResponseSchema = z2.object({
554
+ "data": z2.object({
555
+ "applications": z2.array(applicationSummarySchema)
556
+ }).passthrough()
557
+ }).passthrough();
558
+ var createApplicationResponseSchema = z2.object({
559
+ "data": z2.object({
560
+ "id": z2.string().min(1),
561
+ "clientId": z2.string().nullish()
562
+ }).passthrough()
563
+ }).passthrough();
564
+ var credentialsResponseSchema = z2.object({
565
+ "data": z2.object({
566
+ "clientId": z2.string().min(1)
567
+ }).passthrough()
568
+ }).passthrough();
569
+ function computeSessionExpiresAt(obtainedAtIso) {
570
+ return new Date(
571
+ new Date(obtainedAtIso).getTime() + DEVELOPER_SESSION_TTL_MS
572
+ ).toISOString();
573
+ }
574
+ function defaultLinkPrompter() {
575
+ return {
576
+ async text(question) {
577
+ const rl = createInterface({
578
+ input: process.stdin,
579
+ output: process.stderr
580
+ });
581
+ try {
582
+ return (await rl.question(question)).trim();
583
+ } finally {
584
+ rl.close();
585
+ }
586
+ },
587
+ async masked(question) {
588
+ return new Promise((resolvePromise, rejectPromise) => {
589
+ const stdin = process.stdin;
590
+ process.stderr.write(question);
591
+ stdin.setRawMode?.(true);
592
+ stdin.resume();
593
+ let value = "";
594
+ const finish = (err) => {
595
+ stdin.off("data", onData);
596
+ stdin.setRawMode?.(false);
597
+ stdin.pause();
598
+ process.stderr.write("\n");
599
+ if (err !== void 0) {
600
+ rejectPromise(err);
601
+ } else {
602
+ resolvePromise(value);
603
+ }
604
+ };
605
+ const onData = (chunk) => {
606
+ for (const ch of chunk.toString("utf8")) {
607
+ if (ch === "\r" || ch === "\n") {
608
+ finish();
609
+ return;
610
+ }
611
+ if (ch === "") {
612
+ finish(new ValidationError("Sign-in cancelled."));
613
+ return;
614
+ }
615
+ if (ch === "\x7F" || ch === "\b") {
616
+ value = value.slice(0, -1);
617
+ continue;
618
+ }
619
+ if (ch >= " ") {
620
+ value += ch;
621
+ }
622
+ }
623
+ };
624
+ stdin.on("data", onData);
625
+ });
626
+ }
627
+ };
628
+ }
629
+ var SessionRejectedError = class extends Error {
630
+ constructor() {
631
+ super("session rejected");
632
+ }
633
+ };
634
+ async function httpJson(url, pathLabel, init, trace) {
635
+ const controller = new AbortController();
636
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
637
+ const method = init.method ?? "GET";
638
+ const started = Date.now();
639
+ let response;
640
+ try {
641
+ response = await fetch(url, { ...init, signal: controller.signal });
642
+ } catch {
643
+ throw new NetworkError("Could not reach the Ratio platform.");
644
+ } finally {
645
+ clearTimeout(timer);
646
+ }
647
+ trace(`${method} ${pathLabel} \u2192 ${response.status} (${Date.now() - started}ms)`);
648
+ let body;
649
+ try {
650
+ body = await response.json();
651
+ } catch {
652
+ body = void 0;
653
+ }
654
+ return { status: response.status, body };
655
+ }
656
+ function serverMessage(body) {
657
+ if (typeof body === "object" && body !== null && "message" in body) {
658
+ const message = body.message;
659
+ if (typeof message === "string" && message.length > 0) {
660
+ return message;
661
+ }
662
+ }
663
+ return void 0;
664
+ }
665
+ var AppLink = class _AppLink extends RatioCommand {
666
+ static description = "Link the app in the current directory to a Ratio app registration (sign in, pick or create the app, and write its clientId into ratio.config.jsonc).";
667
+ static examples = [
668
+ "<%= config.bin %> app link",
669
+ "<%= config.bin %> app link --email dev@example.com",
670
+ "<%= config.bin %> app link --app-id 4f9d2c31-8a6b-4f2e-9c1d-3e5a7b9d0f12 --json",
671
+ "<%= config.bin %> app link --client-id ratio_ci_1234"
672
+ ];
673
+ static flags = {
674
+ ...verboseFlag,
675
+ ...jsonFlag,
676
+ email: Flags2.string({
677
+ description: "Developer email for sign-in (prompted when omitted)."
678
+ }),
679
+ "client-id": Flags2.string({
680
+ description: "Write this clientId directly into ratio.config.jsonc without contacting the server."
681
+ }),
682
+ "app-id": Flags2.string({
683
+ description: "Attach to an existing app by id; its clientId is fetched and written."
684
+ }),
685
+ category: Flags2.string({
686
+ description: `Category slug used when creating a new app (default: ${DEFAULT_CATEGORY_SLUG}).`
687
+ })
688
+ };
689
+ // ── Seams: overridable in tests; defaults hit the real process. ──────────
690
+ writeOut = (chunk) => {
691
+ process.stdout.write(chunk);
692
+ };
693
+ writeErr = (chunk) => {
694
+ process.stderr.write(chunk);
695
+ };
696
+ makeCredentialStore = () => new FileCredentialStore();
697
+ prompter = defaultLinkPrompter();
698
+ isInteractive = () => process.stdin.isTTY === true;
699
+ envReader = () => process.env;
700
+ getCwd = () => process.cwd();
701
+ async run() {
702
+ const { flags } = await this.parse(_AppLink);
703
+ const jsonMode = flags.json === true;
704
+ const verbose = flags.verbose === true;
705
+ const trace = (message) => {
706
+ if (verbose) {
707
+ this.writeErr(`[verbose] ${message}
708
+ `);
709
+ }
710
+ };
711
+ const cwd = this.getCwd();
712
+ const manifest = this.readManifest(cwd);
713
+ const directClientId = flags["client-id"];
714
+ if (directClientId !== void 0 && directClientId !== "") {
715
+ await this.writeAndReport(cwd, directClientId, manifest.appName, jsonMode, trace);
716
+ return;
717
+ }
718
+ const store = this.makeCredentialStore();
719
+ const server = await this.resolveServer(store);
720
+ const bearer = await this.acquireBearer(store, server, flags.email, trace);
721
+ let clientId;
722
+ let appName = manifest.appName;
723
+ const appId = flags["app-id"];
724
+ if (appId !== void 0 && appId !== "") {
725
+ clientId = await this.withAuthRetry(
726
+ store,
727
+ server,
728
+ bearer,
729
+ trace,
730
+ (token) => this.fetchAppClientId(server, token, appId, trace)
731
+ );
732
+ } else {
733
+ const picked = await this.pickOrCreateApp(
734
+ store,
735
+ server,
736
+ bearer,
737
+ manifest.appName,
738
+ flags.category,
739
+ trace
740
+ );
741
+ clientId = picked.clientId;
742
+ appName = picked.appName ?? manifest.appName;
743
+ }
744
+ await this.writeAndReport(cwd, clientId, appName, jsonMode, trace);
745
+ }
746
+ // ── Manifest ──────────────────────────────────────────────────────────────
747
+ readManifest(cwd) {
748
+ let content;
749
+ try {
750
+ content = fs3.readFileSync(`${cwd}/${MANIFEST_FILE_NAME}`, "utf8");
751
+ } catch {
752
+ throw new ValidationError(
753
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
754
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
755
+ );
756
+ }
757
+ if (!MANIFEST_CLIENT_ID_ROW.test(content)) {
758
+ throw new ValidationError(
759
+ `${MANIFEST_FILE_NAME} has no top-level "clientId" entry.`,
760
+ 'Restore the "clientId" line from the app template, then re-run.'
761
+ );
762
+ }
763
+ const appName = MANIFEST_APP_NAME_ROW.exec(content)?.[1];
764
+ return appName !== void 0 ? { appName } : {};
765
+ }
766
+ async writeAndReport(cwd, clientId, appName, jsonMode, trace) {
767
+ const result = await writeClientId(cwd, clientId);
768
+ if (result.previousClientId !== void 0) {
769
+ this.writeErr(
770
+ `Warning: replacing existing clientId ${result.previousClientId} with ${clientId}
771
+ `
772
+ );
773
+ }
774
+ if (result.written) {
775
+ const bytes = fs3.statSync(result.path).size;
776
+ trace(`write ${MANIFEST_FILE_NAME} (${bytes}b)`);
777
+ }
778
+ const displayName = result.appName ?? appName ?? clientId;
779
+ if (jsonMode) {
780
+ const data = { "clientId": clientId };
781
+ if (result.appName !== void 0) {
782
+ data["appName"] = result.appName;
783
+ } else if (appName !== void 0) {
784
+ data["appName"] = appName;
785
+ }
786
+ data["written"] = result.written;
787
+ data["path"] = result.path;
788
+ this.writeOut(`${JSON.stringify({ ok: true, data })}
789
+ `);
790
+ return;
791
+ }
792
+ if (result.written) {
793
+ this.writeOut(
794
+ `Linked to app '${displayName}' (clientId: ${clientId}). Wrote to ${result.path}.
795
+ `
796
+ );
797
+ } else {
798
+ this.writeOut(
799
+ `Already linked to '${displayName}' (clientId: ${clientId}). No changes.
800
+ `
801
+ );
802
+ }
803
+ }
804
+ // ── Server + bearer acquisition ───────────────────────────────────────────
805
+ async resolveServer(store) {
806
+ try {
807
+ const creds = await store.load();
808
+ if (typeof creds.server === "string" && creds.server.length > 0) {
809
+ return creds.server;
810
+ }
811
+ } catch {
812
+ }
813
+ const envServer = this.envReader()[SERVER_ENV_VAR];
814
+ if (typeof envServer === "string" && envServer.length > 0) {
815
+ return envServer;
816
+ }
817
+ return DEFAULT_LINK_SERVER;
818
+ }
819
+ async acquireBearer(store, server, emailFlag, trace) {
820
+ const envToken = this.envReader()[DEVELOPER_TOKEN_ENV_VAR];
821
+ if (typeof envToken === "string" && envToken.length > 0) {
822
+ return { token: envToken, source: "env" };
823
+ }
824
+ try {
825
+ const token = await store.getValidDeveloperToken();
826
+ const state = { token, source: "file" };
827
+ let cachedEmail;
828
+ try {
829
+ const creds = await store.load();
830
+ cachedEmail = creds.developer?.email;
831
+ } catch {
832
+ }
833
+ const email = emailFlag ?? cachedEmail;
834
+ if (email !== void 0) {
835
+ state.email = email;
836
+ }
837
+ return state;
838
+ } catch (err) {
839
+ if (!(err instanceof NotLoggedInError)) {
840
+ throw err;
841
+ }
842
+ }
843
+ return this.signinInteractive(store, server, emailFlag, trace);
844
+ }
845
+ async signinInteractive(store, server, knownEmail, trace) {
846
+ if (!this.isInteractive()) {
847
+ throw new ValidationError(
848
+ "Developer sign-in needs an interactive terminal.",
849
+ "Non-interactive shell \u2014 set RATIO_DEVELOPER_TOKEN or run interactively."
850
+ );
851
+ }
852
+ const email = knownEmail !== void 0 && knownEmail !== "" ? knownEmail : await this.prompter.text("Developer email: ");
853
+ const password = await this.prompter.masked("Developer password: ");
854
+ const session = await this.signin(server, email, password, trace);
855
+ await store.saveDeveloperSession(session);
856
+ return { token: session.jwt, source: "signin", email };
857
+ }
858
+ async signin(server, email, password, trace) {
859
+ const base = server.replace(/\/+$/, "");
860
+ const { status, body } = await httpJson(
861
+ `${base}/api/v1/developers/signin`,
862
+ "/developers/signin",
863
+ {
864
+ method: "POST",
865
+ headers: { "content-type": "application/json" },
866
+ body: JSON.stringify({ "email": email, "password": password })
867
+ },
868
+ trace
869
+ );
870
+ if (status === 401) {
871
+ throw new NotLoggedInError(
872
+ "Invalid credentials or account not verified. Sign in via the developer portal to verify your email if you have not done so."
873
+ );
874
+ }
875
+ if (status === 429) {
876
+ throw new NetworkError(
877
+ "Too many signin attempts. Wait 60 seconds or set RATIO_DEVELOPER_TOKEN for CI usage."
878
+ );
879
+ }
880
+ if (status < 200 || status >= 300) {
881
+ throw new NetworkError(
882
+ `The server had a problem processing your request (status: ${status}); try again.`
883
+ );
884
+ }
885
+ const parsed = signinResponseSchema.safeParse(body);
886
+ if (!parsed.success) {
887
+ throw new NetworkError("The server response was not the expected shape.");
888
+ }
889
+ const obtainedAt = (/* @__PURE__ */ new Date()).toISOString();
890
+ return {
891
+ jwt: parsed.data.accessToken,
892
+ developerId: parsed.data.developer.id,
893
+ email: parsed.data.developer.email,
894
+ expiresAt: computeSessionExpiresAt(obtainedAt),
895
+ obtainedAt
896
+ };
897
+ }
898
+ /**
899
+ * Run one app-management call; on a 401 re-prompt the password ONCE
900
+ * (email already known when possible), retry ONCE, and fail typed on a
901
+ * second 401. Unattended runs (env token / non-TTY) fail typed directly.
902
+ */
903
+ async withAuthRetry(store, server, bearer, trace, call) {
904
+ const expired = () => new NotLoggedInError(
905
+ "Your developer session expired.",
906
+ "Your developer session expired. Sign in again by re-running `ratio app link`."
907
+ );
908
+ try {
909
+ return await call(bearer.token);
910
+ } catch (err) {
911
+ if (!(err instanceof SessionRejectedError)) {
912
+ throw err;
913
+ }
914
+ if (bearer.source === "env" || !this.isInteractive()) {
915
+ throw expired();
916
+ }
917
+ const email = bearer.email !== void 0 && bearer.email !== "" ? bearer.email : await this.prompter.text("Developer email: ");
918
+ const password = await this.prompter.masked("Developer password: ");
919
+ const session = await this.signin(server, email, password, trace);
920
+ await store.saveDeveloperSession(session);
921
+ bearer.token = session.jwt;
922
+ bearer.source = "signin";
923
+ bearer.email = email;
924
+ try {
925
+ return await call(bearer.token);
926
+ } catch (err2) {
927
+ if (err2 instanceof SessionRejectedError) {
928
+ throw expired();
929
+ }
930
+ throw err2;
931
+ }
932
+ }
933
+ }
934
+ // ── App-management calls ──────────────────────────────────────────────────
935
+ async fetchAppClientId(server, token, appId, trace) {
936
+ const base = server.replace(/\/+$/, "");
937
+ const { status, body } = await httpJson(
938
+ `${base}/api/v1/applications/${appId}/credentials`,
939
+ `/applications/${appId}/credentials`,
940
+ { method: "GET", headers: { Authorization: `Bearer ${token}` } },
941
+ trace
942
+ );
943
+ if (status === 401) {
944
+ throw new SessionRejectedError();
945
+ }
946
+ if (status === 404) {
947
+ throw new ValidationError("Selected app not found. It may have been deleted.");
948
+ }
949
+ if (status < 200 || status >= 300) {
950
+ throw new NetworkError(
951
+ `The server had a problem processing your request (status: ${status}); try again.`
952
+ );
953
+ }
954
+ const parsed = credentialsResponseSchema.safeParse(body);
955
+ if (!parsed.success) {
956
+ throw new NetworkError("The server response was not the expected shape.");
957
+ }
958
+ return parsed.data.data.clientId;
959
+ }
960
+ async listApplications(server, token, trace) {
961
+ const base = server.replace(/\/+$/, "");
962
+ const { status, body } = await httpJson(
963
+ `${base}/api/v1/applications`,
964
+ "/applications",
965
+ { method: "GET", headers: { Authorization: `Bearer ${token}` } },
966
+ trace
967
+ );
968
+ if (status === 401) {
969
+ throw new SessionRejectedError();
970
+ }
971
+ if (status < 200 || status >= 300) {
972
+ throw new NetworkError(
973
+ `The server had a problem processing your request (status: ${status}); try again.`
974
+ );
975
+ }
976
+ const parsed = listApplicationsResponseSchema.safeParse(body);
977
+ if (!parsed.success) {
978
+ throw new NetworkError("The server response was not the expected shape.");
979
+ }
980
+ return parsed.data.data.applications;
981
+ }
982
+ async createApplication(server, token, payload, trace) {
983
+ const base = server.replace(/\/+$/, "");
984
+ const { status, body } = await httpJson(
985
+ `${base}/api/v1/applications`,
986
+ "/applications",
987
+ {
988
+ method: "POST",
989
+ headers: {
990
+ Authorization: `Bearer ${token}`,
991
+ "content-type": "application/json"
992
+ },
993
+ body: JSON.stringify({
994
+ "name": payload.name,
995
+ "description": payload.description,
996
+ "categoryIds": payload.categoryIds
997
+ })
998
+ },
999
+ trace
1000
+ );
1001
+ if (status === 401) {
1002
+ throw new SessionRejectedError();
1003
+ }
1004
+ if (status === 400) {
1005
+ throw new ValidationError(
1006
+ serverMessage(body) ?? "The server rejected the request.",
1007
+ "Category resolution failed \u2014 visit the portal to seed categories or pass --category <slug> with a known slug."
1008
+ );
1009
+ }
1010
+ if (status < 200 || status >= 300) {
1011
+ throw new NetworkError(
1012
+ `The server had a problem processing your request (status: ${status}); try again.`
1013
+ );
1014
+ }
1015
+ const parsed = createApplicationResponseSchema.safeParse(body);
1016
+ if (!parsed.success) {
1017
+ throw new NetworkError("The server response was not the expected shape.");
1018
+ }
1019
+ const clientId = parsed.data.data.clientId;
1020
+ return {
1021
+ id: parsed.data.data.id,
1022
+ ...typeof clientId === "string" && clientId.length > 0 ? { clientId } : {}
1023
+ };
1024
+ }
1025
+ // ── Interactive pick / create flow ────────────────────────────────────────
1026
+ async pickOrCreateApp(store, server, bearer, manifestAppName, categoryFlag, trace) {
1027
+ if (!this.isInteractive()) {
1028
+ throw new ValidationError(
1029
+ "Choosing an app needs an interactive terminal.",
1030
+ "Non-interactive shell \u2014 pass --app-id <id> or --client-id <x>."
1031
+ );
1032
+ }
1033
+ const apps = await this.withAuthRetry(
1034
+ store,
1035
+ server,
1036
+ bearer,
1037
+ trace,
1038
+ (token) => this.listApplications(server, token, trace)
1039
+ );
1040
+ this.writeErr("0. Create new app\n");
1041
+ apps.forEach((app, index) => {
1042
+ const shortId = app.id.slice(0, 8);
1043
+ const shortClientId = typeof app.clientId === "string" && app.clientId.length > 0 ? app.clientId.slice(0, 8) : "-";
1044
+ this.writeErr(
1045
+ `${index + 1}. ${app.name} (id: ${shortId}, clientId: ${shortClientId})
1046
+ `
1047
+ );
1048
+ });
1049
+ const answer = await this.prompter.text("Select a number: ");
1050
+ const selection = Number.parseInt(answer, 10);
1051
+ if (Number.isNaN(selection) || selection < 0 || selection > apps.length || String(selection) !== answer.trim()) {
1052
+ throw new ValidationError(
1053
+ `Invalid selection: enter a number between 0 and ${apps.length}.`
1054
+ );
1055
+ }
1056
+ if (selection === 0) {
1057
+ return this.createFlow(store, server, bearer, manifestAppName, categoryFlag, trace);
1058
+ }
1059
+ const chosen = apps[selection - 1];
1060
+ if (chosen === void 0) {
1061
+ throw new ValidationError(
1062
+ `Invalid selection: enter a number between 0 and ${apps.length}.`
1063
+ );
1064
+ }
1065
+ const clientId = await this.withAuthRetry(
1066
+ store,
1067
+ server,
1068
+ bearer,
1069
+ trace,
1070
+ (token) => this.fetchAppClientId(server, token, chosen.id, trace)
1071
+ );
1072
+ return { clientId, appName: chosen.name };
1073
+ }
1074
+ async createFlow(store, server, bearer, manifestAppName, categoryFlag, trace) {
1075
+ const nameDefault = manifestAppName !== void 0 && manifestAppName !== "" ? manifestAppName : void 0;
1076
+ const nameAnswer = await this.prompter.text(
1077
+ nameDefault !== void 0 ? `App name [${nameDefault}]: ` : "App name: "
1078
+ );
1079
+ const name = nameAnswer !== "" ? nameAnswer : nameDefault ?? "";
1080
+ if (name === "") {
1081
+ throw new ValidationError("An app name is required.");
1082
+ }
1083
+ const descriptionAnswer = await this.prompter.text(
1084
+ `Description [${DEFAULT_APP_DESCRIPTION}]: `
1085
+ );
1086
+ const description = descriptionAnswer !== "" ? descriptionAnswer : DEFAULT_APP_DESCRIPTION;
1087
+ const categoryIds = [
1088
+ categoryFlag !== void 0 && categoryFlag !== "" ? categoryFlag : DEFAULT_CATEGORY_SLUG
1089
+ ];
1090
+ const created = await this.withAuthRetry(
1091
+ store,
1092
+ server,
1093
+ bearer,
1094
+ trace,
1095
+ (token) => this.createApplication(server, token, { name, description, categoryIds }, trace)
1096
+ );
1097
+ if (created.clientId !== void 0) {
1098
+ return { clientId: created.clientId, appName: name };
1099
+ }
1100
+ const clientId = await this.withAuthRetry(
1101
+ store,
1102
+ server,
1103
+ bearer,
1104
+ trace,
1105
+ (token) => this.fetchAppClientId(server, token, created.id, trace)
1106
+ );
1107
+ return { clientId, appName: name };
1108
+ }
1109
+ };
1110
+ export {
1111
+ DEFAULT_APP_DESCRIPTION,
1112
+ DEFAULT_CATEGORY_SLUG,
1113
+ DEFAULT_LINK_SERVER,
1114
+ DEVELOPER_SESSION_TTL_MS,
1115
+ REQUEST_TIMEOUT_MS,
1116
+ computeSessionExpiresAt,
1117
+ AppLink as default,
1118
+ defaultLinkPrompter
1119
+ };
1120
+ //# sourceMappingURL=link.js.map