@irtio/cli 0.1.0

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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ var [, , command, ...args] = process.argv;
5
+ var HELP = `irtio \u2014 multiplayer rooms for web games
6
+
7
+ usage: irtio <command>
8
+
9
+ commands:
10
+ init [dir] [--tick|--event] scaffold irtio/schema.ts, rpc.ts, room.ts,
11
+ room.test.ts and irtio.json (--tick/--event
12
+ skip the one question it otherwise asks)
13
+ dev [--room <file>] [--port <n>] [--no-watch] bundle the room file and run it locally
14
+ simulate [--bots <n>] [--seconds <n>] drive N real clients at a running room and
15
+ [--room <code>] [--url <ws://\u2026>] check the built-in invariants
16
+ [--cheat] [--key <projectKey>] --cheat sends illegal writes and expects
17
+ [--trace <path>] corrections; --trace dumps every frame
18
+
19
+ login [--url <control>] sign in to a control plane via the browser
20
+ whoami [--url <control>] print the identity the stored credential
21
+ resolves to, and the control URL asked
22
+ deploy [--allow-breaking] [--project <id>] bundle, classify the schema change against the
23
+ [--url <control>] [--room <file>] last deployment, and upload \u2014 refuses breaking
24
+ changes unless a migration covers them
25
+ migrate create <name> scaffold irtio/migrations/<n>_<name>.ts with the
26
+ live breaking changes quoted in its header
27
+ logs [--follow] [--since <cursor>] tail a project's logs
28
+ [--project <id>] [--url <control>]
29
+ rooms [--project <id>] [--url <control>] list a project's rooms (rows not reported for
30
+ longer than the control plane's retention
31
+ window, 30 days by default, are pruned)
32
+ `;
33
+ switch (command) {
34
+ case "init": {
35
+ const { init } = await import("./init.js");
36
+ await init(args);
37
+ break;
38
+ }
39
+ case "dev": {
40
+ const { dev } = await import("./dev-BFWFWJ4J.js");
41
+ await dev(args);
42
+ break;
43
+ }
44
+ case "simulate": {
45
+ const { simulate } = await import("./simulate.js");
46
+ await simulate(args);
47
+ break;
48
+ }
49
+ case "login": {
50
+ const { login } = await import("./login-US2YT5ZB.js");
51
+ await login(args);
52
+ break;
53
+ }
54
+ case "whoami": {
55
+ const { whoami } = await import("./whoami-5Q32SXFX.js");
56
+ await whoami(args);
57
+ break;
58
+ }
59
+ case "deploy": {
60
+ const { deploy } = await import("./deploy-4W6AYYEW.js");
61
+ await deploy(args);
62
+ break;
63
+ }
64
+ case "migrate": {
65
+ const { migrate } = await import("./migrate-FQIDF747.js");
66
+ await migrate(args);
67
+ break;
68
+ }
69
+ case "logs": {
70
+ const { logs } = await import("./logs-6GLBO3TP.js");
71
+ await logs(args);
72
+ break;
73
+ }
74
+ case "rooms": {
75
+ const { rooms } = await import("./rooms-JXT3PRHN.js");
76
+ await rooms(args);
77
+ break;
78
+ }
79
+ case void 0:
80
+ case "help":
81
+ case "--help":
82
+ case "-h":
83
+ console.log(HELP);
84
+ break;
85
+ default:
86
+ console.error(`irtio: unknown command ${JSON.stringify(command)}
87
+ `);
88
+ console.log(HELP);
89
+ process.exitCode = 1;
90
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `irtio init`: the four files a room needs, plus a passing smoke test.
3
+ *
4
+ * It asks exactly one question, because the only thing `init` cannot infer is whether the world
5
+ * moves on its own: tick mode runs a fixed loop, event mode only reacts. Everything else — the
6
+ * `players` entity, the `onJoin`/`onLeave` spawn block, the project id — is a convention the
7
+ * templates carry so an agent copies the pattern instead of inventing one.
8
+ *
9
+ * Two rules shape the code. Nothing is ever overwritten (a second `init` reports what it skipped
10
+ * and leaves your room alone), and the printed output is the whole onboarding document: it has to
11
+ * take a human from an empty directory to two tabs sharing a `?room=` link with nothing else open.
12
+ */
13
+ /** The two ways a room can change state. `init` picks one and writes it into `room.ts`. */
14
+ type RoomMode = 'tick' | 'event';
15
+ /** The packages a scaffolded project needs; added to `package.json` when there is one. */
16
+ declare const INIT_DEPENDENCIES: readonly ["@irtio/schema", "@irtio/server", "@irtio/client", "@irtio/testing"];
17
+ interface InitArgs {
18
+ /** Target directory, relative to the cwd. Defaults to `.`. */
19
+ readonly dir?: string;
20
+ /** Set by `--tick` / `--event`; absent means "ask". */
21
+ readonly mode?: RoomMode;
22
+ }
23
+ interface RunInitOptions {
24
+ /** Where the project lives. Defaults to `process.cwd()`. */
25
+ readonly cwd?: string;
26
+ readonly dir?: string;
27
+ readonly mode?: RoomMode;
28
+ /**
29
+ * Asks the one question. Injected so tests never touch a TTY; the default reads stdin when it
30
+ * is a TTY and otherwise takes the default answer rather than hanging in a pipeline.
31
+ */
32
+ readonly ask?: (question: string) => Promise<string>;
33
+ readonly log?: (line: string) => void;
34
+ /** @internal Pins the generated project id so tests can assert it end to end. */
35
+ readonly projectId?: string;
36
+ }
37
+ interface InitResult {
38
+ /** Absolute path of the scaffolded project. */
39
+ readonly dir: string;
40
+ readonly project: string;
41
+ readonly mode: RoomMode;
42
+ /** Files written, relative to `dir`, in emit order. */
43
+ readonly created: readonly string[];
44
+ /** Files that already existed and were left untouched. */
45
+ readonly skipped: readonly string[];
46
+ /** Whether a `package.json` was found, and whether it needed changing. */
47
+ readonly packageJson: 'updated' | 'unchanged' | 'absent';
48
+ }
49
+ /**
50
+ * `irtio init [dir] [--tick|--event]`. Hand-rolled like `parseDevArgs`: the CLI has no
51
+ * argument-parsing dependency and four flags do not earn one.
52
+ */
53
+ declare function parseInitArgs(args: readonly string[]): InitArgs;
54
+ /** `p_` + 16 hex characters: the project id *is* the public key, so it is not a secret. */
55
+ declare function generateProjectId(): string;
56
+ /**
57
+ * Scaffolds a project. Pure enough to test: every effect is a file write under `dir`, every
58
+ * question goes through `ask`, and every line of output goes through `log`.
59
+ */
60
+ declare function runInit(options?: RunInitOptions): Promise<InitResult>;
61
+ /** Thin on purpose: parse, run, and turn a thrown message into an exit code. */
62
+ declare function init(args: readonly string[]): Promise<void>;
63
+
64
+ export { INIT_DEPENDENCIES, type InitArgs, type InitResult, type RoomMode, type RunInitOptions, generateProjectId, init, parseInitArgs, runInit };
package/dist/init.js ADDED
@@ -0,0 +1,307 @@
1
+ // src/init.ts
2
+ import { randomBytes } from "crypto";
3
+ import { existsSync } from "fs";
4
+ import { mkdir, readFile, writeFile } from "fs/promises";
5
+ import * as path from "path";
6
+ import { createInterface } from "readline/promises";
7
+ import pc from "picocolors";
8
+ var INIT_DEPENDENCIES = [
9
+ "@irtio/schema",
10
+ "@irtio/server",
11
+ "@irtio/client",
12
+ "@irtio/testing"
13
+ ];
14
+ var DEFAULT_TICK_RATE = 20;
15
+ var DEFAULT_IDLE_MS = 3e4;
16
+ function parseInitArgs(args) {
17
+ const parsed = {};
18
+ for (const arg of args) {
19
+ switch (arg) {
20
+ case "--tick":
21
+ case "--event": {
22
+ const mode = arg === "--tick" ? "tick" : "event";
23
+ if (parsed.mode !== void 0 && parsed.mode !== mode) {
24
+ throw new Error("irtio init: --tick and --event are mutually exclusive");
25
+ }
26
+ parsed.mode = mode;
27
+ break;
28
+ }
29
+ default: {
30
+ if (arg.startsWith("-"))
31
+ throw new Error(`irtio init: unknown option ${JSON.stringify(arg)}`);
32
+ if (parsed.dir !== void 0) {
33
+ throw new Error(`irtio init: unexpected extra argument ${JSON.stringify(arg)}`);
34
+ }
35
+ parsed.dir = arg;
36
+ }
37
+ }
38
+ }
39
+ return parsed;
40
+ }
41
+ function generateProjectId() {
42
+ return `p_${randomBytes(8).toString("hex")}`;
43
+ }
44
+ function schemaTemplate(project) {
45
+ return `/**
46
+ * Shared facts about the data. Both sides import this file \u2014 the room to run on, your game
47
+ * to draw from \u2014 so it also carries the project id, which is the public key: domain-locked, never
48
+ * secret, safe in client code.
49
+ */
50
+
51
+ import { defineSchema, entity, f32, str, u8 } from '@irtio/schema';
52
+
53
+ import { rpc } from './rpc.js';
54
+
55
+ export const schema = defineSchema(
56
+ {
57
+ // One instance per connected client, owned by that client (see onJoin in room.ts). Owned
58
+ // means writable: \`room.state.players[room.me].x = 10\` on the client is a local write that
59
+ // syncs, and the server validates it.
60
+ players: entity({ x: f32, y: f32, name: str(24), color: u8 }),
61
+ },
62
+ {
63
+ project: '${project}',
64
+ roles: ['player', 'spectator'] as const,
65
+ rpc,
66
+ },
67
+ );
68
+ `;
69
+ }
70
+ var RPC_TEMPLATE = `/**
71
+ * Typed calls in both directions, declared once and implemented exhaustively. Both sides
72
+ * import this file and the names are part of the schema hash, so a signature change is caught as
73
+ * version skew instead of at runtime.
74
+ *
75
+ * Nothing here yet \u2014 the retrofit path needs owned writes, not RPCs. Uncomment as you need them:
76
+ *
77
+ * import { server, client, u8, str, f32, list } from '@irtio/schema';
78
+ *
79
+ * export const rpc = {
80
+ * // client \u2192 server, void (what other engines call an "input"):
81
+ * answer: server({ params: { choice: u8 } }),
82
+ * // client \u2192 server with a typed return; await it as room.call.dealCards({ count: 5 }):
83
+ * dealCards: server({ params: { count: u8 }, returns: { cards: list(str(8), 52) } }),
84
+ * // server \u2192 client, void; the room calls room.broadcast.shake({ intensity: 1 }):
85
+ * shake: client({ params: { intensity: f32 } }),
86
+ * };
87
+ */
88
+
89
+ export const rpc = {};
90
+ `;
91
+ function roomTemplate(mode) {
92
+ const config = mode === "tick" ? ` // Tick mode: a fixed-rate loop runs even when nobody is doing anything.
93
+ mode: 'tick',
94
+ tickRate: ${DEFAULT_TICK_RATE},` : ` // Event mode: nothing runs between frames, so the room hibernates when it goes quiet.
95
+ mode: 'event',
96
+ idleMs: ${DEFAULT_IDLE_MS / 1e3}_000,`;
97
+ const tick = mode === "tick" ? `
98
+ // Fixed-rate; \`dt\` is seconds since the last tick. Move whatever the players do not move
99
+ // themselves \u2014 everything you write here is tracked and goes out in this tick's delta.
100
+ tick(_state, _dt, _room) {},
101
+ ` : "";
102
+ return `/**
103
+ * Behavior. Server-only: \`irtio dev\` and \`irtio deploy\` bundle this file, and it never
104
+ * reaches the browser. Everything is synchronous \u2014 a handler returns, the runtime encodes.
105
+ */
106
+
107
+ import { defineRoom } from '@irtio/server';
108
+
109
+ import { schema } from './schema.js';
110
+
111
+ export default defineRoom(schema, {
112
+ ${config}
113
+
114
+ // Lifecycle is code, not config. The per-client entity is created here and owned by the
115
+ // client that caused the join, which is exactly what makes \`room.state.players[room.me]\`
116
+ // writable on that client and read-only on everyone else's.
117
+ onJoin(state, ctx) {
118
+ if (ctx.reconnecting) return;
119
+ state.players.add(
120
+ ctx.clientId,
121
+ { x: 0, y: 0, name: ctx.name || 'anon', color: (ctx.tick * 37) % 256 },
122
+ { owner: ctx.clientId },
123
+ );
124
+ },
125
+
126
+ onLeave(state, ctx) {
127
+ state.players.remove(ctx.clientId);
128
+ },
129
+
130
+ // Owner writes pass through here before they are accepted: return \`next\` to accept, \`prev\` to
131
+ // reject, or a clamped object. This is where cheating stops; delete it and anything in range
132
+ // is accepted.
133
+ validate: {
134
+ players(_prev, next) {
135
+ return next;
136
+ },
137
+ },
138
+ ${tick}});
139
+ `;
140
+ }
141
+ var ROOM_TEST_TEMPLATE = `/**
142
+ * The smoke test \`irtio init\` scaffolds: two clients join, one writes a field it owns, and every
143
+ * view agrees with the server. \`testRoom\` runs the real room and real client semantics in
144
+ * process \u2014 no sockets, no server, fake clock \u2014 so this is a unit test in every way that matters.
145
+ *
146
+ * Extend it: that is what it is for.
147
+ */
148
+
149
+ import { expect, test } from 'vitest';
150
+
151
+ import { testRoom } from '@irtio/testing';
152
+ import '@irtio/testing/matchers';
153
+
154
+ import room from './room.js';
155
+
156
+ test('two players join, and a write to my own player reaches everyone', async () => {
157
+ const t = await testRoom(room);
158
+ const [a, b] = await t.join(2, { role: 'player' });
159
+
160
+ const mine = a.state.players.get(a.me);
161
+ expect(mine).toBeDefined();
162
+ mine!.x = 10;
163
+ a.flush();
164
+ await t.tick(2);
165
+
166
+ expect(t.state.players.get(a.me)?.x).toBe(10);
167
+ expect(b.state.players.get(a.me)?.x).toBe(10);
168
+ expect(t).toHaveConverged();
169
+ });
170
+ `;
171
+ function nextSteps(installCommand) {
172
+ return [
173
+ "",
174
+ pc.bold("next:"),
175
+ "",
176
+ " 1. install the packages",
177
+ pc.cyan(` ${installCommand}`),
178
+ "",
179
+ " 2. start the room",
180
+ pc.cyan(" npx irtio dev"),
181
+ " it prints a ws:// URL and an inspector page.",
182
+ "",
183
+ " 3. join from your game \u2014 three lines:",
184
+ "",
185
+ pc.cyan(" import { joinRoom } from '@irtio/client';"),
186
+ pc.cyan(" import { schema } from './irtio/schema.js';"),
187
+ "",
188
+ pc.cyan(" const room = await joinRoom(schema); // reads ?room=, or creates one"),
189
+ pc.cyan(" const me = room.state.players[room.me]; // yours: write it like a local"),
190
+ pc.cyan(" me.x = 10; // object, and it syncs"),
191
+ "",
192
+ " draw everyone else the same way you drew one player:",
193
+ pc.cyan(" for (const [id, p] of room.state.players) draw(p);"),
194
+ "",
195
+ " 4. open your page, then look at the address bar: it now ends in ?room=CODE.",
196
+ " copy that whole URL into a second tab. Both tabs are in the same room, and",
197
+ " each sees the other move. (Drop in <irt-lobby> from @irtio/lobby and it",
198
+ " shows the code, the link and a QR for you.)",
199
+ "",
200
+ " 5. check it under load before you believe it",
201
+ pc.cyan(" npx irtio simulate --bots 5 --seconds 10"),
202
+ ""
203
+ ];
204
+ }
205
+ async function defaultAsk(question) {
206
+ if (!process.stdin.isTTY) return "";
207
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
208
+ try {
209
+ return await rl.question(question);
210
+ } finally {
211
+ rl.close();
212
+ }
213
+ }
214
+ async function updatePackageJson(file) {
215
+ const source = await readFile(file, "utf8");
216
+ let parsed;
217
+ try {
218
+ parsed = JSON.parse(source);
219
+ } catch (err) {
220
+ throw new Error(`irtio init: ${file} is not valid JSON: ${String(err)}`);
221
+ }
222
+ const deps = { ...parsed.dependencies ?? {} };
223
+ let changed = false;
224
+ for (const name of INIT_DEPENDENCIES) {
225
+ if (deps[name] === void 0) {
226
+ deps[name] = "^0.0.0";
227
+ changed = true;
228
+ }
229
+ }
230
+ if (!changed) return "unchanged";
231
+ parsed.dependencies = Object.fromEntries(
232
+ Object.entries(deps).sort(([a], [b]) => a < b ? -1 : 1)
233
+ );
234
+ await writeFile(file, `${JSON.stringify(parsed, null, 2)}
235
+ `, "utf8");
236
+ return "updated";
237
+ }
238
+ async function runInit(options = {}) {
239
+ const cwd = path.resolve(options.cwd ?? process.cwd());
240
+ const dir = path.resolve(cwd, options.dir ?? ".");
241
+ const log = options.log ?? ((line) => console.log(line));
242
+ const ask = options.ask ?? defaultAsk;
243
+ let mode = options.mode;
244
+ if (mode === void 0) {
245
+ const answer = (await ask("does anything move without a player doing something? (y/n) ")).trim().toLowerCase();
246
+ mode = answer.startsWith("n") ? "event" : "tick";
247
+ }
248
+ const configFile = path.join(dir, "irtio.json");
249
+ let project = options.projectId ?? generateProjectId();
250
+ if (existsSync(configFile)) {
251
+ try {
252
+ const existing = JSON.parse(await readFile(configFile, "utf8"));
253
+ if (typeof existing.project === "string" && existing.project !== "")
254
+ project = existing.project;
255
+ } catch {
256
+ }
257
+ }
258
+ const files = [
259
+ ["irtio/schema.ts", schemaTemplate(project)],
260
+ ["irtio/rpc.ts", RPC_TEMPLATE],
261
+ ["irtio/room.ts", roomTemplate(mode)],
262
+ ["irtio/room.test.ts", ROOM_TEST_TEMPLATE],
263
+ ["irtio.json", `${JSON.stringify({ project }, null, 2)}
264
+ `]
265
+ ];
266
+ const created = [];
267
+ const skipped = [];
268
+ await mkdir(path.join(dir, "irtio"), { recursive: true });
269
+ for (const [relative, contents] of files) {
270
+ const file = path.join(dir, relative);
271
+ if (existsSync(file)) {
272
+ skipped.push(relative);
273
+ continue;
274
+ }
275
+ await writeFile(file, contents, "utf8");
276
+ created.push(relative);
277
+ }
278
+ const packageFile = path.join(dir, "package.json");
279
+ const packageJson = existsSync(packageFile) ? await updatePackageJson(packageFile) : "absent";
280
+ if (created.length > 0) log(pc.green(`created ${created.join(", ")}`));
281
+ if (skipped.length > 0) {
282
+ log(pc.yellow(`kept ${skipped.join(", ")} (already there \u2014 nothing was overwritten)`));
283
+ }
284
+ log(pc.dim(`project ${project} \xB7 ${mode} mode`));
285
+ const installCommand = packageJson === "absent" ? `npm i ${INIT_DEPENDENCIES.join(" ")}` : "npm install # the four irtio packages were added to your package.json";
286
+ for (const line of nextSteps(installCommand)) log(line);
287
+ return { dir, project, mode, created, skipped, packageJson };
288
+ }
289
+ async function init(args) {
290
+ try {
291
+ const parsed = parseInitArgs(args);
292
+ await runInit({
293
+ ...parsed.dir !== void 0 ? { dir: parsed.dir } : {},
294
+ ...parsed.mode !== void 0 ? { mode: parsed.mode } : {}
295
+ });
296
+ } catch (err) {
297
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
298
+ process.exitCode = 1;
299
+ }
300
+ }
301
+ export {
302
+ INIT_DEPENDENCIES,
303
+ generateProjectId,
304
+ init,
305
+ parseInitArgs,
306
+ runInit
307
+ };
@@ -0,0 +1,136 @@
1
+ import {
2
+ credentialsPath,
3
+ resolveControlUrl,
4
+ writeCredential
5
+ } from "./chunk-UYN5PWLT.js";
6
+
7
+ // src/login.ts
8
+ import { spawn } from "child_process";
9
+ import { randomBytes } from "crypto";
10
+ import { createServer } from "http";
11
+ import pc from "picocolors";
12
+ var LOGIN_TIMEOUT_MS = 3 * 60 * 1e3;
13
+ function parseLoginArgs(args) {
14
+ const parsed = {};
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg === "--url") {
18
+ const value = args[++i];
19
+ if (value === void 0) throw new Error("irtio login: --url needs a value");
20
+ parsed.url = value;
21
+ } else {
22
+ throw new Error(`irtio login: unknown option ${JSON.stringify(arg)}`);
23
+ }
24
+ }
25
+ return parsed;
26
+ }
27
+ function isLoopback(req) {
28
+ const addr = req.socket.remoteAddress?.replace(/^::ffff:/, "");
29
+ return addr === "127.0.0.1" || addr === "::1";
30
+ }
31
+ async function readBody(req) {
32
+ const chunks = [];
33
+ for await (const chunk of req) chunks.push(chunk);
34
+ return Buffer.concat(chunks).toString("utf8");
35
+ }
36
+ function defaultOpenBrowser(url) {
37
+ const platform = process.platform;
38
+ if (platform === "win32") {
39
+ spawn("cmd", ["/c", "start", '""', url], { stdio: "ignore", detached: true }).unref();
40
+ } else if (platform === "darwin") {
41
+ spawn("open", [url], { stdio: "ignore", detached: true }).unref();
42
+ } else {
43
+ spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
44
+ }
45
+ }
46
+ async function runLogin(options = {}) {
47
+ const controlUrl = resolveControlUrl(options.controlUrl);
48
+ const log = options.log ?? ((line) => console.log(line));
49
+ const openBrowser = options.openBrowser ?? defaultOpenBrowser;
50
+ const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
51
+ const state = randomBytes(8).toString("hex");
52
+ return new Promise((resolve, reject) => {
53
+ let settled = false;
54
+ const finish = (fn) => {
55
+ if (settled) return;
56
+ settled = true;
57
+ clearTimeout(timer);
58
+ server.close();
59
+ fn();
60
+ };
61
+ const server = createServer((req, res) => {
62
+ void (async () => {
63
+ if (!isLoopback(req)) {
64
+ res.writeHead(403).end();
65
+ return;
66
+ }
67
+ if (req.method !== "POST" || req.url?.split("?")[0] !== "/token") {
68
+ res.writeHead(404).end();
69
+ return;
70
+ }
71
+ try {
72
+ const body = await readBody(req);
73
+ const parsed = JSON.parse(body);
74
+ if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "string") {
75
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "expected { token, expiresAt }" }));
76
+ return;
77
+ }
78
+ const credential = {
79
+ token: parsed.token,
80
+ expiresAt: parsed.expiresAt,
81
+ ...typeof parsed.email === "string" && parsed.email !== "" ? { email: parsed.email } : {}
82
+ };
83
+ const file = await writeCredential(controlUrl, credential);
84
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
85
+ finish(() => resolve({ controlUrl, email: credential.email, file }));
86
+ } catch (err) {
87
+ res.writeHead(500).end();
88
+ finish(() => reject(err instanceof Error ? err : new Error(String(err))));
89
+ }
90
+ })();
91
+ });
92
+ server.listen(0, "127.0.0.1", () => {
93
+ const address = server.address();
94
+ const port = typeof address === "object" && address !== null ? address.port : 0;
95
+ const authUrl = `${controlUrl}/cli-auth?port=${port}&state=${state}`;
96
+ log(`opening ${authUrl}`);
97
+ try {
98
+ openBrowser(authUrl);
99
+ } catch {
100
+ log("could not open a browser \u2014 open this:");
101
+ log(pc.cyan(` ${authUrl}`));
102
+ }
103
+ log("waiting for the browser to finish signing in\u2026");
104
+ });
105
+ server.on("error", (err) => finish(() => reject(err)));
106
+ const timer = setTimeout(() => {
107
+ finish(
108
+ () => reject(
109
+ new Error(
110
+ "irtio login: timed out waiting for the browser after 3 minutes \u2014 run `irtio login` again"
111
+ )
112
+ )
113
+ );
114
+ }, timeoutMs);
115
+ });
116
+ }
117
+ async function login(args) {
118
+ try {
119
+ const parsed = parseLoginArgs(args);
120
+ const result = await runLogin({
121
+ ...parsed.url !== void 0 ? { controlUrl: parsed.url } : {}
122
+ });
123
+ console.log(pc.green(`logged in as ${result.email ?? "(email not provided by the server)"}`));
124
+ console.log(pc.dim(`control plane: ${result.controlUrl}`));
125
+ console.log(pc.dim(`credentials stored at ${result.file ?? credentialsPath()}`));
126
+ } catch (err) {
127
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
128
+ process.exitCode = 1;
129
+ }
130
+ }
131
+ export {
132
+ defaultOpenBrowser,
133
+ login,
134
+ parseLoginArgs,
135
+ runLogin
136
+ };