@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
package/dist/index.js ADDED
@@ -0,0 +1,617 @@
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
+
12
+ // src/lib/credentials/clock.ts
13
+ var SystemClock = class {
14
+ now() {
15
+ return Date.now();
16
+ }
17
+ };
18
+
19
+ // src/lib/credentials/path.ts
20
+ import os from "os";
21
+ import path from "path";
22
+ function snapshotPathEnv() {
23
+ const snapshot = {};
24
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
25
+ if (xdgConfigHome !== void 0) {
26
+ snapshot.XDG_CONFIG_HOME = xdgConfigHome;
27
+ }
28
+ const appData = process.env["APPDATA"];
29
+ if (appData !== void 0) {
30
+ snapshot.APPDATA = appData;
31
+ }
32
+ return snapshot;
33
+ }
34
+ function defaultCredentialsPath(platform, homedir, env) {
35
+ const resolvedPlatform = platform ?? process.platform;
36
+ const resolvedHome = homedir ?? os.homedir();
37
+ const resolvedEnv = env ?? snapshotPathEnv();
38
+ if (resolvedPlatform === "win32") {
39
+ const appData = resolvedEnv.APPDATA;
40
+ const base2 = appData !== void 0 && appData !== "" ? appData : path.win32.join(resolvedHome, "AppData", "Roaming");
41
+ return path.win32.join(base2, "ratio", "credentials");
42
+ }
43
+ const xdgConfigHome = resolvedEnv.XDG_CONFIG_HOME;
44
+ const base = typeof xdgConfigHome === "string" && xdgConfigHome !== "" ? xdgConfigHome : path.posix.join(resolvedHome, ".config");
45
+ return path.posix.join(base, "ratio", "credentials");
46
+ }
47
+
48
+ // src/lib/errors.ts
49
+ var RatioCLIError = class extends Error {
50
+ code;
51
+ exitCode;
52
+ hint;
53
+ docsUrl;
54
+ constructor(opts) {
55
+ super(opts.message);
56
+ this.name = new.target.name;
57
+ this.code = opts.code;
58
+ this.exitCode = opts.exitCode;
59
+ if (opts.hint !== void 0) this.hint = opts.hint;
60
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
61
+ }
62
+ };
63
+ var NotLoggedInError = class extends RatioCLIError {
64
+ constructor(message, hint = "Run 'ratio auth login' to sign in.") {
65
+ super({ code: "NOT_LOGGED_IN", exitCode: 3, message, hint });
66
+ }
67
+ };
68
+ var TokenExpiredError = class extends RatioCLIError {
69
+ constructor(message, hint = "Run 'ratio auth login' to refresh your session.") {
70
+ super({ code: "TOKEN_EXPIRED", exitCode: 3, 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
+
85
+ // src/lib/credentials/refresher.ts
86
+ var NotImplementedRefresher = class {
87
+ async refresh(refreshToken) {
88
+ void refreshToken;
89
+ throw new NotLoggedInError(
90
+ "Token refresh is not available yet in this CLI version."
91
+ );
92
+ }
93
+ };
94
+
95
+ // src/lib/credentials/store.ts
96
+ import crypto from "crypto";
97
+ import fs from "fs";
98
+ import { dirname } from "path";
99
+
100
+ // src/lib/credentials/schema.ts
101
+ import { z } from "zod";
102
+ var identitySchema = z.object({
103
+ developerId: z.string(),
104
+ email: z.string(),
105
+ // Token-derived fields captured at login. Optional (additive) so files
106
+ // written before these fields existed still validate as version 1.
107
+ merchantId: z.string().optional(),
108
+ scopes: z.array(z.string()).optional()
109
+ }).strict();
110
+ var profileSchema = z.object({
111
+ accessToken: z.string(),
112
+ refreshToken: z.string(),
113
+ expiresAt: z.string().datetime({ offset: true }),
114
+ identity: identitySchema,
115
+ obtainedAt: z.string().datetime({ offset: true })
116
+ }).strict();
117
+ var developerSessionSchema = z.object({
118
+ jwt: z.string(),
119
+ developerId: z.string(),
120
+ email: z.string(),
121
+ expiresAt: z.string().datetime({ offset: true }),
122
+ obtainedAt: z.string().datetime({ offset: true })
123
+ }).strict();
124
+ var credentialsSchema = z.object({
125
+ version: z.literal(1),
126
+ server: z.string(),
127
+ clientId: z.string(),
128
+ profiles: z.record(z.string(), profileSchema),
129
+ // Optional cached developer session. Additive so files written before
130
+ // this field existed still validate as version 1.
131
+ developer: developerSessionSchema.optional()
132
+ }).strict();
133
+ function parseCredentials(input) {
134
+ if (typeof input === "object" && input !== null && "version" in input) {
135
+ const version = input.version;
136
+ if (version !== 1) {
137
+ const label = typeof version === "number" ? String(version) : "unknown";
138
+ throw new ValidationError(
139
+ `credentials file version ${label} is unsupported by this CLI \u2014 upgrade the CLI or sign in again`
140
+ );
141
+ }
142
+ }
143
+ const result = credentialsSchema.safeParse(input);
144
+ if (!result.success) {
145
+ throw new ValidationError(
146
+ "credentials file schema mismatch \u2014 expected v1 shape"
147
+ );
148
+ }
149
+ return result.data;
150
+ }
151
+
152
+ // src/lib/credentials/store.ts
153
+ var REFRESH_WINDOW_MS = 3e5;
154
+ var ENV_TOKEN_VAR = "RATIO_API_TOKEN";
155
+ var ENV_DEVELOPER_TOKEN_VAR = "RATIO_DEVELOPER_TOKEN";
156
+ function hasErrnoCode(err, code) {
157
+ return err instanceof Error && err.code === code;
158
+ }
159
+ var FileCredentialStore = class {
160
+ filePath;
161
+ clock;
162
+ refresher;
163
+ envReader;
164
+ platform;
165
+ constructor(opts) {
166
+ this.filePath = opts?.filePath ?? defaultCredentialsPath();
167
+ this.clock = opts?.clock ?? new SystemClock();
168
+ this.refresher = opts?.refresher ?? new NotImplementedRefresher();
169
+ this.envReader = opts?.envReader ?? (() => process.env);
170
+ this.platform = opts?.platform ?? process.platform;
171
+ }
172
+ async load() {
173
+ this.reapplyFileModeIfDrifted();
174
+ let raw;
175
+ try {
176
+ raw = fs.readFileSync(this.filePath, "utf8");
177
+ } catch (err) {
178
+ if (hasErrnoCode(err, "ENOENT")) {
179
+ throw new NotLoggedInError("You are not signed in.");
180
+ }
181
+ throw err;
182
+ }
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(raw);
186
+ } catch {
187
+ throw new ValidationError(
188
+ 'credentials file is not valid JSON \u2014 run "ratio auth login" to reset'
189
+ );
190
+ }
191
+ return parseCredentials(parsed);
192
+ }
193
+ async save(creds) {
194
+ const dir = dirname(this.filePath);
195
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
196
+ if (this.platform !== "win32") {
197
+ fs.chmodSync(dir, 448);
198
+ }
199
+ const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
200
+ try {
201
+ fs.writeFileSync(tmpPath, JSON.stringify(creds, null, 2), {
202
+ mode: 384
203
+ });
204
+ fs.renameSync(tmpPath, this.filePath);
205
+ } catch (err) {
206
+ try {
207
+ fs.unlinkSync(tmpPath);
208
+ } catch {
209
+ }
210
+ throw err;
211
+ }
212
+ if (this.platform !== "win32") {
213
+ fs.chmodSync(this.filePath, 384);
214
+ }
215
+ }
216
+ async clear() {
217
+ try {
218
+ fs.unlinkSync(this.filePath);
219
+ } catch (err) {
220
+ if (hasErrnoCode(err, "ENOENT")) {
221
+ return;
222
+ }
223
+ throw err;
224
+ }
225
+ }
226
+ async getValidAccessToken(profileName) {
227
+ const envToken = this.envReader()[ENV_TOKEN_VAR];
228
+ if (typeof envToken === "string" && envToken.length > 0) {
229
+ return envToken;
230
+ }
231
+ const creds = await this.load();
232
+ const name = profileName ?? "default";
233
+ const profile = creds.profiles[name];
234
+ if (profile === void 0) {
235
+ throw new NotLoggedInError("You are not signed in.");
236
+ }
237
+ const msUntilExpiry = new Date(profile.expiresAt).getTime() - this.clock.now();
238
+ if (msUntilExpiry < REFRESH_WINDOW_MS) {
239
+ return this.refreshAndPersist(creds, name, profile);
240
+ }
241
+ return profile.accessToken;
242
+ }
243
+ async getValidDeveloperToken() {
244
+ const envToken = this.envReader()[ENV_DEVELOPER_TOKEN_VAR];
245
+ if (typeof envToken === "string" && envToken.length > 0) {
246
+ return envToken;
247
+ }
248
+ let creds;
249
+ try {
250
+ creds = await this.load();
251
+ } catch (err) {
252
+ if (err instanceof NotLoggedInError) {
253
+ throw new NotLoggedInError("Developer signin required.");
254
+ }
255
+ throw err;
256
+ }
257
+ const session = creds.developer;
258
+ if (session === void 0) {
259
+ throw new NotLoggedInError("Developer signin required.");
260
+ }
261
+ const msUntilExpiry = new Date(session.expiresAt).getTime() - this.clock.now();
262
+ if (msUntilExpiry < REFRESH_WINDOW_MS) {
263
+ throw new NotLoggedInError("Developer signin required.");
264
+ }
265
+ return session.jwt;
266
+ }
267
+ async saveDeveloperSession(session) {
268
+ let creds;
269
+ try {
270
+ creds = await this.load();
271
+ } catch (err) {
272
+ if (err instanceof NotLoggedInError) {
273
+ creds = { version: 1, server: "", clientId: "", profiles: {} };
274
+ } else {
275
+ throw err;
276
+ }
277
+ }
278
+ const updated = {
279
+ ...creds,
280
+ developer: session
281
+ };
282
+ await this.save(updated);
283
+ }
284
+ /**
285
+ * Exchange the profile's refresh token for a fresh token set, merge it
286
+ * into the loaded credentials (preserving every other profile), persist
287
+ * atomically, and return the new access token. A refresher failure is
288
+ * wrapped in TokenExpiredError; the underlying message is deliberately
289
+ * discarded so a secret can never leak through the error surface.
290
+ */
291
+ async refreshAndPersist(creds, name, profile) {
292
+ let refreshed;
293
+ try {
294
+ refreshed = await this.refresher.refresh(profile.refreshToken);
295
+ } catch {
296
+ throw new TokenExpiredError(
297
+ "Your session has expired and could not be refreshed."
298
+ );
299
+ }
300
+ const updated = {
301
+ ...creds,
302
+ profiles: {
303
+ ...creds.profiles,
304
+ [name]: {
305
+ ...profile,
306
+ accessToken: refreshed.accessToken,
307
+ refreshToken: refreshed.refreshToken,
308
+ expiresAt: refreshed.expiresAt
309
+ }
310
+ }
311
+ };
312
+ await this.save(updated);
313
+ return refreshed.accessToken;
314
+ }
315
+ /**
316
+ * POSIX-only startup permission check: if the file exists with a mode
317
+ * other than 0600, re-apply 0600 and emit a warning to stderr. Never
318
+ * runs on Windows (stat modes there do not map to POSIX bits and chmod
319
+ * must not be called). Missing file is NOT an error here — load()
320
+ * reports it as NotLoggedInError.
321
+ */
322
+ reapplyFileModeIfDrifted() {
323
+ if (this.platform === "win32") {
324
+ return;
325
+ }
326
+ let stat2;
327
+ try {
328
+ stat2 = fs.statSync(this.filePath);
329
+ } catch (err) {
330
+ if (hasErrnoCode(err, "ENOENT")) {
331
+ return;
332
+ }
333
+ throw err;
334
+ }
335
+ const mode = stat2.mode & 511;
336
+ if (mode !== 384) {
337
+ fs.chmodSync(this.filePath, 384);
338
+ process.stderr.write(
339
+ `[warn] credentials file permissions were 0o${mode.toString(8)}; corrected to 0600.
340
+ `
341
+ );
342
+ }
343
+ }
344
+ };
345
+
346
+ // src/scaffold/render.ts
347
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
348
+ import { dirname as dirname2, join, resolve } from "path";
349
+ import { fileURLToPath } from "url";
350
+ var RatioTemplateError = class extends Error {
351
+ code;
352
+ templateName;
353
+ key;
354
+ constructor(opts) {
355
+ super(opts.message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
356
+ this.name = "RatioTemplateError";
357
+ this.code = opts.code;
358
+ if (opts.templateName !== void 0) {
359
+ this.templateName = opts.templateName;
360
+ }
361
+ if (opts.key !== void 0) {
362
+ this.key = opts.key;
363
+ }
364
+ }
365
+ };
366
+ var PLACEHOLDER_REGEX = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
367
+ var TMPL_SUFFIX = ".tmpl";
368
+ async function isDirectory(path2) {
369
+ try {
370
+ return (await stat(path2)).isDirectory();
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+ async function discoverSourceRoot(sourceRootOverride) {
376
+ if (sourceRootOverride !== void 0) {
377
+ return sourceRootOverride;
378
+ }
379
+ const moduleDir = dirname2(fileURLToPath(import.meta.url));
380
+ const candidates = [join(moduleDir, "templates"), resolve(moduleDir, "..", "templates")];
381
+ for (const candidate of candidates) {
382
+ if (await isDirectory(candidate)) {
383
+ return candidate;
384
+ }
385
+ }
386
+ throw new RatioTemplateError({
387
+ code: "io_error",
388
+ message: `templates root not found (looked in: ${candidates.join(", ")})`
389
+ });
390
+ }
391
+ function assertManifestShape(raw, path2) {
392
+ if (typeof raw !== "object" || raw === null || typeof raw.name !== "string" || typeof raw.description !== "string" || !Array.isArray(raw.substitutionKeys) || !raw.substitutionKeys.every(
393
+ (k) => typeof k === "string"
394
+ )) {
395
+ throw new RatioTemplateError({
396
+ code: "io_error",
397
+ message: `invalid template manifest at ${path2}: expected { name, description, substitutionKeys[] }`
398
+ });
399
+ }
400
+ const shaped = raw;
401
+ return {
402
+ name: shaped.name,
403
+ description: shaped.description,
404
+ substitutionKeys: shaped.substitutionKeys
405
+ };
406
+ }
407
+ async function loadManifest(templateRoot) {
408
+ const manifestPath = join(templateRoot, "manifest.json");
409
+ let content;
410
+ try {
411
+ content = await readFile(manifestPath, "utf8");
412
+ } catch (err) {
413
+ throw new RatioTemplateError({
414
+ code: "io_error",
415
+ message: `failed to read template manifest at ${manifestPath}`,
416
+ cause: err
417
+ });
418
+ }
419
+ let parsed;
420
+ try {
421
+ parsed = JSON.parse(content);
422
+ } catch (err) {
423
+ throw new RatioTemplateError({
424
+ code: "io_error",
425
+ message: `template manifest at ${manifestPath} is not valid JSON`,
426
+ cause: err
427
+ });
428
+ }
429
+ return assertManifestShape(parsed, manifestPath);
430
+ }
431
+ function substituteContent(content, substitutions, allowedKeys, templateName) {
432
+ return content.replace(PLACEHOLDER_REGEX, (_match, key) => {
433
+ if (!allowedKeys.has(key)) {
434
+ throw new RatioTemplateError({
435
+ code: "unknown_substitution_key",
436
+ message: `placeholder {{${key}}} is not declared in the template manifest's substitutionKeys`,
437
+ templateName,
438
+ key
439
+ });
440
+ }
441
+ const value = substitutions[key];
442
+ if (value === void 0) {
443
+ throw new RatioTemplateError({
444
+ code: "missing_substitution_value",
445
+ message: `no substitution value provided for {{${key}}}`,
446
+ templateName,
447
+ key
448
+ });
449
+ }
450
+ return value;
451
+ });
452
+ }
453
+ async function walkTemplateTree(root, relative = "") {
454
+ const entries = await readdir(join(root, relative), { withFileTypes: true });
455
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
456
+ const files = [];
457
+ for (const entry of entries) {
458
+ const rel = relative === "" ? entry.name : join(relative, entry.name);
459
+ if (entry.isDirectory()) {
460
+ files.push(...await walkTemplateTree(root, rel));
461
+ } else {
462
+ files.push({ relativePath: rel, absolutePath: join(root, rel) });
463
+ }
464
+ }
465
+ return files;
466
+ }
467
+ async function listTemplates(sourceRootOverride) {
468
+ const sourceRoot = await discoverSourceRoot(sourceRootOverride);
469
+ let entries;
470
+ try {
471
+ entries = await readdir(sourceRoot, { withFileTypes: true });
472
+ } catch (err) {
473
+ throw new RatioTemplateError({
474
+ code: "io_error",
475
+ message: `failed to read templates root at ${sourceRoot}`,
476
+ cause: err
477
+ });
478
+ }
479
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
480
+ const manifests = [];
481
+ for (const dir of dirs) {
482
+ manifests.push(await loadManifest(join(sourceRoot, dir)));
483
+ }
484
+ return manifests;
485
+ }
486
+ async function renderTemplate(input) {
487
+ const sourceRoot = await discoverSourceRoot(input.sourceRootOverride);
488
+ const templateRoot = join(sourceRoot, input.templateName);
489
+ if (!await isDirectory(templateRoot)) {
490
+ throw new RatioTemplateError({
491
+ code: "template_not_found",
492
+ message: `template '${input.templateName}' not found under ${sourceRoot}`,
493
+ templateName: input.templateName
494
+ });
495
+ }
496
+ const manifest = await loadManifest(templateRoot);
497
+ const allowedKeys = new Set(manifest.substitutionKeys);
498
+ try {
499
+ const existing = await readdir(input.destinationDir);
500
+ if (existing.length > 0) {
501
+ throw new RatioTemplateError({
502
+ code: "destination_not_empty",
503
+ message: `destination directory ${input.destinationDir} is not empty`,
504
+ templateName: input.templateName
505
+ });
506
+ }
507
+ } catch (err) {
508
+ if (err instanceof RatioTemplateError) {
509
+ throw err;
510
+ }
511
+ if (err.code !== "ENOENT") {
512
+ throw new RatioTemplateError({
513
+ code: "io_error",
514
+ message: `failed to inspect destination directory ${input.destinationDir}`,
515
+ templateName: input.templateName,
516
+ cause: err
517
+ });
518
+ }
519
+ }
520
+ let files;
521
+ try {
522
+ files = await walkTemplateTree(templateRoot);
523
+ } catch (err) {
524
+ throw new RatioTemplateError({
525
+ code: "io_error",
526
+ message: `failed to walk template tree at ${templateRoot}`,
527
+ templateName: input.templateName,
528
+ cause: err
529
+ });
530
+ }
531
+ const filesWritten = [];
532
+ let bytesWritten = 0;
533
+ for (const file of files) {
534
+ const isTmpl = file.relativePath.endsWith(TMPL_SUFFIX);
535
+ const outRelative = isTmpl ? file.relativePath.slice(0, -TMPL_SUFFIX.length) : file.relativePath;
536
+ const isTopLevelGitignore = file.relativePath === "gitignore";
537
+ const effectiveOutRelative = isTopLevelGitignore ? ".gitignore" : outRelative;
538
+ const outPath = join(input.destinationDir, effectiveOutRelative);
539
+ let perFileBytes = 0;
540
+ if (isTmpl) {
541
+ let raw;
542
+ try {
543
+ raw = await readFile(file.absolutePath, "utf8");
544
+ } catch (err) {
545
+ throw new RatioTemplateError({
546
+ code: "io_error",
547
+ message: `failed to read template file ${file.absolutePath}`,
548
+ templateName: input.templateName,
549
+ cause: err
550
+ });
551
+ }
552
+ const rendered = substituteContent(
553
+ raw,
554
+ input.substitutions,
555
+ allowedKeys,
556
+ input.templateName
557
+ );
558
+ try {
559
+ await mkdir(dirname2(outPath), { recursive: true });
560
+ await writeFile(outPath, rendered, "utf8");
561
+ } catch (err) {
562
+ throw new RatioTemplateError({
563
+ code: "io_error",
564
+ message: `failed to write rendered file ${outPath}`,
565
+ templateName: input.templateName,
566
+ cause: err
567
+ });
568
+ }
569
+ perFileBytes = Buffer.byteLength(rendered, "utf8");
570
+ bytesWritten += perFileBytes;
571
+ } else {
572
+ try {
573
+ await mkdir(dirname2(outPath), { recursive: true });
574
+ await copyFile(file.absolutePath, outPath);
575
+ perFileBytes = (await stat(outPath)).size;
576
+ bytesWritten += perFileBytes;
577
+ } catch (err) {
578
+ throw new RatioTemplateError({
579
+ code: "io_error",
580
+ message: `failed to copy file ${file.absolutePath} to ${outPath}`,
581
+ templateName: input.templateName,
582
+ cause: err
583
+ });
584
+ }
585
+ }
586
+ filesWritten.push(outPath);
587
+ if (input.onFile !== void 0) {
588
+ try {
589
+ input.onFile(effectiveOutRelative, perFileBytes);
590
+ } catch (err) {
591
+ throw new RatioTemplateError({
592
+ code: "io_error",
593
+ message: "onFile callback threw",
594
+ templateName: input.templateName,
595
+ cause: err
596
+ });
597
+ }
598
+ }
599
+ }
600
+ return { filesWritten, bytesWritten };
601
+ }
602
+
603
+ // src/index.ts
604
+ var CLI_NAME = "@ratio-app/cli";
605
+ export {
606
+ CLI_NAME,
607
+ FileCredentialStore,
608
+ NoopDevStoreGate,
609
+ NotImplementedRefresher,
610
+ RatioTemplateError,
611
+ SystemClock,
612
+ defaultCredentialsPath,
613
+ getDevStoreGate,
614
+ listTemplates,
615
+ renderTemplate
616
+ };
617
+ //# sourceMappingURL=index.js.map