@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,221 @@
1
+ // src/simulate.ts
2
+ import { existsSync } from "fs";
3
+ import { mkdir } from "fs/promises";
4
+ import * as path from "path";
5
+ import { fileURLToPath, pathToFileURL } from "url";
6
+ import { randomScript, relayEchoScript, spawnBots } from "@irtio/bots";
7
+ import * as esbuild from "esbuild";
8
+ import pc from "picocolors";
9
+ var DEFAULT_URL = "ws://localhost:7070";
10
+ var DEFAULT_BOTS = 5;
11
+ var DEFAULT_SECONDS = 10;
12
+ var SCHEMA_CANDIDATES = ["irtio/schema.ts", "irtio/schema.js", "schema.ts"];
13
+ function positive(raw, flag) {
14
+ const value = Number(raw);
15
+ if (!Number.isFinite(value) || value <= 0) {
16
+ throw new Error(`irtio simulate: ${flag} must be a positive number`);
17
+ }
18
+ return value;
19
+ }
20
+ function parseSimulateArgs(args) {
21
+ const parsed = { bots: DEFAULT_BOTS, seconds: DEFAULT_SECONDS, cheat: false };
22
+ for (let i = 0; i < args.length; i++) {
23
+ const arg = args[i];
24
+ const eq = arg.indexOf("=");
25
+ const flag = eq === -1 ? arg : arg.slice(0, eq);
26
+ const inline = eq === -1 ? void 0 : arg.slice(eq + 1);
27
+ const value = () => {
28
+ const v = inline ?? args[++i];
29
+ if (v === void 0) throw new Error(`irtio simulate: ${flag} needs a value`);
30
+ return v;
31
+ };
32
+ switch (flag) {
33
+ case "--bots": {
34
+ const bots = positive(value(), "--bots");
35
+ if (!Number.isInteger(bots))
36
+ throw new Error("irtio simulate: --bots must be a whole number");
37
+ parsed.bots = bots;
38
+ break;
39
+ }
40
+ case "--seconds":
41
+ parsed.seconds = positive(value(), "--seconds");
42
+ break;
43
+ case "--room":
44
+ parsed.room = value();
45
+ break;
46
+ case "--url":
47
+ parsed.url = value();
48
+ break;
49
+ case "--key":
50
+ parsed.key = value();
51
+ break;
52
+ case "--trace":
53
+ parsed.trace = value();
54
+ break;
55
+ case "--cheat":
56
+ parsed.cheat = true;
57
+ break;
58
+ default:
59
+ throw new Error(`irtio simulate: unknown option ${JSON.stringify(arg)}`);
60
+ }
61
+ }
62
+ return parsed;
63
+ }
64
+ function monorepoPackages() {
65
+ const packagesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
66
+ const schema = path.join(packagesDir, "schema/src/index.ts");
67
+ if (!existsSync(schema)) return void 0;
68
+ return {
69
+ "@irtio/schema": schema,
70
+ "@irtio/protocol": path.join(packagesDir, "protocol/src/index.ts"),
71
+ "@irtio/server": path.join(packagesDir, "server/src/index.ts")
72
+ };
73
+ }
74
+ function looksLikeSchema(value) {
75
+ const candidate = value;
76
+ return typeof candidate === "object" && candidate !== null && Array.isArray(candidate.collections) && candidate.hash8 instanceof Uint8Array;
77
+ }
78
+ async function loadProjectSchema(options) {
79
+ const entry = SCHEMA_CANDIDATES.map((c) => path.resolve(options.cwd, c)).find(
80
+ (f) => existsSync(f)
81
+ );
82
+ if (entry === void 0) return void 0;
83
+ await mkdir(options.outDir, { recursive: true });
84
+ const outfile = path.join(options.outDir, "schema.mjs");
85
+ const alias = options.irtioPackages ?? monorepoPackages();
86
+ await esbuild.build({
87
+ entryPoints: [entry],
88
+ bundle: true,
89
+ format: "esm",
90
+ platform: "node",
91
+ outfile,
92
+ ...alias !== void 0 ? { alias } : {}
93
+ });
94
+ const module = await import(`${pathToFileURL(outfile).href}?v=${Date.now()}`);
95
+ const schema = module.schema ?? module.default;
96
+ if (!looksLikeSchema(schema)) {
97
+ const names = Object.keys(module).join(", ") || "nothing";
98
+ throw new Error(
99
+ `irtio simulate: ${entry} does not export a schema.
100
+ it exports: ${names}
101
+ add \`export const schema = defineSchema({ ... })\` (this is what \`irtio init\` writes)`
102
+ );
103
+ }
104
+ return { schema, file: entry };
105
+ }
106
+ function rate(bytesPerSecond) {
107
+ return bytesPerSecond >= 1e3 ? `${(bytesPerSecond / 1e3).toFixed(1)} kB/s` : `${Math.round(bytesPerSecond)} B/s`;
108
+ }
109
+ function formatReport(report, schemaLoaded, cheating) {
110
+ const lines = [];
111
+ for (const invariant of report.invariants) {
112
+ const mark = invariant.ok ? pc.green("ok ") : pc.red("FAIL");
113
+ lines.push(` ${mark} ${invariant.name.padEnd(17)} ${pc.dim(invariant.detail)}`);
114
+ }
115
+ lines.push("");
116
+ const convergence = report.convergenceLagMs === void 0 ? "convergence not sampled" : `convergence ${report.convergenceLagMs} ms (p50 of ${report.convergence?.samples ?? 0})`;
117
+ lines.push(
118
+ pc.dim(
119
+ ` ${report.framesPerSecond} frames/s \xB7 in ${rate(report.bytesInPerSecondPerBot)}/bot \xB7 out ${rate(report.bytesOutPerSecondPerBot)}/bot \xB7 ${report.totals.corrections} corrections \xB7 ${convergence}`
120
+ )
121
+ );
122
+ if (report.tracePath !== void 0) lines.push(pc.dim(` trace: ${report.tracePath}`));
123
+ lines.push("");
124
+ const failed = report.invariants.filter((i) => !i.ok);
125
+ if (failed.length === 0) {
126
+ lines.push(pc.green(` every invariant held across ${report.bots} bots.`));
127
+ } else {
128
+ lines.push(
129
+ pc.red(` ${failed.length} invariant(s) failed: ${failed.map((i) => i.name).join(", ")}`)
130
+ );
131
+ }
132
+ if (cheating && report.totals.corrections === 0) {
133
+ lines.push(
134
+ pc.yellow(
135
+ " --cheat drew no corrections: the room accepted every illegal write.\n add rules to `validate` in irtio/room.ts (reject by returning `prev`, or clamp)."
136
+ )
137
+ );
138
+ }
139
+ if (!schemaLoaded) {
140
+ lines.push(
141
+ pc.yellow(
142
+ " no irtio/schema.ts here, so this was a schema-less relay run: presence and messages only.\n run it from a project directory to simulate your room."
143
+ )
144
+ );
145
+ }
146
+ return lines;
147
+ }
148
+ async function runSimulation(options = {}) {
149
+ const cwd = path.resolve(options.cwd ?? process.cwd());
150
+ const output = [];
151
+ const log = (line) => {
152
+ output.push(line);
153
+ (options.log ?? ((l) => console.log(l)))(line);
154
+ };
155
+ const bots = options.bots ?? DEFAULT_BOTS;
156
+ const seconds = options.seconds ?? DEFAULT_SECONDS;
157
+ const url = options.url ?? DEFAULT_URL;
158
+ const outDir = path.join(cwd, ".irtio", "sim");
159
+ const loaded = await loadProjectSchema({
160
+ cwd,
161
+ outDir,
162
+ irtioPackages: options.irtioPackages
163
+ });
164
+ const common = {
165
+ url,
166
+ durationMs: seconds * 1e3,
167
+ ...options.key !== void 0 ? { key: options.key } : {},
168
+ ...options.room !== void 0 ? { room: options.room } : {}
169
+ };
170
+ const runner = loaded ? await spawnBots(bots, {
171
+ ...common,
172
+ schema: loaded.schema,
173
+ script: randomScript(loaded.schema, { cheat: options.cheat ?? false })
174
+ }) : await spawnBots(bots, { ...common, script: relayEchoScript() });
175
+ log("");
176
+ log(
177
+ `${pc.bold("irtio simulate")} \u2014 ${bots} bot${bots === 1 ? "" : "s"} \xD7 ${seconds}s on ${url} (room ${pc.bold(runner.roomId)})${options.cheat ? pc.yellow(" [cheat]") : ""}`
178
+ );
179
+ log("");
180
+ await runner.done();
181
+ const stopped = await runner.stop();
182
+ await mkdir(outDir, { recursive: true });
183
+ const tracePath = path.resolve(
184
+ cwd,
185
+ options.trace ?? path.join(outDir, `trace-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`)
186
+ );
187
+ await mkdir(path.dirname(tracePath), { recursive: true });
188
+ await runner.trace.save(tracePath);
189
+ const report = { ...stopped, tracePath };
190
+ for (const line of formatReport(report, loaded !== void 0, options.cheat ?? false)) {
191
+ log(line);
192
+ }
193
+ for (const failure of runner.scriptErrors) {
194
+ log(pc.red(` bot ${failure.bot} script failed: ${String(failure.error)}`));
195
+ }
196
+ return { report, tracePath, schema: loaded !== void 0, output };
197
+ }
198
+ async function simulate(args) {
199
+ let parsed;
200
+ try {
201
+ parsed = parseSimulateArgs(args);
202
+ } catch (err) {
203
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
204
+ process.exitCode = 1;
205
+ return;
206
+ }
207
+ try {
208
+ const run = await runSimulation(parsed);
209
+ if (!run.report.ok) process.exitCode = 1;
210
+ } catch (err) {
211
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
212
+ console.error(pc.dim("is `irtio dev` running? pass --url to point somewhere else."));
213
+ process.exitCode = 1;
214
+ }
215
+ }
216
+ export {
217
+ loadProjectSchema,
218
+ parseSimulateArgs,
219
+ runSimulation,
220
+ simulate
221
+ };
@@ -0,0 +1,63 @@
1
+ import {
2
+ createApiClient,
3
+ isLoginRequired
4
+ } from "./chunk-J3G6AUJY.js";
5
+ import {
6
+ resolveControlUrlForUser
7
+ } from "./chunk-UYN5PWLT.js";
8
+
9
+ // src/whoami.ts
10
+ import pc from "picocolors";
11
+ function parseWhoamiArgs(args) {
12
+ const parsed = {};
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ switch (arg) {
16
+ case "--url": {
17
+ const value = args[++i];
18
+ if (value === void 0) throw new Error("irtio whoami: --url needs a value");
19
+ parsed.url = value;
20
+ break;
21
+ }
22
+ default:
23
+ throw new Error(`irtio whoami: unknown option ${JSON.stringify(arg)}`);
24
+ }
25
+ }
26
+ return parsed;
27
+ }
28
+ async function runWhoami(options = {}) {
29
+ const log = options.log ?? ((line) => console.log(line));
30
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
31
+ const client = options.client ?? await createApiClient(controlUrl);
32
+ const me = await client.get("/v1/me");
33
+ log(`${pc.green(me.email)}${me.isAdmin ? pc.dim(" (admin)") : ""}`);
34
+ log(pc.dim(`org: ${me.orgId}`));
35
+ log(pc.dim(`control plane: ${me.controlUrl}`));
36
+ return me;
37
+ }
38
+ async function whoami(args, deps = {}) {
39
+ const log = deps.log ?? ((line) => console.log(line));
40
+ const errorLog = deps.errorLog ?? ((line) => console.error(line));
41
+ try {
42
+ const parsed = parseWhoamiArgs(args);
43
+ await runWhoami({
44
+ log,
45
+ ...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
46
+ ...deps.client !== void 0 ? { client: deps.client } : {}
47
+ });
48
+ } catch (err) {
49
+ if (isLoginRequired(err)) {
50
+ errorLog(pc.red("not logged in"));
51
+ errorLog("run: irtio login");
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ errorLog(pc.red(err instanceof Error ? err.message : String(err)));
56
+ process.exitCode = 1;
57
+ }
58
+ }
59
+ export {
60
+ parseWhoamiArgs,
61
+ runWhoami,
62
+ whoami
63
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@irtio/cli",
3
+ "version": "0.1.0",
4
+ "description": "irtio CLI: local dev server, room scaffolding, bot simulation, deploy, logs and rooms",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./bundle": {
18
+ "types": "./dist/bundle.d.ts",
19
+ "import": "./dist/bundle.js"
20
+ }
21
+ },
22
+ "bin": {
23
+ "irtio": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "dependencies": {
29
+ "esbuild": "^0.27.3",
30
+ "picocolors": "^1.1.1",
31
+ "ws": "^8.18.0",
32
+ "@irtio/bots": "0.1.0",
33
+ "@irtio/client": "0.1.0",
34
+ "@irtio/protocol": "0.1.0",
35
+ "@irtio/runtime": "0.1.0",
36
+ "@irtio/schema": "0.1.0",
37
+ "@irtio/server": "0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@irtio/supervisor": "0.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "test": "vitest run"
45
+ }
46
+ }