@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,609 @@
1
+ // src/commands/app/create.ts
2
+ import { existsSync, readFileSync, statSync } from "fs";
3
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { Args, Flags as Flags2 } from "@oclif/core";
6
+
7
+ // src/lib/errors.ts
8
+ var RatioCLIError = class extends Error {
9
+ code;
10
+ exitCode;
11
+ hint;
12
+ docsUrl;
13
+ constructor(opts) {
14
+ super(opts.message);
15
+ this.name = new.target.name;
16
+ this.code = opts.code;
17
+ this.exitCode = opts.exitCode;
18
+ if (opts.hint !== void 0) this.hint = opts.hint;
19
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
20
+ }
21
+ };
22
+ var TemplateExistsError = class extends RatioCLIError {
23
+ constructor(message, hint = "Choose a different name or remove the existing directory.") {
24
+ super({ code: "TEMPLATE_EXISTS", exitCode: 7, message, hint });
25
+ }
26
+ };
27
+ var ValidationError = class extends RatioCLIError {
28
+ constructor(message, hint) {
29
+ const opts = {
30
+ code: "VALIDATION_ERROR",
31
+ exitCode: 8,
32
+ message
33
+ };
34
+ if (hint !== void 0) opts.hint = hint;
35
+ super(opts);
36
+ }
37
+ };
38
+ function isRatioCLIError(err) {
39
+ return err instanceof RatioCLIError;
40
+ }
41
+
42
+ // src/lib/ratio-command.ts
43
+ import { Command, Errors } from "@oclif/core";
44
+
45
+ // src/lib/render-error.ts
46
+ function isOclifParserError(err) {
47
+ return err instanceof Error && "parse" in err && err.parse !== null && err.parse !== void 0;
48
+ }
49
+ function buildErrorEnvelope(code, message, hint, docsUrl) {
50
+ const obj = { ok: false, code, message };
51
+ if (hint !== void 0) obj["hint"] = hint;
52
+ if (docsUrl !== void 0) obj["docsUrl"] = docsUrl;
53
+ return JSON.stringify(obj) + "\n";
54
+ }
55
+ function renderError(input) {
56
+ const { err, verbose, json } = input;
57
+ if (isRatioCLIError(err)) {
58
+ const ratioErr = err;
59
+ if (ratioErr.exitCode === 0) {
60
+ if (json) {
61
+ return {
62
+ stdout: JSON.stringify({ ok: true, data: null }) + "\n",
63
+ stderr: "",
64
+ exitCode: 0
65
+ };
66
+ }
67
+ return {
68
+ stdout: ratioErr.message + "\n",
69
+ stderr: "",
70
+ exitCode: 0
71
+ };
72
+ }
73
+ let stderrLines = `${ratioErr.code}: ${ratioErr.message}
74
+ `;
75
+ if (ratioErr.hint !== void 0) stderrLines += `${ratioErr.hint}
76
+ `;
77
+ if (ratioErr.docsUrl !== void 0) stderrLines += `${ratioErr.docsUrl}
78
+ `;
79
+ if (json) {
80
+ return {
81
+ stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),
82
+ stderr: verbose && ratioErr.stack ? ratioErr.stack + "\n" : "",
83
+ exitCode: ratioErr.exitCode
84
+ };
85
+ }
86
+ if (verbose && ratioErr.stack) {
87
+ stderrLines += ratioErr.stack + "\n";
88
+ }
89
+ return { stdout: "", stderr: stderrLines, exitCode: ratioErr.exitCode };
90
+ }
91
+ if (isOclifParserError(err)) {
92
+ const parserErr = err;
93
+ const msg = parserErr.message || "Unknown parse error";
94
+ const coercedErr = new ValidationError(msg);
95
+ let stderrText2 = `${coercedErr.code}: ${msg}
96
+ `;
97
+ if (json) {
98
+ return {
99
+ stdout: buildErrorEnvelope(coercedErr.code, msg),
100
+ stderr: verbose && parserErr.stack ? parserErr.stack + "\n" : "",
101
+ exitCode: 8
102
+ };
103
+ }
104
+ if (verbose && parserErr.stack) {
105
+ stderrText2 += parserErr.stack + "\n";
106
+ }
107
+ return { stdout: "", stderr: stderrText2, exitCode: 8 };
108
+ }
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ const stack = err instanceof Error ? err.stack : void 0;
111
+ const stderrPrefix = `INTERNAL: ${message}
112
+ `;
113
+ if (json) {
114
+ return {
115
+ stdout: buildErrorEnvelope("INTERNAL", message),
116
+ stderr: verbose && stack ? stack + "\n" : "",
117
+ exitCode: 1
118
+ };
119
+ }
120
+ let stderrText = stderrPrefix;
121
+ if (verbose && stack) {
122
+ stderrText += stack + "\n";
123
+ }
124
+ return { stdout: "", stderr: stderrText, exitCode: 1 };
125
+ }
126
+
127
+ // src/lib/ratio-command.ts
128
+ var RatioCommand = class extends Command {
129
+ _getRenderFlags() {
130
+ const argv = this.argv;
131
+ return {
132
+ verbose: argv.includes("--verbose"),
133
+ json: argv.includes("--json")
134
+ };
135
+ }
136
+ async catch(err) {
137
+ const { verbose, json } = this._getRenderFlags();
138
+ const output = renderError({ err, verbose, json });
139
+ if (output.stdout) process.stdout.write(output.stdout);
140
+ if (output.stderr) process.stderr.write(output.stderr);
141
+ if (output.exitCode === 0) {
142
+ throw new Errors.ExitError(0);
143
+ }
144
+ err.skipOclifErrorHandling = true;
145
+ err.oclif = { exit: output.exitCode };
146
+ throw err;
147
+ }
148
+ };
149
+
150
+ // src/lib/verbose-flag.ts
151
+ import { Flags } from "@oclif/core";
152
+ var verboseFlag = {
153
+ verbose: Flags.boolean({
154
+ description: "Show diagnostic detail: HTTP status/timing lines for auth requests (stderr) and stack traces for internal errors."
155
+ })
156
+ };
157
+ var jsonFlag = {
158
+ json: Flags.boolean({
159
+ description: "Emit machine-readable JSON on stdout."
160
+ })
161
+ };
162
+
163
+ // src/scaffold/render.ts
164
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
165
+ import { dirname, join, resolve } from "path";
166
+ import { fileURLToPath } from "url";
167
+ var RatioTemplateError = class extends Error {
168
+ code;
169
+ templateName;
170
+ key;
171
+ constructor(opts) {
172
+ super(opts.message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
173
+ this.name = "RatioTemplateError";
174
+ this.code = opts.code;
175
+ if (opts.templateName !== void 0) {
176
+ this.templateName = opts.templateName;
177
+ }
178
+ if (opts.key !== void 0) {
179
+ this.key = opts.key;
180
+ }
181
+ }
182
+ };
183
+ var PLACEHOLDER_REGEX = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
184
+ var TMPL_SUFFIX = ".tmpl";
185
+ async function isDirectory(path) {
186
+ try {
187
+ return (await stat(path)).isDirectory();
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ async function discoverSourceRoot(sourceRootOverride) {
193
+ if (sourceRootOverride !== void 0) {
194
+ return sourceRootOverride;
195
+ }
196
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
197
+ const candidates = [join(moduleDir, "templates"), resolve(moduleDir, "..", "templates")];
198
+ for (const candidate of candidates) {
199
+ if (await isDirectory(candidate)) {
200
+ return candidate;
201
+ }
202
+ }
203
+ throw new RatioTemplateError({
204
+ code: "io_error",
205
+ message: `templates root not found (looked in: ${candidates.join(", ")})`
206
+ });
207
+ }
208
+ function assertManifestShape(raw, path) {
209
+ if (typeof raw !== "object" || raw === null || typeof raw.name !== "string" || typeof raw.description !== "string" || !Array.isArray(raw.substitutionKeys) || !raw.substitutionKeys.every(
210
+ (k) => typeof k === "string"
211
+ )) {
212
+ throw new RatioTemplateError({
213
+ code: "io_error",
214
+ message: `invalid template manifest at ${path}: expected { name, description, substitutionKeys[] }`
215
+ });
216
+ }
217
+ const shaped = raw;
218
+ return {
219
+ name: shaped.name,
220
+ description: shaped.description,
221
+ substitutionKeys: shaped.substitutionKeys
222
+ };
223
+ }
224
+ async function loadManifest(templateRoot) {
225
+ const manifestPath = join(templateRoot, "manifest.json");
226
+ let content;
227
+ try {
228
+ content = await readFile(manifestPath, "utf8");
229
+ } catch (err) {
230
+ throw new RatioTemplateError({
231
+ code: "io_error",
232
+ message: `failed to read template manifest at ${manifestPath}`,
233
+ cause: err
234
+ });
235
+ }
236
+ let parsed;
237
+ try {
238
+ parsed = JSON.parse(content);
239
+ } catch (err) {
240
+ throw new RatioTemplateError({
241
+ code: "io_error",
242
+ message: `template manifest at ${manifestPath} is not valid JSON`,
243
+ cause: err
244
+ });
245
+ }
246
+ return assertManifestShape(parsed, manifestPath);
247
+ }
248
+ function substituteContent(content, substitutions, allowedKeys, templateName) {
249
+ return content.replace(PLACEHOLDER_REGEX, (_match, key) => {
250
+ if (!allowedKeys.has(key)) {
251
+ throw new RatioTemplateError({
252
+ code: "unknown_substitution_key",
253
+ message: `placeholder {{${key}}} is not declared in the template manifest's substitutionKeys`,
254
+ templateName,
255
+ key
256
+ });
257
+ }
258
+ const value = substitutions[key];
259
+ if (value === void 0) {
260
+ throw new RatioTemplateError({
261
+ code: "missing_substitution_value",
262
+ message: `no substitution value provided for {{${key}}}`,
263
+ templateName,
264
+ key
265
+ });
266
+ }
267
+ return value;
268
+ });
269
+ }
270
+ async function walkTemplateTree(root, relative = "") {
271
+ const entries = await readdir(join(root, relative), { withFileTypes: true });
272
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
273
+ const files = [];
274
+ for (const entry of entries) {
275
+ const rel = relative === "" ? entry.name : join(relative, entry.name);
276
+ if (entry.isDirectory()) {
277
+ files.push(...await walkTemplateTree(root, rel));
278
+ } else {
279
+ files.push({ relativePath: rel, absolutePath: join(root, rel) });
280
+ }
281
+ }
282
+ return files;
283
+ }
284
+ async function renderTemplate(input) {
285
+ const sourceRoot = await discoverSourceRoot(input.sourceRootOverride);
286
+ const templateRoot = join(sourceRoot, input.templateName);
287
+ if (!await isDirectory(templateRoot)) {
288
+ throw new RatioTemplateError({
289
+ code: "template_not_found",
290
+ message: `template '${input.templateName}' not found under ${sourceRoot}`,
291
+ templateName: input.templateName
292
+ });
293
+ }
294
+ const manifest = await loadManifest(templateRoot);
295
+ const allowedKeys = new Set(manifest.substitutionKeys);
296
+ try {
297
+ const existing = await readdir(input.destinationDir);
298
+ if (existing.length > 0) {
299
+ throw new RatioTemplateError({
300
+ code: "destination_not_empty",
301
+ message: `destination directory ${input.destinationDir} is not empty`,
302
+ templateName: input.templateName
303
+ });
304
+ }
305
+ } catch (err) {
306
+ if (err instanceof RatioTemplateError) {
307
+ throw err;
308
+ }
309
+ if (err.code !== "ENOENT") {
310
+ throw new RatioTemplateError({
311
+ code: "io_error",
312
+ message: `failed to inspect destination directory ${input.destinationDir}`,
313
+ templateName: input.templateName,
314
+ cause: err
315
+ });
316
+ }
317
+ }
318
+ let files;
319
+ try {
320
+ files = await walkTemplateTree(templateRoot);
321
+ } catch (err) {
322
+ throw new RatioTemplateError({
323
+ code: "io_error",
324
+ message: `failed to walk template tree at ${templateRoot}`,
325
+ templateName: input.templateName,
326
+ cause: err
327
+ });
328
+ }
329
+ const filesWritten = [];
330
+ let bytesWritten = 0;
331
+ for (const file of files) {
332
+ const isTmpl = file.relativePath.endsWith(TMPL_SUFFIX);
333
+ const outRelative = isTmpl ? file.relativePath.slice(0, -TMPL_SUFFIX.length) : file.relativePath;
334
+ const isTopLevelGitignore = file.relativePath === "gitignore";
335
+ const effectiveOutRelative = isTopLevelGitignore ? ".gitignore" : outRelative;
336
+ const outPath = join(input.destinationDir, effectiveOutRelative);
337
+ let perFileBytes = 0;
338
+ if (isTmpl) {
339
+ let raw;
340
+ try {
341
+ raw = await readFile(file.absolutePath, "utf8");
342
+ } catch (err) {
343
+ throw new RatioTemplateError({
344
+ code: "io_error",
345
+ message: `failed to read template file ${file.absolutePath}`,
346
+ templateName: input.templateName,
347
+ cause: err
348
+ });
349
+ }
350
+ const rendered = substituteContent(
351
+ raw,
352
+ input.substitutions,
353
+ allowedKeys,
354
+ input.templateName
355
+ );
356
+ try {
357
+ await mkdir(dirname(outPath), { recursive: true });
358
+ await writeFile(outPath, rendered, "utf8");
359
+ } catch (err) {
360
+ throw new RatioTemplateError({
361
+ code: "io_error",
362
+ message: `failed to write rendered file ${outPath}`,
363
+ templateName: input.templateName,
364
+ cause: err
365
+ });
366
+ }
367
+ perFileBytes = Buffer.byteLength(rendered, "utf8");
368
+ bytesWritten += perFileBytes;
369
+ } else {
370
+ try {
371
+ await mkdir(dirname(outPath), { recursive: true });
372
+ await copyFile(file.absolutePath, outPath);
373
+ perFileBytes = (await stat(outPath)).size;
374
+ bytesWritten += perFileBytes;
375
+ } catch (err) {
376
+ throw new RatioTemplateError({
377
+ code: "io_error",
378
+ message: `failed to copy file ${file.absolutePath} to ${outPath}`,
379
+ templateName: input.templateName,
380
+ cause: err
381
+ });
382
+ }
383
+ }
384
+ filesWritten.push(outPath);
385
+ if (input.onFile !== void 0) {
386
+ try {
387
+ input.onFile(effectiveOutRelative, perFileBytes);
388
+ } catch (err) {
389
+ throw new RatioTemplateError({
390
+ code: "io_error",
391
+ message: "onFile callback threw",
392
+ templateName: input.templateName,
393
+ cause: err
394
+ });
395
+ }
396
+ }
397
+ }
398
+ return { filesWritten, bytesWritten };
399
+ }
400
+
401
+ // src/commands/app/create.ts
402
+ var TEMPLATE_OPTIONS = ["minimal", "with-admin-ui", "serverless"];
403
+ var APP_NAME_PLACEHOLDER = "<APP_NAME>";
404
+ function nextSteps(template) {
405
+ const common = {
406
+ cd: `cd ${APP_NAME_PLACEHOLDER}`,
407
+ install: "npm install",
408
+ config: "Open ratio.config.jsonc and paste the client id (or run 'ratio app link').",
409
+ endpoints: "Point your merchant test store at the /oauth/callback and /webhooks endpoints (use 'ratio dev listen' for local testing)."
410
+ };
411
+ if (template === "minimal") {
412
+ return [
413
+ common.cd,
414
+ common.install,
415
+ common.config,
416
+ "Copy .env.example to .env and fill RATIO_CLIENT_SECRET + RATIO_WEBHOOK_SECRET.",
417
+ "Run npm run dev to start the local server on port 3000.",
418
+ common.endpoints
419
+ ];
420
+ }
421
+ if (template === "with-admin-ui") {
422
+ return [
423
+ common.cd,
424
+ common.install,
425
+ common.config,
426
+ "Copy .env.example to .env and fill RATIO_CLIENT_SECRET + RATIO_WEBHOOK_SECRET.",
427
+ "Run npm run dev to start the API server on port 3000, and npm run admin:dev to start the admin UI on http://localhost:3001.",
428
+ common.endpoints
429
+ ];
430
+ }
431
+ return [
432
+ common.cd,
433
+ common.install,
434
+ common.config,
435
+ "Set your secrets with 'wrangler secret put RATIO_CLIENT_SECRET' and 'wrangler secret put RATIO_WEBHOOK_SECRET'.",
436
+ "Run npm run dev to start wrangler dev locally, then npm run deploy when ready.",
437
+ "Point your merchant test store at your workers.dev route's /oauth/callback and /webhooks endpoints."
438
+ ];
439
+ }
440
+ function formatNextSteps(template, appName) {
441
+ return nextSteps(template).map(
442
+ (step) => step.replace(APP_NAME_PLACEHOLDER, appName)
443
+ );
444
+ }
445
+ function readOwnSdkVersion(startDir = dirname2(fileURLToPath2(import.meta.url))) {
446
+ let dir = startDir;
447
+ for (; ; ) {
448
+ const candidate = join2(dir, "package.json");
449
+ if (existsSync(candidate)) {
450
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
451
+ const version = parsed.dependencies?.["@ratio-app/sdk"];
452
+ if (typeof version === "string" && version.length > 0) {
453
+ return version;
454
+ }
455
+ }
456
+ const parent = dirname2(dir);
457
+ if (parent === dir) {
458
+ throw new Error(
459
+ "Could not locate the CLI package.json with an @ratio-app/sdk dependency."
460
+ );
461
+ }
462
+ dir = parent;
463
+ }
464
+ }
465
+ function discoverTemplatesRoot(startDir = dirname2(fileURLToPath2(import.meta.url))) {
466
+ let dir = startDir;
467
+ for (; ; ) {
468
+ const candidate = join2(dir, "templates");
469
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
470
+ return candidate;
471
+ }
472
+ const atPackageRoot = existsSync(join2(dir, "package.json"));
473
+ const parent = dirname2(dir);
474
+ if (atPackageRoot || parent === dir) {
475
+ throw new Error(
476
+ `Could not locate the bundled templates directory (walked up from ${startDir}).`
477
+ );
478
+ }
479
+ dir = parent;
480
+ }
481
+ }
482
+ var AppCreate = class _AppCreate extends RatioCommand {
483
+ static description = "Create a new Ratio app from a template.";
484
+ static examples = [
485
+ "<%= config.bin %> app create my-app",
486
+ "<%= config.bin %> app create my-app --template=serverless",
487
+ "<%= config.bin %> app create my-app --json"
488
+ ];
489
+ static args = {
490
+ name: Args.string({
491
+ name: "name",
492
+ required: true,
493
+ description: "Directory name for the new Ratio app."
494
+ })
495
+ };
496
+ static flags = {
497
+ ...verboseFlag,
498
+ ...jsonFlag,
499
+ template: Flags2.string({
500
+ options: [...TEMPLATE_OPTIONS],
501
+ default: "minimal",
502
+ description: "Template to scaffold from."
503
+ })
504
+ };
505
+ // ── Seams: overridable in tests; defaults hit the real process. ──────────
506
+ writeOut = (chunk) => {
507
+ process.stdout.write(chunk);
508
+ };
509
+ writeErr = (chunk) => {
510
+ process.stderr.write(chunk);
511
+ };
512
+ renderTemplate = renderTemplate;
513
+ async run() {
514
+ const { args, flags } = await this.parse(_AppCreate);
515
+ const jsonMode = flags.json === true;
516
+ const verbose = flags.verbose === true;
517
+ const template = flags.template;
518
+ const trace = (message) => {
519
+ if (verbose) {
520
+ this.writeErr(`${message}
521
+ `);
522
+ }
523
+ };
524
+ const destinationDir = resolve2(process.cwd(), args.name);
525
+ const sdkVersion = readOwnSdkVersion();
526
+ trace(`template: ${template}`);
527
+ trace(`destination: ${destinationDir}`);
528
+ const substitutions = {
529
+ appName: args.name,
530
+ // Intentionally empty — `ratio app link` fills it later.
531
+ clientId: "",
532
+ sdkVersion,
533
+ templateName: template
534
+ };
535
+ let result;
536
+ const started = Date.now();
537
+ try {
538
+ const input = {
539
+ templateName: template,
540
+ destinationDir,
541
+ substitutions,
542
+ // The built CLI bundles this command with its own copy of the
543
+ // engine, so the engine's dist-relative discovery does not apply
544
+ // here; the command resolves the templates root itself.
545
+ sourceRootOverride: discoverTemplatesRoot(),
546
+ ...verbose ? {
547
+ onFile: (relativePath) => {
548
+ this.writeErr(`wrote ${relativePath}
549
+ `);
550
+ }
551
+ } : {}
552
+ };
553
+ result = await this.renderTemplate(input);
554
+ } catch (err) {
555
+ if (err instanceof RatioTemplateError) {
556
+ if (err.code === "destination_not_empty") {
557
+ throw new TemplateExistsError(`Directory '${args.name}' is not empty.`);
558
+ }
559
+ if (err.code === "template_not_found") {
560
+ throw new ValidationError(
561
+ `Unknown template '${template}'.`,
562
+ "Use --template=minimal|with-admin-ui|serverless."
563
+ );
564
+ }
565
+ throw new RatioCLIError({
566
+ code: "TEMPLATE_IO",
567
+ exitCode: 1,
568
+ message: err.message
569
+ });
570
+ }
571
+ throw err;
572
+ }
573
+ trace(`rendered in ${Date.now() - started}ms`);
574
+ const steps = formatNextSteps(template, args.name);
575
+ if (jsonMode) {
576
+ this.writeOut(
577
+ `${JSON.stringify({
578
+ ok: true,
579
+ data: {
580
+ template,
581
+ path: destinationDir,
582
+ filesWritten: result.filesWritten.length,
583
+ next: steps
584
+ }
585
+ })}
586
+ `
587
+ );
588
+ return;
589
+ }
590
+ const numbered = steps.map((step, i) => ` ${i + 1}. ${step}`).join("\n");
591
+ this.writeOut(
592
+ `Created ${args.name} from the ${template} template (${result.filesWritten.length} files, ${result.bytesWritten} bytes).
593
+
594
+ Next steps:
595
+ ${numbered}
596
+ `
597
+ );
598
+ }
599
+ };
600
+ export {
601
+ APP_NAME_PLACEHOLDER,
602
+ TEMPLATE_OPTIONS,
603
+ AppCreate as default,
604
+ discoverTemplatesRoot,
605
+ formatNextSteps,
606
+ nextSteps,
607
+ readOwnSdkVersion
608
+ };
609
+ //# sourceMappingURL=create.js.map