@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,52 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all Ratio CLI commands.
|
|
5
|
+
*
|
|
6
|
+
* Overrides catch() to intercept every error thrown from run(), render it
|
|
7
|
+
* per the Ratio CLI error-UX spec (typed hierarchy -> structured stderr/stdout,
|
|
8
|
+
* --verbose for stack traces, --json for machine-readable envelope), and then
|
|
9
|
+
* suppress oclif's own double-render by setting skipOclifErrorHandling on the
|
|
10
|
+
* re-thrown error.
|
|
11
|
+
*
|
|
12
|
+
* Path B: base-class override — no oclif.hooks wiring needed.
|
|
13
|
+
* oclif v4 does not expose a command_error hook; the reliable interception
|
|
14
|
+
* point is Command.catch(), which is called by Command._run() before the
|
|
15
|
+
* error bubbles up to oclif's global handle() function.
|
|
16
|
+
*
|
|
17
|
+
* Double-render prevention:
|
|
18
|
+
* oclif's handle() checks err.skipOclifErrorHandling before printing.
|
|
19
|
+
* After rendering we set skipOclifErrorHandling = true so oclif's
|
|
20
|
+
* pretty-print is skipped. We also set err.oclif.exit = N so handle()
|
|
21
|
+
* calls process.exit(N) with the correct code.
|
|
22
|
+
*
|
|
23
|
+
* Exit-code routing:
|
|
24
|
+
* - exitCode === 0 (ComingSoonError): throw ExitError(0) so oclif calls
|
|
25
|
+
* process.exit(0) cleanly. ExitError has code 'EEXIT' and is treated as
|
|
26
|
+
* a clean exit — shouldPrint is false in handle().
|
|
27
|
+
* - exitCode > 0: mark err.skipOclifErrorHandling = true and set
|
|
28
|
+
* err.oclif = { exit: N }, then re-throw so handle() exits N.
|
|
29
|
+
*
|
|
30
|
+
* Verbose/json detection strategy:
|
|
31
|
+
* We scan this.argv directly. this.argv is set by oclif's Command before
|
|
32
|
+
* run() is called and always contains the raw argument list. This approach
|
|
33
|
+
* works both for:
|
|
34
|
+
* (a) post-parse errors (thrown after this.parse() succeeds) — argv has the
|
|
35
|
+
* flags that were successfully recognised.
|
|
36
|
+
* (b) pre-parse errors (thrown BY this.parse(), e.g. unknown flag) — argv
|
|
37
|
+
* still contains --verbose / --json even though parse() threw.
|
|
38
|
+
* Simple string scan is sufficient since --verbose and --json are boolean
|
|
39
|
+
* flags with no value argument.
|
|
40
|
+
*/
|
|
41
|
+
declare abstract class RatioCommand extends Command {
|
|
42
|
+
private _getRenderFlags;
|
|
43
|
+
catch(err: Error & {
|
|
44
|
+
exitCode?: number;
|
|
45
|
+
oclif?: {
|
|
46
|
+
exit?: number;
|
|
47
|
+
};
|
|
48
|
+
skipOclifErrorHandling?: boolean;
|
|
49
|
+
}): Promise<never>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { RatioCommand };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/lib/ratio-command.ts
|
|
2
|
+
import { Command, Errors } from "@oclif/core";
|
|
3
|
+
|
|
4
|
+
// src/lib/errors.ts
|
|
5
|
+
var RatioCLIError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
exitCode;
|
|
8
|
+
hint;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.exitCode = opts.exitCode;
|
|
15
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
16
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ValidationError = class extends RatioCLIError {
|
|
20
|
+
constructor(message, hint) {
|
|
21
|
+
const opts = {
|
|
22
|
+
code: "VALIDATION_ERROR",
|
|
23
|
+
exitCode: 8,
|
|
24
|
+
message
|
|
25
|
+
};
|
|
26
|
+
if (hint !== void 0) opts.hint = hint;
|
|
27
|
+
super(opts);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function isRatioCLIError(err) {
|
|
31
|
+
return err instanceof RatioCLIError;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/lib/render-error.ts
|
|
35
|
+
function isOclifParserError(err) {
|
|
36
|
+
return err instanceof Error && "parse" in err && err.parse !== null && err.parse !== void 0;
|
|
37
|
+
}
|
|
38
|
+
function buildErrorEnvelope(code, message, hint, docsUrl) {
|
|
39
|
+
const obj = { ok: false, code, message };
|
|
40
|
+
if (hint !== void 0) obj["hint"] = hint;
|
|
41
|
+
if (docsUrl !== void 0) obj["docsUrl"] = docsUrl;
|
|
42
|
+
return JSON.stringify(obj) + "\n";
|
|
43
|
+
}
|
|
44
|
+
function renderError(input) {
|
|
45
|
+
const { err, verbose, json } = input;
|
|
46
|
+
if (isRatioCLIError(err)) {
|
|
47
|
+
const ratioErr = err;
|
|
48
|
+
if (ratioErr.exitCode === 0) {
|
|
49
|
+
if (json) {
|
|
50
|
+
return {
|
|
51
|
+
stdout: JSON.stringify({ ok: true, data: null }) + "\n",
|
|
52
|
+
stderr: "",
|
|
53
|
+
exitCode: 0
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
stdout: ratioErr.message + "\n",
|
|
58
|
+
stderr: "",
|
|
59
|
+
exitCode: 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let stderrLines = `${ratioErr.code}: ${ratioErr.message}
|
|
63
|
+
`;
|
|
64
|
+
if (ratioErr.hint !== void 0) stderrLines += `${ratioErr.hint}
|
|
65
|
+
`;
|
|
66
|
+
if (ratioErr.docsUrl !== void 0) stderrLines += `${ratioErr.docsUrl}
|
|
67
|
+
`;
|
|
68
|
+
if (json) {
|
|
69
|
+
return {
|
|
70
|
+
stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),
|
|
71
|
+
stderr: verbose && ratioErr.stack ? ratioErr.stack + "\n" : "",
|
|
72
|
+
exitCode: ratioErr.exitCode
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (verbose && ratioErr.stack) {
|
|
76
|
+
stderrLines += ratioErr.stack + "\n";
|
|
77
|
+
}
|
|
78
|
+
return { stdout: "", stderr: stderrLines, exitCode: ratioErr.exitCode };
|
|
79
|
+
}
|
|
80
|
+
if (isOclifParserError(err)) {
|
|
81
|
+
const parserErr = err;
|
|
82
|
+
const msg = parserErr.message || "Unknown parse error";
|
|
83
|
+
const coercedErr = new ValidationError(msg);
|
|
84
|
+
let stderrText2 = `${coercedErr.code}: ${msg}
|
|
85
|
+
`;
|
|
86
|
+
if (json) {
|
|
87
|
+
return {
|
|
88
|
+
stdout: buildErrorEnvelope(coercedErr.code, msg),
|
|
89
|
+
stderr: verbose && parserErr.stack ? parserErr.stack + "\n" : "",
|
|
90
|
+
exitCode: 8
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (verbose && parserErr.stack) {
|
|
94
|
+
stderrText2 += parserErr.stack + "\n";
|
|
95
|
+
}
|
|
96
|
+
return { stdout: "", stderr: stderrText2, exitCode: 8 };
|
|
97
|
+
}
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
100
|
+
const stderrPrefix = `INTERNAL: ${message}
|
|
101
|
+
`;
|
|
102
|
+
if (json) {
|
|
103
|
+
return {
|
|
104
|
+
stdout: buildErrorEnvelope("INTERNAL", message),
|
|
105
|
+
stderr: verbose && stack ? stack + "\n" : "",
|
|
106
|
+
exitCode: 1
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
let stderrText = stderrPrefix;
|
|
110
|
+
if (verbose && stack) {
|
|
111
|
+
stderrText += stack + "\n";
|
|
112
|
+
}
|
|
113
|
+
return { stdout: "", stderr: stderrText, exitCode: 1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/lib/ratio-command.ts
|
|
117
|
+
var RatioCommand = class extends Command {
|
|
118
|
+
_getRenderFlags() {
|
|
119
|
+
const argv = this.argv;
|
|
120
|
+
return {
|
|
121
|
+
verbose: argv.includes("--verbose"),
|
|
122
|
+
json: argv.includes("--json")
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async catch(err) {
|
|
126
|
+
const { verbose, json } = this._getRenderFlags();
|
|
127
|
+
const output = renderError({ err, verbose, json });
|
|
128
|
+
if (output.stdout) process.stdout.write(output.stdout);
|
|
129
|
+
if (output.stderr) process.stderr.write(output.stderr);
|
|
130
|
+
if (output.exitCode === 0) {
|
|
131
|
+
throw new Errors.ExitError(0);
|
|
132
|
+
}
|
|
133
|
+
err.skipOclifErrorHandling = true;
|
|
134
|
+
err.oclif = { exit: output.exitCode };
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
export {
|
|
139
|
+
RatioCommand
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=ratio-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/ratio-command.ts","../../src/lib/errors.ts","../../src/lib/render-error.ts"],"sourcesContent":["import { Command, Errors } from '@oclif/core';\nimport { renderError } from './render-error.js';\n\n/**\n * Base class for all Ratio CLI commands.\n *\n * Overrides catch() to intercept every error thrown from run(), render it\n * per the Ratio CLI error-UX spec (typed hierarchy -> structured stderr/stdout,\n * --verbose for stack traces, --json for machine-readable envelope), and then\n * suppress oclif's own double-render by setting skipOclifErrorHandling on the\n * re-thrown error.\n *\n * Path B: base-class override — no oclif.hooks wiring needed.\n * oclif v4 does not expose a command_error hook; the reliable interception\n * point is Command.catch(), which is called by Command._run() before the\n * error bubbles up to oclif's global handle() function.\n *\n * Double-render prevention:\n * oclif's handle() checks err.skipOclifErrorHandling before printing.\n * After rendering we set skipOclifErrorHandling = true so oclif's\n * pretty-print is skipped. We also set err.oclif.exit = N so handle()\n * calls process.exit(N) with the correct code.\n *\n * Exit-code routing:\n * - exitCode === 0 (ComingSoonError): throw ExitError(0) so oclif calls\n * process.exit(0) cleanly. ExitError has code 'EEXIT' and is treated as\n * a clean exit — shouldPrint is false in handle().\n * - exitCode > 0: mark err.skipOclifErrorHandling = true and set\n * err.oclif = { exit: N }, then re-throw so handle() exits N.\n *\n * Verbose/json detection strategy:\n * We scan this.argv directly. this.argv is set by oclif's Command before\n * run() is called and always contains the raw argument list. This approach\n * works both for:\n * (a) post-parse errors (thrown after this.parse() succeeds) — argv has the\n * flags that were successfully recognised.\n * (b) pre-parse errors (thrown BY this.parse(), e.g. unknown flag) — argv\n * still contains --verbose / --json even though parse() threw.\n * Simple string scan is sufficient since --verbose and --json are boolean\n * flags with no value argument.\n */\nexport abstract class RatioCommand extends Command {\n private _getRenderFlags(): { verbose: boolean; json: boolean } {\n const argv = this.argv as string[];\n return {\n verbose: argv.includes('--verbose'),\n json: argv.includes('--json'),\n };\n }\n\n override async catch(\n err: Error & {\n exitCode?: number;\n oclif?: { exit?: number };\n skipOclifErrorHandling?: boolean;\n },\n ): Promise<never> {\n const { verbose, json } = this._getRenderFlags();\n\n const output = renderError({ err, verbose, json });\n\n // Write rendered output before any exit/throw\n if (output.stdout) process.stdout.write(output.stdout);\n if (output.stderr) process.stderr.write(output.stderr);\n\n if (output.exitCode === 0) {\n // ComingSoonError path — ExitError(0) causes oclif to call process.exit(0)\n // cleanly. ExitError has code 'EEXIT'; handle() treats it as a clean exit\n // (shouldPrint is false) so no additional output is written.\n throw new Errors.ExitError(0);\n }\n\n // For non-zero exits: suppress oclif's pretty-print by marking the error,\n // then route the correct exit code via oclif.exit so handle() calls process.exit(N).\n err.skipOclifErrorHandling = true;\n err.oclif = { exit: output.exitCode };\n throw err;\n }\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n","import { isRatioCLIError, RatioCLIError, ValidationError } from './errors.js';\n\nexport interface RenderInput {\n err: unknown;\n verbose: boolean;\n json: boolean;\n}\n\nexport interface RenderOutput {\n /** Text to write to stdout. Empty string means nothing to write. */\n stdout: string;\n /** Text to write to stderr. Empty string means nothing to write. */\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Detects whether an error originates from the oclif argument/flag parser.\n * oclif's CLIParseError subclasses (NonExistentFlagsError, RequiredArgsError,\n * etc.) all carry a `parse` property set by CLIParseErrorOptions. This is the\n * stable structural signal — more reliable than checking the class name string.\n */\nfunction isOclifParserError(err: unknown): err is Error & { parse: unknown } {\n return (\n err instanceof Error &&\n 'parse' in err &&\n err.parse !== null &&\n err.parse !== undefined\n );\n}\n\n/**\n * Builds the JSON error envelope. Fields with undefined values are omitted\n * so the consumer never sees `\"hint\": null` or `\"docsUrl\": null`.\n */\nfunction buildErrorEnvelope(\n code: string,\n message: string,\n hint?: string,\n docsUrl?: string,\n): string {\n const obj: Record<string, unknown> = { ok: false, code, message };\n if (hint !== undefined) obj['hint'] = hint;\n if (docsUrl !== undefined) obj['docsUrl'] = docsUrl;\n return JSON.stringify(obj) + '\\n';\n}\n\n/**\n * Pure rendering function — no side effects, no process.exit, no I/O.\n * The caller (RatioCommand.catch) is responsible for writing output and exiting.\n *\n * Rendering rules (all branches):\n *\n * | Error shape | --json | --verbose | stdout | stderr | exit |\n * |----------------------|--------|-----------|--------------------------------|----------------------------------|------|\n * | ComingSoonError | false | any | err.message + '\\n' | '' | 0 |\n * | ComingSoonError | true | any | '{\"ok\":true,\"data\":null}\\n' | '' | 0 |\n * | RatioCLIError >0 | false | false | '' | code: msg [+ hint] [+ docsUrl] | N |\n * | RatioCLIError >0 | false | true | '' | above + stack | N |\n * | RatioCLIError >0 | true | false | JSON envelope | '' | N |\n * | RatioCLIError >0 | true | true | JSON envelope | stack only | N |\n * | oclif ParserError | false | any | '' | VALIDATION_ERROR: msg [+stack] | 8 |\n * | oclif ParserError | true | any | JSON VALIDATION_ERROR envelope | stack if verbose | 8 |\n * | Plain Error / other | false | false | '' | INTERNAL: msg\\n | 1 |\n * | Plain Error / other | false | true | '' | INTERNAL: msg\\n + stack\\n | 1 |\n * | Plain Error / other | true | any | JSON INTERNAL envelope | stack if verbose | 1 |\n */\nexport function renderError(input: RenderInput): RenderOutput {\n const { err, verbose, json } = input;\n\n // ── Branch 1: our own typed hierarchy ───────────────────────────────────\n if (isRatioCLIError(err)) {\n const ratioErr = err as RatioCLIError;\n\n // exitCode === 0 → coming-soon: render on stdout, no stderr, exit 0\n if (ratioErr.exitCode === 0) {\n if (json) {\n return {\n stdout: JSON.stringify({ ok: true, data: null }) + '\\n',\n stderr: '',\n exitCode: 0,\n };\n }\n return {\n stdout: ratioErr.message + '\\n',\n stderr: '',\n exitCode: 0,\n };\n }\n\n // exitCode > 0 → error render\n let stderrLines = `${ratioErr.code}: ${ratioErr.message}\\n`;\n if (ratioErr.hint !== undefined) stderrLines += `${ratioErr.hint}\\n`;\n if (ratioErr.docsUrl !== undefined) stderrLines += `${ratioErr.docsUrl}\\n`;\n\n if (json) {\n return {\n stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),\n stderr: verbose && ratioErr.stack ? ratioErr.stack + '\\n' : '',\n exitCode: ratioErr.exitCode,\n };\n }\n\n if (verbose && ratioErr.stack) {\n stderrLines += ratioErr.stack + '\\n';\n }\n\n return { stdout: '', stderr: stderrLines, exitCode: ratioErr.exitCode };\n }\n\n // ── Branch 2: oclif parser error (unknown flag, missing required arg, etc.) ──\n if (isOclifParserError(err)) {\n const parserErr = err as Error & { parse: unknown };\n const msg = parserErr.message || 'Unknown parse error';\n\n // Strip oclif's \"Unexpected argument: --foo\" or similar prefix noise where\n // possible, but keep the core message as-is since it's already human-readable.\n const coercedErr = new ValidationError(msg);\n\n let stderrText = `${coercedErr.code}: ${msg}\\n`;\n\n if (json) {\n return {\n stdout: buildErrorEnvelope(coercedErr.code, msg),\n stderr: verbose && parserErr.stack ? parserErr.stack + '\\n' : '',\n exitCode: 8,\n };\n }\n\n if (verbose && parserErr.stack) {\n stderrText += parserErr.stack + '\\n';\n }\n\n return { stdout: '', stderr: stderrText, exitCode: 8 };\n }\n\n // ── Branch 3: plain Error or non-Error throw ─────────────────────────────\n const message = err instanceof Error ? err.message : String(err);\n const stack = err instanceof Error ? err.stack : undefined;\n const stderrPrefix = `INTERNAL: ${message}\\n`;\n\n if (json) {\n return {\n stdout: buildErrorEnvelope('INTERNAL', message),\n stderr: verbose && stack ? stack + '\\n' : '',\n exitCode: 1,\n };\n }\n\n let stderrText = stderrPrefix;\n if (verbose && stack) {\n stderrText += stack + '\\n';\n }\n\n return { stdout: '', stderr: stderrText, exitCode: 1 };\n}\n"],"mappings":";AAAA,SAAS,SAAS,cAAc;;;ACUzB,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAMT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO,WAAW;AACvB,SAAK,OAAO,KAAK;AACjB,SAAK,WAAW,KAAK;AACrB,QAAI,KAAK,SAAS,OAAW,MAAK,OAAO,KAAK;AAC9C,QAAI,KAAK,YAAY,OAAW,MAAK,UAAU,KAAK;AAAA,EACtD;AACF;AAgEO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EACjD,YAAY,SAAiB,MAAe;AAC1C,UAAM,OAA2E;AAAA,MAC/E,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AACA,QAAI,SAAS,OAAW,MAAK,OAAO;AACpC,UAAM,IAAI;AAAA,EACZ;AACF;AA6BO,SAAS,gBAAgB,KAAoC;AAClE,SAAO,eAAe;AACxB;;;ACjHA,SAAS,mBAAmB,KAAiD;AAC3E,SACE,eAAe,SACf,WAAW,OACX,IAAI,UAAU,QACd,IAAI,UAAU;AAElB;AAMA,SAAS,mBACP,MACA,SACA,MACA,SACQ;AACR,QAAM,MAA+B,EAAE,IAAI,OAAO,MAAM,QAAQ;AAChE,MAAI,SAAS,OAAW,KAAI,MAAM,IAAI;AACtC,MAAI,YAAY,OAAW,KAAI,SAAS,IAAI;AAC5C,SAAO,KAAK,UAAU,GAAG,IAAI;AAC/B;AAsBO,SAAS,YAAY,OAAkC;AAC5D,QAAM,EAAE,KAAK,SAAS,KAAK,IAAI;AAG/B,MAAI,gBAAgB,GAAG,GAAG;AACxB,UAAM,WAAW;AAGjB,QAAI,SAAS,aAAa,GAAG;AAC3B,UAAI,MAAM;AACR,eAAO;AAAA,UACL,QAAQ,KAAK,UAAU,EAAE,IAAI,MAAM,MAAM,KAAK,CAAC,IAAI;AAAA,UACnD,QAAQ;AAAA,UACR,UAAU;AAAA,QACZ;AAAA,MACF;AACA,aAAO;AAAA,QACL,QAAQ,SAAS,UAAU;AAAA,QAC3B,QAAQ;AAAA,QACR,UAAU;AAAA,MACZ;AAAA,IACF;AAGA,QAAI,cAAc,GAAG,SAAS,IAAI,KAAK,SAAS,OAAO;AAAA;AACvD,QAAI,SAAS,SAAS,OAAW,gBAAe,GAAG,SAAS,IAAI;AAAA;AAChE,QAAI,SAAS,YAAY,OAAW,gBAAe,GAAG,SAAS,OAAO;AAAA;AAEtE,QAAI,MAAM;AACR,aAAO;AAAA,QACL,QAAQ,mBAAmB,SAAS,MAAM,SAAS,SAAS,SAAS,MAAM,SAAS,OAAO;AAAA,QAC3F,QAAQ,WAAW,SAAS,QAAQ,SAAS,QAAQ,OAAO;AAAA,QAC5D,UAAU,SAAS;AAAA,MACrB;AAAA,IACF;AAEA,QAAI,WAAW,SAAS,OAAO;AAC7B,qBAAe,SAAS,QAAQ;AAAA,IAClC;AAEA,WAAO,EAAE,QAAQ,IAAI,QAAQ,aAAa,UAAU,SAAS,SAAS;AAAA,EACxE;AAGA,MAAI,mBAAmB,GAAG,GAAG;AAC3B,UAAM,YAAY;AAClB,UAAM,MAAM,UAAU,WAAW;AAIjC,UAAM,aAAa,IAAI,gBAAgB,GAAG;AAE1C,QAAIA,cAAa,GAAG,WAAW,IAAI,KAAK,GAAG;AAAA;AAE3C,QAAI,MAAM;AACR,aAAO;AAAA,QACL,QAAQ,mBAAmB,WAAW,MAAM,GAAG;AAAA,QAC/C,QAAQ,WAAW,UAAU,QAAQ,UAAU,QAAQ,OAAO;AAAA,QAC9D,UAAU;AAAA,MACZ;AAAA,IACF;AAEA,QAAI,WAAW,UAAU,OAAO;AAC9B,MAAAA,eAAc,UAAU,QAAQ;AAAA,IAClC;AAEA,WAAO,EAAE,QAAQ,IAAI,QAAQA,aAAY,UAAU,EAAE;AAAA,EACvD;AAGA,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,QAAM,QAAQ,eAAe,QAAQ,IAAI,QAAQ;AACjD,QAAM,eAAe,aAAa,OAAO;AAAA;AAEzC,MAAI,MAAM;AACR,WAAO;AAAA,MACL,QAAQ,mBAAmB,YAAY,OAAO;AAAA,MAC9C,QAAQ,WAAW,QAAQ,QAAQ,OAAO;AAAA,MAC1C,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,aAAa;AACjB,MAAI,WAAW,OAAO;AACpB,kBAAc,QAAQ;AAAA,EACxB;AAEA,SAAO,EAAE,QAAQ,IAAI,QAAQ,YAAY,UAAU,EAAE;AACvD;;;AFlHO,IAAe,eAAf,cAAoC,QAAQ;AAAA,EACzC,kBAAuD;AAC7D,UAAM,OAAO,KAAK;AAClB,WAAO;AAAA,MACL,SAAS,KAAK,SAAS,WAAW;AAAA,MAClC,MAAM,KAAK,SAAS,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAe,MACb,KAKgB;AAChB,UAAM,EAAE,SAAS,KAAK,IAAI,KAAK,gBAAgB;AAE/C,UAAM,SAAS,YAAY,EAAE,KAAK,SAAS,KAAK,CAAC;AAGjD,QAAI,OAAO,OAAQ,SAAQ,OAAO,MAAM,OAAO,MAAM;AACrD,QAAI,OAAO,OAAQ,SAAQ,OAAO,MAAM,OAAO,MAAM;AAErD,QAAI,OAAO,aAAa,GAAG;AAIzB,YAAM,IAAI,OAAO,UAAU,CAAC;AAAA,IAC9B;AAIA,QAAI,yBAAyB;AAC7B,QAAI,QAAQ,EAAE,MAAM,OAAO,SAAS;AACpC,UAAM;AAAA,EACR;AACF;","names":["stderrText"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LOCAL_BUFFER_MAX, RelayClient } from './relay-client.js';
|
|
2
|
+
export { RelayClientOptions, RelayEvent, RelayRandom, RelayScheduler, ResumeCursor } from './types.js';
|
|
3
|
+
import '../credentials/store.js';
|
|
4
|
+
import '../credentials/clock.js';
|
|
5
|
+
import '../credentials/refresher.js';
|
|
6
|
+
import '../credentials/types.js';
|
|
7
|
+
import '../auth/verbose-trace.js';
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// src/lib/relay/relay-client.ts
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
|
|
4
|
+
// src/lib/errors.ts
|
|
5
|
+
var RatioCLIError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
exitCode;
|
|
8
|
+
hint;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.exitCode = opts.exitCode;
|
|
15
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
16
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var RelayConnectError = class extends RatioCLIError {
|
|
20
|
+
constructor(message, hint = "Verify the tunnel URL and that the auth-server is reachable.") {
|
|
21
|
+
super({ code: "RELAY_CONNECT", exitCode: 6, message, hint });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var ValidationError = class extends RatioCLIError {
|
|
25
|
+
constructor(message, hint) {
|
|
26
|
+
const opts = {
|
|
27
|
+
code: "VALIDATION_ERROR",
|
|
28
|
+
exitCode: 8,
|
|
29
|
+
message
|
|
30
|
+
};
|
|
31
|
+
if (hint !== void 0) opts.hint = hint;
|
|
32
|
+
super(opts);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var RelayResumeGapError = class extends RatioCLIError {
|
|
36
|
+
constructor(message, hint = "Reconnect from the current cursor; older events are outside the buffer window.") {
|
|
37
|
+
super({ code: "RELAY_RESUME_GAP", exitCode: 9, message, hint });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/lib/relay/relay-client.ts
|
|
42
|
+
var LOCAL_BUFFER_MAX = 1e3;
|
|
43
|
+
var BACKOFF_BASE_MS = 100;
|
|
44
|
+
var BACKOFF_CAP_MS = 3e4;
|
|
45
|
+
var CLOSE_CODE_AUTH_REJECTED = 4401;
|
|
46
|
+
var CLOSE_CODE_NORMAL = 1e3;
|
|
47
|
+
var defaultScheduler = (fn, ms) => {
|
|
48
|
+
const handle = setTimeout(fn, ms);
|
|
49
|
+
return { cancel: () => clearTimeout(handle) };
|
|
50
|
+
};
|
|
51
|
+
var RelayClient = class {
|
|
52
|
+
wsUrl;
|
|
53
|
+
options;
|
|
54
|
+
scheduler;
|
|
55
|
+
random;
|
|
56
|
+
trace;
|
|
57
|
+
ws = null;
|
|
58
|
+
lastSeq = -1;
|
|
59
|
+
reconnectAttempt = 0;
|
|
60
|
+
reconnectTimer = null;
|
|
61
|
+
queue = [];
|
|
62
|
+
waiters = [];
|
|
63
|
+
/** First fatal error; once set, every consumer pull rejects with it. */
|
|
64
|
+
failure = null;
|
|
65
|
+
/** Clean end of stream (normal close or caller-initiated close()). */
|
|
66
|
+
ended = false;
|
|
67
|
+
userClosed = false;
|
|
68
|
+
started = false;
|
|
69
|
+
pendingConnect = null;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
if (options.wsUrl === "") {
|
|
72
|
+
throw new ValidationError("Relay stream URL must not be empty.");
|
|
73
|
+
}
|
|
74
|
+
if (options.sessionId === "") {
|
|
75
|
+
throw new ValidationError("Relay session id must not be empty.");
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(options.wsUrl);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new ValidationError(
|
|
82
|
+
"Relay stream URL is not a valid URL.",
|
|
83
|
+
"The server returned a malformed stream endpoint \u2014 retry creating the session."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
for (const [name, value] of parsed.searchParams.entries()) {
|
|
87
|
+
if (name === "sessionId") {
|
|
88
|
+
if (value !== options.sessionId) {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
"Relay stream URL carries a session id that does not match the created session.",
|
|
91
|
+
"The server and client disagree on the session \u2014 retry creating the session."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new ValidationError(
|
|
97
|
+
"Relay stream URL must not carry query parameters other than the session id.",
|
|
98
|
+
"The client builds the connection query string itself."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
this.options = options;
|
|
102
|
+
this.wsUrl = `${parsed.origin}${parsed.pathname}`;
|
|
103
|
+
this.scheduler = options.scheduler ?? defaultScheduler;
|
|
104
|
+
this.random = options.random ?? Math.random;
|
|
105
|
+
this.trace = options.tracer ?? (() => {
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Opens the first connection. Resolves when the socket is open;
|
|
110
|
+
* rejects with a typed error on authentication rejection or when the
|
|
111
|
+
* stream fails before ever becoming established. May only be called
|
|
112
|
+
* once per client instance.
|
|
113
|
+
*/
|
|
114
|
+
async connect() {
|
|
115
|
+
if (this.started) {
|
|
116
|
+
throw new ValidationError("connect() may only be called once.");
|
|
117
|
+
}
|
|
118
|
+
this.started = true;
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
this.pendingConnect = { resolve, reject };
|
|
121
|
+
void this.openSocket();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pull-based event stream. Single-consumer: the iterator shares the
|
|
126
|
+
* client's internal queue. Ends when close() is called or the server
|
|
127
|
+
* closes cleanly; rejects with a typed error on fatal conditions.
|
|
128
|
+
*/
|
|
129
|
+
events() {
|
|
130
|
+
return {
|
|
131
|
+
[Symbol.asyncIterator]: () => ({
|
|
132
|
+
next: () => this.nextEvent(),
|
|
133
|
+
return: async () => {
|
|
134
|
+
await this.close();
|
|
135
|
+
return { done: true, value: void 0 };
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Graceful shutdown: cancels any pending reconnect, closes the
|
|
142
|
+
* socket with a normal-closure code, and completes the iterator.
|
|
143
|
+
*/
|
|
144
|
+
async close() {
|
|
145
|
+
this.userClosed = true;
|
|
146
|
+
if (this.reconnectTimer !== null) {
|
|
147
|
+
this.reconnectTimer.cancel();
|
|
148
|
+
this.reconnectTimer = null;
|
|
149
|
+
}
|
|
150
|
+
const ws = this.ws;
|
|
151
|
+
if (ws === null || ws.readyState === WebSocket.CLOSED) {
|
|
152
|
+
this.endClean();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
ws.once("close", () => resolve());
|
|
157
|
+
ws.close(CLOSE_CODE_NORMAL);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ── internal state machine ────────────────────────────────────────
|
|
161
|
+
async openSocket() {
|
|
162
|
+
let token;
|
|
163
|
+
try {
|
|
164
|
+
token = await this.options.credentialStore.getValidAccessToken(
|
|
165
|
+
this.options.profileName
|
|
166
|
+
);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
this.fail(err instanceof Error ? err : new RelayConnectError(
|
|
169
|
+
"Could not obtain an access token for the relay connection."
|
|
170
|
+
));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const query = new URLSearchParams({ sessionId: this.options.sessionId });
|
|
174
|
+
if (this.lastSeq !== -1) {
|
|
175
|
+
query.set("resumeCursor", String(this.lastSeq));
|
|
176
|
+
}
|
|
177
|
+
const url = `${this.wsUrl}?${query.toString()}`;
|
|
178
|
+
this.trace(
|
|
179
|
+
`relay connect: session=${this.options.sessionId} cursor=${this.lastSeq}`
|
|
180
|
+
);
|
|
181
|
+
const ws = new WebSocket(url, {
|
|
182
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
183
|
+
});
|
|
184
|
+
this.ws = ws;
|
|
185
|
+
ws.on("open", () => {
|
|
186
|
+
this.reconnectAttempt = 0;
|
|
187
|
+
this.trace("relay connected");
|
|
188
|
+
if (this.pendingConnect !== null) {
|
|
189
|
+
this.pendingConnect.resolve();
|
|
190
|
+
this.pendingConnect = null;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
ws.on("message", (data) => {
|
|
194
|
+
this.handleMessage(String(data));
|
|
195
|
+
});
|
|
196
|
+
ws.on("error", (err) => {
|
|
197
|
+
this.trace(`relay socket error: ${err.message}`);
|
|
198
|
+
});
|
|
199
|
+
ws.on("close", (code) => {
|
|
200
|
+
this.handleClose(code);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
handleMessage(raw) {
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(raw);
|
|
207
|
+
} catch {
|
|
208
|
+
this.trace("relay frame dropped: not valid JSON");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
212
|
+
this.trace("relay frame dropped: not an object");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const frame = parsed;
|
|
216
|
+
if (frame["kind"] === "control") {
|
|
217
|
+
if (frame["type"] === "resume-gap") {
|
|
218
|
+
this.trace("relay resume gap reported by server");
|
|
219
|
+
this.fail(
|
|
220
|
+
new RelayResumeGapError(
|
|
221
|
+
"The server could not replay all missed events: the resume cursor is older than the replay window."
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.trace("relay control frame ignored: unknown type");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (frame["kind"] === "webhook" && typeof frame["seq"] === "number") {
|
|
230
|
+
const seq = frame["seq"];
|
|
231
|
+
if (seq <= this.lastSeq) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.lastSeq = seq;
|
|
235
|
+
this.deliver({
|
|
236
|
+
kind: "webhook",
|
|
237
|
+
seq,
|
|
238
|
+
timestamp: typeof frame["timestamp"] === "number" ? new Date(frame["timestamp"]).toISOString() : "",
|
|
239
|
+
payload: frame["event"]
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.trace("relay frame dropped: unrecognized envelope");
|
|
244
|
+
}
|
|
245
|
+
deliver(event) {
|
|
246
|
+
const waiter = this.waiters.shift();
|
|
247
|
+
if (waiter !== void 0) {
|
|
248
|
+
waiter.resolve({ done: false, value: event });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (this.queue.length >= LOCAL_BUFFER_MAX) {
|
|
252
|
+
this.trace("relay local buffer overflow \u2014 closing connection");
|
|
253
|
+
this.fail(
|
|
254
|
+
new RelayConnectError(
|
|
255
|
+
"Local buffer overflow \u2014 consumer stalled",
|
|
256
|
+
"Slow down the consumer or reduce upstream event rate."
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.queue.push(event);
|
|
262
|
+
}
|
|
263
|
+
handleClose(code) {
|
|
264
|
+
this.ws = null;
|
|
265
|
+
if (this.failure !== null) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (this.userClosed || code === CLOSE_CODE_NORMAL) {
|
|
269
|
+
this.trace(`relay closed cleanly (code ${code})`);
|
|
270
|
+
this.endClean();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (code === CLOSE_CODE_AUTH_REJECTED) {
|
|
274
|
+
this.trace("relay rejected the connection: authentication failed");
|
|
275
|
+
this.fail(
|
|
276
|
+
new RelayConnectError(
|
|
277
|
+
"The relay rejected the connection: authentication failed (close code 4401).",
|
|
278
|
+
"Run 'ratio auth login' and retry."
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.scheduleReconnect(code);
|
|
284
|
+
}
|
|
285
|
+
scheduleReconnect(closeCode) {
|
|
286
|
+
this.reconnectAttempt += 1;
|
|
287
|
+
const base = Math.min(
|
|
288
|
+
2 ** this.reconnectAttempt * BACKOFF_BASE_MS,
|
|
289
|
+
BACKOFF_CAP_MS
|
|
290
|
+
);
|
|
291
|
+
const delay = base * (0.75 + this.random() * 0.5);
|
|
292
|
+
this.trace(
|
|
293
|
+
`relay reconnect attempt ${this.reconnectAttempt} in ${Math.round(delay)}ms (close code ${closeCode})`
|
|
294
|
+
);
|
|
295
|
+
this.reconnectTimer = this.scheduler(() => {
|
|
296
|
+
this.reconnectTimer = null;
|
|
297
|
+
void this.openSocket();
|
|
298
|
+
}, delay);
|
|
299
|
+
}
|
|
300
|
+
fail(err) {
|
|
301
|
+
if (this.failure !== null) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.failure = err;
|
|
305
|
+
if (this.reconnectTimer !== null) {
|
|
306
|
+
this.reconnectTimer.cancel();
|
|
307
|
+
this.reconnectTimer = null;
|
|
308
|
+
}
|
|
309
|
+
if (this.pendingConnect !== null) {
|
|
310
|
+
this.pendingConnect.reject(err);
|
|
311
|
+
this.pendingConnect = null;
|
|
312
|
+
}
|
|
313
|
+
let waiter = this.waiters.shift();
|
|
314
|
+
while (waiter !== void 0) {
|
|
315
|
+
waiter.reject(err);
|
|
316
|
+
waiter = this.waiters.shift();
|
|
317
|
+
}
|
|
318
|
+
const ws = this.ws;
|
|
319
|
+
if (ws !== null && ws.readyState !== WebSocket.CLOSED) {
|
|
320
|
+
ws.close(CLOSE_CODE_NORMAL);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
endClean() {
|
|
324
|
+
if (this.ended) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.ended = true;
|
|
328
|
+
if (this.pendingConnect !== null) {
|
|
329
|
+
this.pendingConnect.reject(
|
|
330
|
+
new RelayConnectError(
|
|
331
|
+
"The relay connection closed before it was established."
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
this.pendingConnect = null;
|
|
335
|
+
}
|
|
336
|
+
let waiter = this.waiters.shift();
|
|
337
|
+
while (waiter !== void 0) {
|
|
338
|
+
waiter.resolve({ done: true, value: void 0 });
|
|
339
|
+
waiter = this.waiters.shift();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
nextEvent() {
|
|
343
|
+
if (this.failure !== null) {
|
|
344
|
+
return Promise.reject(this.failure);
|
|
345
|
+
}
|
|
346
|
+
const queued = this.queue.shift();
|
|
347
|
+
if (queued !== void 0) {
|
|
348
|
+
return Promise.resolve({ done: false, value: queued });
|
|
349
|
+
}
|
|
350
|
+
if (this.ended) {
|
|
351
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
352
|
+
}
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
this.waiters.push({ resolve, reject });
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
export {
|
|
359
|
+
LOCAL_BUFFER_MAX,
|
|
360
|
+
RelayClient
|
|
361
|
+
};
|
|
362
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/relay/relay-client.ts","../../../src/lib/errors.ts"],"sourcesContent":["/**\n * WebSocket relay tunnel client.\n *\n * Responsibilities:\n * - Connect to the relay stream endpoint with `Authorization: Bearer`\n * at the HTTP handshake (the token never appears in the URL). The\n * server authenticates the upgrade by reading the `sessionId` query\n * parameter, so every connect URL carries `?sessionId=<id>`.\n * - Accept the server-issued stream URL, which may already carry the\n * canonical `?sessionId=` parameter; it must match the session the\n * client was constructed with, and no other query parameter is\n * allowed — the client owns the connect query string.\n * - Track the monotonic per-session sequence number; discard\n * duplicates delivered during a resume replay.\n * - Auto-reconnect on abnormal closes with exponential backoff\n * (capped) and multiplicative jitter, resuming from the last seen\n * sequence via the `resumeCursor` query parameter (sent alongside\n * `sessionId`).\n * - Surface a server-declared resume gap as a typed\n * RelayResumeGapError — recovery policy belongs to the caller.\n * - Deliver events through a pull-based AsyncIterable with a bounded\n * local buffer; a stalled consumer fails fast instead of growing\n * memory without limit.\n *\n * Uses the `ws` package (not the runtime's built-in WebSocket, which\n * is only available as a stable global from Node 22 — this package\n * supports Node 18+).\n */\n\nimport WebSocket from 'ws';\nimport {\n RelayConnectError,\n RelayResumeGapError,\n ValidationError,\n} from '../errors.js';\nimport type {\n RelayClientOptions,\n RelayEvent,\n RelayRandom,\n RelayScheduler,\n ResumeCursor,\n} from './types.js';\n\n/**\n * Maximum number of undelivered events held for a slow consumer.\n * Exceeding it rejects the consumer's next pull and closes the\n * connection — the client never buffers without bound.\n */\nexport const LOCAL_BUFFER_MAX = 1000;\n\n/** Base backoff unit; attempt n waits min(2^n * 100, 30_000) ms. */\nconst BACKOFF_BASE_MS = 100;\n/** Backoff ceiling: 30_000 ms (30 seconds). */\nconst BACKOFF_CAP_MS = 30_000;\n\n/** Close code the server uses to reject an unauthenticated socket. */\nconst CLOSE_CODE_AUTH_REJECTED = 4401;\n/** Normal-closure code — clean shutdown, never reconnected. */\nconst CLOSE_CODE_NORMAL = 1000;\n\nconst defaultScheduler: RelayScheduler = (fn, ms) => {\n const handle = setTimeout(fn, ms);\n return { cancel: () => clearTimeout(handle) };\n};\n\ninterface Waiter {\n resolve: (result: IteratorResult<RelayEvent>) => void;\n reject: (err: Error) => void;\n}\n\nexport class RelayClient {\n private readonly wsUrl: string;\n private readonly options: RelayClientOptions;\n private readonly scheduler: RelayScheduler;\n private readonly random: RelayRandom;\n private readonly trace: (msg: string) => void;\n\n private ws: WebSocket | null = null;\n private lastSeq: ResumeCursor = -1;\n private reconnectAttempt = 0;\n private reconnectTimer: { cancel: () => void } | null = null;\n\n private readonly queue: RelayEvent[] = [];\n private readonly waiters: Waiter[] = [];\n /** First fatal error; once set, every consumer pull rejects with it. */\n private failure: Error | null = null;\n /** Clean end of stream (normal close or caller-initiated close()). */\n private ended = false;\n private userClosed = false;\n private started = false;\n\n private pendingConnect: {\n resolve: () => void;\n reject: (err: Error) => void;\n } | null = null;\n\n constructor(options: RelayClientOptions) {\n if (options.wsUrl === '') {\n throw new ValidationError('Relay stream URL must not be empty.');\n }\n if (options.sessionId === '') {\n throw new ValidationError('Relay session id must not be empty.');\n }\n let parsed: URL;\n try {\n parsed = new URL(options.wsUrl);\n } catch {\n throw new ValidationError(\n 'Relay stream URL is not a valid URL.',\n 'The server returned a malformed stream endpoint — retry creating the session.',\n );\n }\n for (const [name, value] of parsed.searchParams.entries()) {\n if (name === 'sessionId') {\n if (value !== options.sessionId) {\n throw new ValidationError(\n 'Relay stream URL carries a session id that does not match the created session.',\n 'The server and client disagree on the session — retry creating the session.',\n );\n }\n continue;\n }\n throw new ValidationError(\n 'Relay stream URL must not carry query parameters other than the session id.',\n 'The client builds the connection query string itself.',\n );\n }\n this.options = options;\n // Canonical base: origin + path only. The connect query string\n // (sessionId, and resumeCursor on resume) is built per attempt.\n this.wsUrl = `${parsed.origin}${parsed.pathname}`;\n this.scheduler = options.scheduler ?? defaultScheduler;\n this.random = options.random ?? Math.random;\n this.trace = options.tracer ?? (() => {});\n }\n\n /**\n * Opens the first connection. Resolves when the socket is open;\n * rejects with a typed error on authentication rejection or when the\n * stream fails before ever becoming established. May only be called\n * once per client instance.\n */\n async connect(): Promise<void> {\n if (this.started) {\n throw new ValidationError('connect() may only be called once.');\n }\n this.started = true;\n await new Promise<void>((resolve, reject) => {\n this.pendingConnect = { resolve, reject };\n void this.openSocket();\n });\n }\n\n /**\n * Pull-based event stream. Single-consumer: the iterator shares the\n * client's internal queue. Ends when close() is called or the server\n * closes cleanly; rejects with a typed error on fatal conditions.\n */\n events(): AsyncIterable<RelayEvent> {\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => this.nextEvent(),\n return: async (): Promise<IteratorResult<RelayEvent>> => {\n await this.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n /**\n * Graceful shutdown: cancels any pending reconnect, closes the\n * socket with a normal-closure code, and completes the iterator.\n */\n async close(): Promise<void> {\n this.userClosed = true;\n if (this.reconnectTimer !== null) {\n this.reconnectTimer.cancel();\n this.reconnectTimer = null;\n }\n const ws = this.ws;\n if (ws === null || ws.readyState === WebSocket.CLOSED) {\n this.endClean();\n return;\n }\n await new Promise<void>((resolve) => {\n ws.once('close', () => resolve());\n ws.close(CLOSE_CODE_NORMAL);\n });\n }\n\n // ── internal state machine ────────────────────────────────────────\n\n private async openSocket(): Promise<void> {\n let token: string;\n try {\n token = await this.options.credentialStore.getValidAccessToken(\n this.options.profileName,\n );\n } catch (err) {\n // Credential-store failures are already typed; surface as fatal.\n this.fail(err instanceof Error ? err : new RelayConnectError(\n 'Could not obtain an access token for the relay connection.',\n ));\n return;\n }\n\n // The server authenticates the upgrade via the sessionId query\n // parameter — every connect carries it; a resume adds the cursor.\n const query = new URLSearchParams({ sessionId: this.options.sessionId });\n if (this.lastSeq !== -1) {\n query.set('resumeCursor', String(this.lastSeq));\n }\n const url = `${this.wsUrl}?${query.toString()}`;\n\n this.trace(\n `relay connect: session=${this.options.sessionId} cursor=${this.lastSeq}`,\n );\n\n const ws = new WebSocket(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n this.ws = ws;\n\n ws.on('open', () => {\n this.reconnectAttempt = 0;\n this.trace('relay connected');\n if (this.pendingConnect !== null) {\n this.pendingConnect.resolve();\n this.pendingConnect = null;\n }\n });\n ws.on('message', (data) => {\n this.handleMessage(String(data));\n });\n ws.on('error', (err) => {\n // The matching 'close' event drives the state machine; this\n // listener only prevents an unhandled-error crash.\n this.trace(`relay socket error: ${err.message}`);\n });\n ws.on('close', (code) => {\n this.handleClose(code);\n });\n }\n\n private handleMessage(raw: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.trace('relay frame dropped: not valid JSON');\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n this.trace('relay frame dropped: not an object');\n return;\n }\n const frame = parsed as Record<string, unknown>;\n\n if (frame['kind'] === 'control') {\n if (frame['type'] === 'resume-gap') {\n this.trace('relay resume gap reported by server');\n this.fail(\n new RelayResumeGapError(\n 'The server could not replay all missed events: the resume cursor is older than the replay window.',\n ),\n );\n return;\n }\n this.trace('relay control frame ignored: unknown type');\n return;\n }\n\n if (frame['kind'] === 'webhook' && typeof frame['seq'] === 'number') {\n const seq = frame['seq'];\n if (seq <= this.lastSeq) {\n // Duplicate delivered during a resume replay — discard.\n return;\n }\n this.lastSeq = seq;\n this.deliver({\n kind: 'webhook',\n seq,\n timestamp:\n typeof frame['timestamp'] === 'number'\n ? new Date(frame['timestamp']).toISOString()\n : '',\n payload: frame['event'],\n });\n return;\n }\n\n this.trace('relay frame dropped: unrecognized envelope');\n }\n\n private deliver(event: RelayEvent): void {\n const waiter = this.waiters.shift();\n if (waiter !== undefined) {\n waiter.resolve({ done: false, value: event });\n return;\n }\n if (this.queue.length >= LOCAL_BUFFER_MAX) {\n this.trace('relay local buffer overflow — closing connection');\n this.fail(\n new RelayConnectError(\n 'Local buffer overflow — consumer stalled',\n 'Slow down the consumer or reduce upstream event rate.',\n ),\n );\n return;\n }\n this.queue.push(event);\n }\n\n private handleClose(code: number): void {\n this.ws = null;\n\n if (this.failure !== null) {\n // Already failed (resume gap, overflow, auth) — nothing to do.\n return;\n }\n\n if (this.userClosed || code === CLOSE_CODE_NORMAL) {\n this.trace(`relay closed cleanly (code ${code})`);\n this.endClean();\n return;\n }\n\n if (code === CLOSE_CODE_AUTH_REJECTED) {\n this.trace('relay rejected the connection: authentication failed');\n this.fail(\n new RelayConnectError(\n 'The relay rejected the connection: authentication failed (close code 4401).',\n \"Run 'ratio auth login' and retry.\",\n ),\n );\n return;\n }\n\n this.scheduleReconnect(code);\n }\n\n private scheduleReconnect(closeCode: number): void {\n this.reconnectAttempt += 1;\n const base = Math.min(\n 2 ** this.reconnectAttempt * BACKOFF_BASE_MS,\n BACKOFF_CAP_MS,\n );\n // Multiplicative jitter in [0.75, 1.25] avoids reconnect stampedes.\n const delay = base * (0.75 + this.random() * 0.5);\n this.trace(\n `relay reconnect attempt ${this.reconnectAttempt} in ${Math.round(delay)}ms (close code ${closeCode})`,\n );\n this.reconnectTimer = this.scheduler(() => {\n this.reconnectTimer = null;\n void this.openSocket();\n }, delay);\n }\n\n private fail(err: Error): void {\n if (this.failure !== null) {\n return;\n }\n this.failure = err;\n // A failed stream must never reconnect — drop any pending timer.\n if (this.reconnectTimer !== null) {\n this.reconnectTimer.cancel();\n this.reconnectTimer = null;\n }\n if (this.pendingConnect !== null) {\n this.pendingConnect.reject(err);\n this.pendingConnect = null;\n }\n let waiter = this.waiters.shift();\n while (waiter !== undefined) {\n waiter.reject(err);\n waiter = this.waiters.shift();\n }\n const ws = this.ws;\n if (ws !== null && ws.readyState !== WebSocket.CLOSED) {\n ws.close(CLOSE_CODE_NORMAL);\n }\n }\n\n private endClean(): void {\n if (this.ended) {\n return;\n }\n this.ended = true;\n if (this.pendingConnect !== null) {\n // Closed before ever opening — the stream never established.\n this.pendingConnect.reject(\n new RelayConnectError(\n 'The relay connection closed before it was established.',\n ),\n );\n this.pendingConnect = null;\n }\n let waiter = this.waiters.shift();\n while (waiter !== undefined) {\n waiter.resolve({ done: true, value: undefined });\n waiter = this.waiters.shift();\n }\n }\n\n private nextEvent(): Promise<IteratorResult<RelayEvent>> {\n if (this.failure !== null) {\n return Promise.reject(this.failure);\n }\n const queued = this.queue.shift();\n if (queued !== undefined) {\n return Promise.resolve({ done: false, value: queued });\n }\n if (this.ended) {\n return Promise.resolve({ done: true, value: undefined });\n }\n return new Promise<IteratorResult<RelayEvent>>((resolve, reject) => {\n this.waiters.push({ resolve, reject });\n });\n }\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n"],"mappings":"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","names":[]}
|