@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.
- package/README.md +504 -0
- package/bin/run.mjs +5 -0
- package/dist/commands/__test__/throw.d.ts +25 -0
- package/dist/commands/__test__/throw.js +238 -0
- package/dist/commands/__test__/throw.js.map +1 -0
- package/dist/commands/app/create.d.ts +55 -0
- package/dist/commands/app/create.js +609 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.d.ts +14 -0
- package/dist/commands/app/deploy.js +179 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/dev.d.ts +28 -0
- package/dist/commands/app/dev.js +701 -0
- package/dist/commands/app/dev.js.map +1 -0
- package/dist/commands/app/generate.d.ts +14 -0
- package/dist/commands/app/generate.js +179 -0
- package/dist/commands/app/generate.js.map +1 -0
- package/dist/commands/app/info.d.ts +14 -0
- package/dist/commands/app/info.js +179 -0
- package/dist/commands/app/info.js.map +1 -0
- package/dist/commands/app/link.d.ts +118 -0
- package/dist/commands/app/link.js +1120 -0
- package/dist/commands/app/link.js.map +1 -0
- package/dist/commands/auth/login.d.ts +43 -0
- package/dist/commands/auth/login.js +987 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +15 -0
- package/dist/commands/auth/logout.js +486 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.d.ts +16 -0
- package/dist/commands/auth/whoami.js +531 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/dev/listen.d.ts +84 -0
- package/dist/commands/dev/listen.js +1187 -0
- package/dist/commands/dev/listen.js.map +1 -0
- package/dist/commands/dev/trigger.d.ts +88 -0
- package/dist/commands/dev/trigger.js +729 -0
- package/dist/commands/dev/trigger.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
- package/dist/lib/auth/authorize-code-exchange.js +114 -0
- package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
- package/dist/lib/auth/authorize-url.d.ts +42 -0
- package/dist/lib/auth/authorize-url.js +35 -0
- package/dist/lib/auth/authorize-url.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +16 -0
- package/dist/lib/auth/browser.js +48 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/config.d.ts +41 -0
- package/dist/lib/auth/config.js +21 -0
- package/dist/lib/auth/config.js.map +1 -0
- package/dist/lib/auth/http-refresher.d.ts +30 -0
- package/dist/lib/auth/http-refresher.js +103 -0
- package/dist/lib/auth/http-refresher.js.map +1 -0
- package/dist/lib/auth/identity.d.ts +26 -0
- package/dist/lib/auth/identity.js +10 -0
- package/dist/lib/auth/identity.js.map +1 -0
- package/dist/lib/auth/index.d.ts +13 -0
- package/dist/lib/auth/index.js +527 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/loopback-server.d.ts +65 -0
- package/dist/lib/auth/loopback-server.js +245 -0
- package/dist/lib/auth/loopback-server.js.map +1 -0
- package/dist/lib/auth/manual-prompt.d.ts +17 -0
- package/dist/lib/auth/manual-prompt.js +20 -0
- package/dist/lib/auth/manual-prompt.js.map +1 -0
- package/dist/lib/auth/pkce.d.ts +24 -0
- package/dist/lib/auth/pkce.js +15 -0
- package/dist/lib/auth/pkce.js.map +1 -0
- package/dist/lib/auth/state.d.ts +20 -0
- package/dist/lib/auth/state.js +19 -0
- package/dist/lib/auth/state.js.map +1 -0
- package/dist/lib/auth/verbose-trace.d.ts +25 -0
- package/dist/lib/auth/verbose-trace.js +17 -0
- package/dist/lib/auth/verbose-trace.js.map +1 -0
- package/dist/lib/credentials/clock.d.ts +18 -0
- package/dist/lib/credentials/clock.js +10 -0
- package/dist/lib/credentials/clock.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.js +340 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/path.d.ts +26 -0
- package/dist/lib/credentials/path.js +32 -0
- package/dist/lib/credentials/path.js.map +1 -0
- package/dist/lib/credentials/refresher.d.ts +26 -0
- package/dist/lib/credentials/refresher.js +34 -0
- package/dist/lib/credentials/refresher.js.map +1 -0
- package/dist/lib/credentials/schema.d.ts +138 -0
- package/dist/lib/credentials/schema.js +85 -0
- package/dist/lib/credentials/schema.js.map +1 -0
- package/dist/lib/credentials/store.d.ts +102 -0
- package/dist/lib/credentials/store.js +337 -0
- package/dist/lib/credentials/store.js.map +1 -0
- package/dist/lib/credentials/types.d.ts +65 -0
- package/dist/lib/credentials/types.js +1 -0
- package/dist/lib/credentials/types.js.map +1 -0
- package/dist/lib/dev-store-gate.d.ts +66 -0
- package/dist/lib/dev-store-gate.js +15 -0
- package/dist/lib/dev-store-gate.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.js +101 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +2 -0
- package/dist/lib/manifest/index.js +144 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/read-manifest.d.ts +32 -0
- package/dist/lib/manifest/read-manifest.js +87 -0
- package/dist/lib/manifest/read-manifest.js.map +1 -0
- package/dist/lib/manifest/write-client-id.d.ts +33 -0
- package/dist/lib/manifest/write-client-id.js +94 -0
- package/dist/lib/manifest/write-client-id.js.map +1 -0
- package/dist/lib/messages.d.ts +15 -0
- package/dist/lib/messages.js +16 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/orchestrator/child-runner.d.ts +140 -0
- package/dist/lib/orchestrator/child-runner.js +471 -0
- package/dist/lib/orchestrator/child-runner.js.map +1 -0
- package/dist/lib/preview/index.d.ts +11 -0
- package/dist/lib/preview/index.js +17 -0
- package/dist/lib/preview/index.js.map +1 -0
- package/dist/lib/preview/types.d.ts +16 -0
- package/dist/lib/preview/types.js +11 -0
- package/dist/lib/preview/types.js.map +1 -0
- package/dist/lib/ratio-command.d.ts +52 -0
- package/dist/lib/ratio-command.js +141 -0
- package/dist/lib/ratio-command.js.map +1 -0
- package/dist/lib/relay/index.d.ts +7 -0
- package/dist/lib/relay/index.js +362 -0
- package/dist/lib/relay/index.js.map +1 -0
- package/dist/lib/relay/relay-client.d.ts +91 -0
- package/dist/lib/relay/relay-client.js +362 -0
- package/dist/lib/relay/relay-client.js.map +1 -0
- package/dist/lib/relay/types.d.ts +71 -0
- package/dist/lib/relay/types.js +1 -0
- package/dist/lib/relay/types.js.map +1 -0
- package/dist/lib/render-error.d.ts +35 -0
- package/dist/lib/render-error.js +115 -0
- package/dist/lib/render-error.js.map +1 -0
- package/dist/lib/verbose-flag.d.ts +12 -0
- package/dist/lib/verbose-flag.js +17 -0
- package/dist/lib/verbose-flag.js.map +1 -0
- package/dist/render-CrHGqTPH.d.ts +115 -0
- package/dist/templates/.gitkeep +0 -0
- package/dist/templates/minimal/.env.example.tmpl +4 -0
- package/dist/templates/minimal/README.md.tmpl +26 -0
- package/dist/templates/minimal/gitignore +5 -0
- package/dist/templates/minimal/manifest.json +5 -0
- package/dist/templates/minimal/package.json.tmpl +25 -0
- package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/minimal/src/index.ts.tmpl +26 -0
- package/dist/templates/minimal/src/install.ts.tmpl +37 -0
- package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/minimal/tsconfig.json +17 -0
- package/dist/templates/serverless/README.md.tmpl +28 -0
- package/dist/templates/serverless/gitignore +6 -0
- package/dist/templates/serverless/manifest.json +5 -0
- package/dist/templates/serverless/package.json.tmpl +22 -0
- package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/serverless/src/index.ts.tmpl +20 -0
- package/dist/templates/serverless/src/install.ts.tmpl +20 -0
- package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
- package/dist/templates/serverless/tsconfig.json +18 -0
- package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
- package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
- package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
- package/dist/templates/with-admin-ui/gitignore +6 -0
- package/dist/templates/with-admin-ui/manifest.json +5 -0
- package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
- package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
- package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
- package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
- package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
- package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
- package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
- package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
- package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
- package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/with-admin-ui/tsconfig.json +17 -0
- 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
|