@eigeninteractive/testkit 0.5.1 → 0.6.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.
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- LocalStore
4
- } from "./chunk-YJE4AGHJ.js";
5
-
6
- // src/inspect-cli.ts
2
+ import { t as LocalStore } from "./local-state-CnO-Oi-R.js";
7
3
  import { parseArgs } from "node:util";
8
- var HELP = `Usage: eigen-inspect <command> [options]
4
+ //#region src/inspect-cli.ts
5
+ /**
6
+ * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.
7
+ *
8
+ * Deliberately narrow. Wrangler already ships a first-party browser for local
9
+ * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and
10
+ * generic table browsing is its job, not this tool's. What that view cannot do
11
+ * is answer a question in the engine's own terms, because a game's truth is
12
+ * split across two stores on purpose: the D1 index and the game's own Durable
13
+ * Object. This reads both, decodes the JSON columns, prints the transition log
14
+ * as a timeline, and says what the game is waiting for.
15
+ *
16
+ * It also runs with no dev server, no browser and no account, and emits
17
+ * `--json` for a script or an agent.
18
+ */
19
+ const HELP = `Usage: eigen-inspect <command> [options]
9
20
 
10
21
  Read the local .wrangler state of an EigenInteractive Worker: the D1 index and
11
22
  each game's own Durable Object database.
@@ -30,169 +41,174 @@ Options:
30
41
  Every database is opened read-only, so this is safe to run while the Worker is
31
42
  running. Wrangler's own browser for the same files is \`e\` in \`wrangler dev\`.`;
32
43
  function relative(ms) {
33
- if (ms === null) return "never";
34
- const delta = Date.now() - ms;
35
- const future = delta < 0;
36
- const seconds = Math.round(Math.abs(delta) / 1e3);
37
- const [value, unit] = seconds < 60 ? [seconds, "s"] : seconds < 3600 ? [Math.round(seconds / 60), "m"] : seconds < 86400 ? [Math.round(seconds / 3600), "h"] : [Math.round(seconds / 86400), "d"];
38
- return future ? `in ${value}${unit}` : `${value}${unit} ago`;
44
+ if (ms === null) return "never";
45
+ const delta = Date.now() - ms;
46
+ const future = delta < 0;
47
+ const seconds = Math.round(Math.abs(delta) / 1e3);
48
+ const [value, unit] = seconds < 60 ? [seconds, "s"] : seconds < 3600 ? [Math.round(seconds / 60), "m"] : seconds < 86400 ? [Math.round(seconds / 3600), "h"] : [Math.round(seconds / 86400), "d"];
49
+ return future ? `in ${value}${unit}` : `${value}${unit} ago`;
39
50
  }
51
+ /** Render rows as an aligned table. Values are stringified defensively because
52
+ * SQLite hands back whatever the column holds, including blobs and nulls. */
40
53
  function table(rows) {
41
- if (rows.length === 0) return "(no rows)";
42
- const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
43
- const cell = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
44
- const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));
45
- const line = (values) => values.map((value, i) => value.padEnd(widths[i])).join(" ").trimEnd();
46
- return [line(columns), line(widths.map((w) => "-".repeat(w))), ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))].join("\n");
54
+ if (rows.length === 0) return "(no rows)";
55
+ const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
56
+ const cell = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
57
+ const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));
58
+ const line = (values) => values.map((value, i) => value.padEnd(widths[i])).join(" ").trimEnd();
59
+ return [
60
+ line(columns),
61
+ line(widths.map((w) => "-".repeat(w))),
62
+ ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))
63
+ ].join("\n");
47
64
  }
48
65
  function short(id, length = 8) {
49
- return id === null ? "" : id.length <= length ? id : `${id.slice(0, length)}\u2026`;
66
+ return id === null ? "" : id.length <= length ? id : `${id.slice(0, length)}…`;
50
67
  }
51
68
  function seatLabel(seat) {
52
- const identity = seat.username ?? short(seat.userId ?? seat.botId);
53
- return `${seat.playerIndex}:${identity || "(empty)"}${seat.type === "bot" ? " (bot)" : ""}`;
69
+ const identity = seat.username ?? short(seat.userId ?? seat.botId);
70
+ return `${seat.playerIndex}:${identity || "(empty)"}${seat.type === "bot" ? " (bot)" : ""}`;
54
71
  }
55
72
  function timingLabel(row) {
56
- if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;
57
- if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? "" : ` +${row.incrementSeconds}s`}`;
58
- return "untimed";
73
+ if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;
74
+ if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? "" : ` +${row.incrementSeconds}s`}`;
75
+ return "untimed";
59
76
  }
60
77
  function gamesTable(rows) {
61
- return rows.map((row) => ({
62
- id: short(row.id, 8),
63
- code: row.shortCode,
64
- status: row.status,
65
- access: row.access,
66
- seats: `${row.minPlayers}-${row.maxPlayers}`,
67
- rated: row.rated ? "yes" : "no",
68
- timing: timingLabel(row),
69
- pending: row.pendingPlayers === null ? "" : JSON.stringify(row.pendingPlayers),
70
- updated: relative(row.updatedAt)
71
- }));
78
+ return rows.map((row) => ({
79
+ id: short(row.id, 8),
80
+ code: row.shortCode,
81
+ status: row.status,
82
+ access: row.access,
83
+ seats: `${row.minPlayers}-${row.maxPlayers}`,
84
+ rated: row.rated ? "yes" : "no",
85
+ timing: timingLabel(row),
86
+ pending: row.pendingPlayers === null ? "" : JSON.stringify(row.pendingPlayers),
87
+ updated: relative(row.updatedAt)
88
+ }));
72
89
  }
90
+ /** The `game` report: the whole session in reading order, index first, then the
91
+ * authoritative session, then the log, then the one sentence that explains it. */
73
92
  function renderGame(view) {
74
- const out = [];
75
- const index = view.index;
76
- out.push(`Game ${view.gameId}${index === null ? "" : ` ${index.shortCode}`}`);
77
- if (index === null) {
78
- out.push("", "D1 index (no games row)");
79
- } else {
80
- out.push(
81
- "",
82
- `D1 index status=${index.status} access=${index.access} rated=${index.rated ? "yes" : "no"} schema=v${index.schemaVersion}`,
83
- ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`,
84
- `config ${JSON.stringify(index.config)}`,
85
- `participants ${view.indexSeats.length === 0 ? "(none)" : view.indexSeats.map(seatLabel).join(" ")}`
86
- );
87
- if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);
88
- }
89
- out.push("");
90
- if (view.durableObject === null) {
91
- out.push("Durable Object (none: no command or socket has touched this game yet)");
92
- } else {
93
- out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);
94
- const meta = view.meta;
95
- if (meta === null) {
96
- out.push("DO meta (no meta row: the object exists but never initialised)");
97
- } else {
98
- out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? "(unset, so no start has committed)" : short(meta.rngSeed, 12)}`);
99
- }
100
- out.push(`roster ${view.roster.length === 0 ? "(empty)" : view.roster.map(seatLabel).join(" ")}`);
101
- out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? "none armed" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);
102
- out.push(`commands ${view.commands.length} recorded`);
103
- if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(", ")}`);
104
- }
105
- out.push("", "Timeline");
106
- if (view.transitions.length === 0) {
107
- out.push(" (no transitions: the game has not started, so there is no v0)");
108
- } else {
109
- for (const transition of view.transitions) {
110
- const action = transition.action;
111
- const cause = action === null ? "start" : `${action.kind}/${action.type}${action.playerIndex === null ? "" : ` seat ${action.playerIndex}`}`;
112
- const payload = action === null || action.data === null || action.data === void 0 ? "" : ` ${JSON.stringify(action.data)}`;
113
- out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);
114
- out.push(` state ${JSON.stringify(transition.state)}`);
115
- out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? "none" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? "(compacted)" : JSON.stringify(transition.frameSeats)}`);
116
- }
117
- }
118
- out.push("", `Diagnosis ${view.diagnosis.summary}`);
119
- for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);
120
- return out.join("\n");
93
+ const out = [];
94
+ const index = view.index;
95
+ out.push(`Game ${view.gameId}${index === null ? "" : ` ${index.shortCode}`}`);
96
+ if (index === null) out.push("", "D1 index (no games row)");
97
+ else {
98
+ out.push("", `D1 index status=${index.status} access=${index.access} rated=${index.rated ? "yes" : "no"} schema=v${index.schemaVersion}`, ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`, `config ${JSON.stringify(index.config)}`, `participants ${view.indexSeats.length === 0 ? "(none)" : view.indexSeats.map(seatLabel).join(" ")}`);
99
+ if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);
100
+ }
101
+ out.push("");
102
+ if (view.durableObject === null) out.push("Durable Object (none: no command or socket has touched this game yet)");
103
+ else {
104
+ out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);
105
+ const meta = view.meta;
106
+ if (meta === null) out.push("DO meta (no meta row: the object exists but never initialised)");
107
+ else out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? "(unset, so no start has committed)" : short(meta.rngSeed, 12)}`);
108
+ out.push(`roster ${view.roster.length === 0 ? "(empty)" : view.roster.map(seatLabel).join(" ")}`);
109
+ out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? "none armed" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);
110
+ if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(", ")}`);
111
+ }
112
+ out.push("", "Timeline");
113
+ if (view.transitions.length === 0) out.push(" (no transitions: the game has not started, so there is no v0)");
114
+ else for (const transition of view.transitions) {
115
+ const action = transition.action;
116
+ const cause = action === null ? "start" : `${action.kind}/${action.type}${action.playerIndex === null ? "" : ` seat ${action.playerIndex}`}`;
117
+ const payload = action === null || action.data === null || action.data === void 0 ? "" : ` ${JSON.stringify(action.data)}`;
118
+ out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);
119
+ out.push(` state ${JSON.stringify(transition.state)}`);
120
+ out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? "none" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? "(compacted)" : JSON.stringify(transition.frameSeats)}`);
121
+ }
122
+ out.push("", `Diagnosis ${view.diagnosis.summary}`);
123
+ for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);
124
+ return out.join("\n");
121
125
  }
122
126
  function main() {
123
- const { values, positionals } = parseArgs({
124
- allowPositionals: true,
125
- options: {
126
- dir: { type: "string" },
127
- status: { type: "string" },
128
- limit: { type: "string" },
129
- game: { type: "string" },
130
- json: { type: "boolean" },
131
- help: { type: "boolean", short: "h" }
132
- }
133
- });
134
- const [command, ...rest] = positionals;
135
- if (values.help === true || command === void 0) {
136
- console.log(HELP);
137
- return;
138
- }
139
- const emit = (value, text) => {
140
- console.log(values.json === true ? JSON.stringify(value, null, 2) : text());
141
- };
142
- const store = LocalStore.open(values.dir);
143
- try {
144
- switch (command) {
145
- case "games": {
146
- const rows = store.games({ status: values.status, limit: values.limit === void 0 ? 20 : Number(values.limit) });
147
- emit(rows, () => rows.length === 0 ? "no games in the local D1 index" : table(gamesTable(rows)));
148
- return;
149
- }
150
- case "game": {
151
- const ref = rest[0];
152
- if (ref === void 0) throw new Error("game needs an id, short code, or id prefix");
153
- const view = store.game(ref);
154
- if (view === null) throw new Error(`no game matching "${ref}" in the local D1 index or in any local Durable Object`);
155
- emit(view, () => renderGame(view));
156
- return;
157
- }
158
- case "do": {
159
- const objects = store.durableObjects().map((object) => ({ class: object.className, game: object.name ?? "(unnamed)", doId: short(object.id, 12), file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file }));
160
- emit(objects, () => objects.length === 0 ? "no local Durable Objects: nothing has created a game session yet" : table(objects));
161
- return;
162
- }
163
- case "players": {
164
- const users = store.query("select id, username, display_name, is_anonymous, created_at from users order by created_at desc");
165
- const bots = store.query("select id, username, display_name, type, schema_version from bots order by username");
166
- emit({ users, bots }, () => `Users
167
- ${table(users)}
168
-
169
- Bots
170
- ${table(bots)}`);
171
- return;
172
- }
173
- case "tables": {
174
- const rows = store.tables({ game: values.game });
175
- emit(rows, () => table(rows));
176
- return;
177
- }
178
- case "sql": {
179
- const sql = rest.join(" ");
180
- if (sql === "") throw new Error("sql needs a statement");
181
- const rows = store.query(sql, { game: values.game });
182
- emit(rows, () => table(rows));
183
- return;
184
- }
185
- default:
186
- throw new Error(`unknown command "${command}". Run eigen-inspect --help`);
187
- }
188
- } finally {
189
- store.close();
190
- }
127
+ const { values, positionals } = parseArgs({
128
+ allowPositionals: true,
129
+ options: {
130
+ dir: { type: "string" },
131
+ status: { type: "string" },
132
+ limit: { type: "string" },
133
+ game: { type: "string" },
134
+ json: { type: "boolean" },
135
+ help: {
136
+ type: "boolean",
137
+ short: "h"
138
+ }
139
+ }
140
+ });
141
+ const [command, ...rest] = positionals;
142
+ if (values.help === true || command === void 0) {
143
+ console.log(HELP);
144
+ return;
145
+ }
146
+ const emit = (value, text) => {
147
+ console.log(values.json === true ? JSON.stringify(value, null, 2) : text());
148
+ };
149
+ const store = LocalStore.open(values.dir);
150
+ try {
151
+ switch (command) {
152
+ case "games": {
153
+ const rows = store.games({
154
+ status: values.status,
155
+ limit: values.limit === void 0 ? 20 : Number(values.limit)
156
+ });
157
+ emit(rows, () => rows.length === 0 ? "no games in the local D1 index" : table(gamesTable(rows)));
158
+ return;
159
+ }
160
+ case "game": {
161
+ const ref = rest[0];
162
+ if (ref === void 0) throw new Error("game needs an id, short code, or id prefix");
163
+ const view = store.game(ref);
164
+ if (view === null) throw new Error(`no game matching "${ref}" in the local D1 index or in any local Durable Object`);
165
+ emit(view, () => renderGame(view));
166
+ return;
167
+ }
168
+ case "do": {
169
+ const objects = store.durableObjects().map((object) => ({
170
+ class: object.className,
171
+ game: object.name ?? "(unnamed)",
172
+ doId: short(object.id, 12),
173
+ file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file
174
+ }));
175
+ emit(objects, () => objects.length === 0 ? "no local Durable Objects: nothing has created a game session yet" : table(objects));
176
+ return;
177
+ }
178
+ case "players": {
179
+ const users = store.query("select id, username, display_name, is_anonymous, created_at from users order by created_at desc");
180
+ const bots = store.query("select id, username, display_name, type, schema_version from bots order by username");
181
+ emit({
182
+ users,
183
+ bots
184
+ }, () => `Users\n${table(users)}\n\nBots\n${table(bots)}`);
185
+ return;
186
+ }
187
+ case "tables": {
188
+ const rows = store.tables({ game: values.game });
189
+ emit(rows, () => table(rows));
190
+ return;
191
+ }
192
+ case "sql": {
193
+ const sql = rest.join(" ");
194
+ if (sql === "") throw new Error("sql needs a statement");
195
+ const rows = store.query(sql, { game: values.game });
196
+ emit(rows, () => table(rows));
197
+ return;
198
+ }
199
+ default: throw new Error(`unknown command "${command}". Run eigen-inspect --help`);
200
+ }
201
+ } finally {
202
+ store.close();
203
+ }
191
204
  }
192
205
  try {
193
- main();
206
+ main();
194
207
  } catch (error) {
195
- console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);
196
- process.exitCode = 1;
208
+ console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);
209
+ process.exitCode = 1;
197
210
  }
211
+ //#endregion
212
+ export {};
213
+
198
214
  //# sourceMappingURL=inspect-cli.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/inspect-cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.\n *\n * Deliberately narrow. Wrangler already ships a first-party browser for local\n * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and\n * generic table browsing is its job, not this tool's. What that view cannot do\n * is answer a question in the engine's own terms, because a game's truth is\n * split across two stores on purpose: the D1 index and the game's own Durable\n * Object. This reads both, decodes the JSON columns, prints the transition log\n * as a timeline, and says what the game is waiting for.\n *\n * It also runs with no dev server, no browser and no account, and emits\n * `--json` for a script or an agent.\n */\n\nimport { parseArgs } from \"node:util\";\nimport type { LocalGameRow, LocalGameView, LocalSeat } from \"./local-state.js\";\nimport { LocalStore } from \"./local-state.js\";\n\nconst HELP = `Usage: eigen-inspect <command> [options]\n\nRead the local .wrangler state of an EigenInteractive Worker: the D1 index and\neach game's own Durable Object database.\n\nCommands:\n games The D1 index, newest first\n game <id|code|prefix> One game across both stores, with its timeline\n do Every local Durable Object, mapped back to its game\n players Local users and bots, by id\n tables [--game <ref>] Table names and row counts\n sql <query> [--game <ref>]\n Run a read-only statement against D1 or one game's DO\n\nOptions:\n --dir <path> Start the search for .wrangler/state here (default: cwd)\n --status <s> games: filter by status\n --limit <n> games: row cap (default 20)\n --game <ref> tables/sql: target one game's Durable Object instead of D1\n --json Emit JSON instead of a table\n -h, --help Show this help\n\nEvery database is opened read-only, so this is safe to run while the Worker is\nrunning. Wrangler's own browser for the same files is \\`e\\` in \\`wrangler dev\\`.`;\n\nfunction relative(ms: number | null): string {\n if (ms === null) return \"never\";\n const delta = Date.now() - ms;\n const future = delta < 0;\n const seconds = Math.round(Math.abs(delta) / 1000);\n const [value, unit] = seconds < 60 ? [seconds, \"s\"] : seconds < 3600 ? [Math.round(seconds / 60), \"m\"] : seconds < 86400 ? [Math.round(seconds / 3600), \"h\"] : [Math.round(seconds / 86400), \"d\"];\n return future ? `in ${value}${unit}` : `${value}${unit} ago`;\n}\n\n/** Render rows as an aligned table. Values are stringified defensively because\n * SQLite hands back whatever the column holds, including blobs and nulls. */\nfunction table(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return \"(no rows)\";\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];\n const cell = (value: unknown): string => (value === null || value === undefined ? \"\" : typeof value === \"object\" ? JSON.stringify(value) : String(value));\n const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));\n const line = (values: string[]): string =>\n values\n .map((value, i) => value.padEnd(widths[i]))\n .join(\" \")\n .trimEnd();\n return [line(columns), line(widths.map((w) => \"-\".repeat(w))), ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))].join(\"\\n\");\n}\n\nfunction short(id: string | null, length = 8): string {\n return id === null ? \"\" : id.length <= length ? id : `${id.slice(0, length)}…`;\n}\n\nfunction seatLabel(seat: LocalSeat): string {\n const identity = seat.username ?? short(seat.userId ?? seat.botId);\n return `${seat.playerIndex}:${identity || \"(empty)\"}${seat.type === \"bot\" ? \" (bot)\" : \"\"}`;\n}\n\nfunction timingLabel(row: { turnSeconds: number | null; budgetSeconds: number | null; incrementSeconds: number | null }): string {\n if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;\n if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? \"\" : ` +${row.incrementSeconds}s`}`;\n return \"untimed\";\n}\n\nfunction gamesTable(rows: LocalGameRow[]): Record<string, unknown>[] {\n return rows.map((row) => ({\n id: short(row.id, 8),\n code: row.shortCode,\n status: row.status,\n access: row.access,\n seats: `${row.minPlayers}-${row.maxPlayers}`,\n rated: row.rated ? \"yes\" : \"no\",\n timing: timingLabel(row),\n pending: row.pendingPlayers === null ? \"\" : JSON.stringify(row.pendingPlayers),\n updated: relative(row.updatedAt),\n }));\n}\n\n/** The `game` report: the whole session in reading order, index first, then the\n * authoritative session, then the log, then the one sentence that explains it. */\nfunction renderGame(view: LocalGameView): string {\n const out: string[] = [];\n const index = view.index;\n out.push(`Game ${view.gameId}${index === null ? \"\" : ` ${index.shortCode}`}`);\n\n if (index === null) {\n out.push(\"\", \"D1 index (no games row)\");\n } else {\n out.push(\n \"\",\n `D1 index status=${index.status} access=${index.access} rated=${index.rated ? \"yes\" : \"no\"} schema=v${index.schemaVersion}`,\n ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`,\n `config ${JSON.stringify(index.config)}`,\n `participants ${view.indexSeats.length === 0 ? \"(none)\" : view.indexSeats.map(seatLabel).join(\" \")}`,\n );\n if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);\n }\n\n out.push(\"\");\n if (view.durableObject === null) {\n out.push(\"Durable Object (none: no command or socket has touched this game yet)\");\n } else {\n out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);\n const meta = view.meta;\n if (meta === null) {\n out.push(\"DO meta (no meta row: the object exists but never initialised)\");\n } else {\n out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? \"(unset, so no start has committed)\" : short(meta.rngSeed, 12)}`);\n }\n out.push(`roster ${view.roster.length === 0 ? \"(empty)\" : view.roster.map(seatLabel).join(\" \")}`);\n out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? \"none armed\" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);\n out.push(`commands ${view.commands.length} recorded`);\n if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(\", \")}`);\n }\n\n out.push(\"\", \"Timeline\");\n if (view.transitions.length === 0) {\n out.push(\" (no transitions: the game has not started, so there is no v0)\");\n } else {\n for (const transition of view.transitions) {\n const action = transition.action;\n const cause = action === null ? \"start\" : `${action.kind}/${action.type}${action.playerIndex === null ? \"\" : ` seat ${action.playerIndex}`}`;\n const payload = action === null || action.data === null || action.data === undefined ? \"\" : ` ${JSON.stringify(action.data)}`;\n out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);\n out.push(` state ${JSON.stringify(transition.state)}`);\n out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? \"none\" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? \"(compacted)\" : JSON.stringify(transition.frameSeats)}`);\n }\n }\n\n out.push(\"\", `Diagnosis ${view.diagnosis.summary}`);\n for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);\n return out.join(\"\\n\");\n}\n\nfunction main(): void {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n dir: { type: \"string\" },\n status: { type: \"string\" },\n limit: { type: \"string\" },\n game: { type: \"string\" },\n json: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n\n const [command, ...rest] = positionals;\n if (values.help === true || command === undefined) {\n console.log(HELP);\n return;\n }\n\n const emit = (value: unknown, text: () => string): void => {\n console.log(values.json === true ? JSON.stringify(value, null, 2) : text());\n };\n\n const store = LocalStore.open(values.dir);\n try {\n switch (command) {\n case \"games\": {\n const rows = store.games({ status: values.status, limit: values.limit === undefined ? 20 : Number(values.limit) });\n emit(rows, () => (rows.length === 0 ? \"no games in the local D1 index\" : table(gamesTable(rows))));\n return;\n }\n case \"game\": {\n const ref = rest[0];\n if (ref === undefined) throw new Error(\"game needs an id, short code, or id prefix\");\n const view = store.game(ref);\n if (view === null) throw new Error(`no game matching \"${ref}\" in the local D1 index or in any local Durable Object`);\n emit(view, () => renderGame(view));\n return;\n }\n case \"do\": {\n // Paths print relative to the state root: the absolute prefix is the\n // same on every row and pushes the useful columns off the terminal.\n const objects = store.durableObjects().map((object) => ({ class: object.className, game: object.name ?? \"(unnamed)\", doId: short(object.id, 12), file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file }));\n emit(objects, () => (objects.length === 0 ? \"no local Durable Objects: nothing has created a game session yet\" : table(objects)));\n return;\n }\n case \"players\": {\n const users = store.query(\"select id, username, display_name, is_anonymous, created_at from users order by created_at desc\");\n const bots = store.query(\"select id, username, display_name, type, schema_version from bots order by username\");\n emit({ users, bots }, () => `Users\\n${table(users)}\\n\\nBots\\n${table(bots)}`);\n return;\n }\n case \"tables\": {\n const rows = store.tables({ game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n case \"sql\": {\n const sql = rest.join(\" \");\n if (sql === \"\") throw new Error(\"sql needs a statement\");\n const rows = store.query(sql, { game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n default:\n throw new Error(`unknown command \"${command}\". Run eigen-inspect --help`);\n }\n } finally {\n store.close();\n }\n}\n\ntry {\n main();\n} catch (error) {\n console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"inspect-cli.js","names":[],"sources":["../src/inspect-cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.\n *\n * Deliberately narrow. Wrangler already ships a first-party browser for local\n * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and\n * generic table browsing is its job, not this tool's. What that view cannot do\n * is answer a question in the engine's own terms, because a game's truth is\n * split across two stores on purpose: the D1 index and the game's own Durable\n * Object. This reads both, decodes the JSON columns, prints the transition log\n * as a timeline, and says what the game is waiting for.\n *\n * It also runs with no dev server, no browser and no account, and emits\n * `--json` for a script or an agent.\n */\n\nimport { parseArgs } from \"node:util\";\nimport type { LocalGameRow, LocalGameView, LocalSeat } from \"./local-state.js\";\nimport { LocalStore } from \"./local-state.js\";\n\nconst HELP = `Usage: eigen-inspect <command> [options]\n\nRead the local .wrangler state of an EigenInteractive Worker: the D1 index and\neach game's own Durable Object database.\n\nCommands:\n games The D1 index, newest first\n game <id|code|prefix> One game across both stores, with its timeline\n do Every local Durable Object, mapped back to its game\n players Local users and bots, by id\n tables [--game <ref>] Table names and row counts\n sql <query> [--game <ref>]\n Run a read-only statement against D1 or one game's DO\n\nOptions:\n --dir <path> Start the search for .wrangler/state here (default: cwd)\n --status <s> games: filter by status\n --limit <n> games: row cap (default 20)\n --game <ref> tables/sql: target one game's Durable Object instead of D1\n --json Emit JSON instead of a table\n -h, --help Show this help\n\nEvery database is opened read-only, so this is safe to run while the Worker is\nrunning. Wrangler's own browser for the same files is \\`e\\` in \\`wrangler dev\\`.`;\n\nfunction relative(ms: number | null): string {\n if (ms === null) return \"never\";\n const delta = Date.now() - ms;\n const future = delta < 0;\n const seconds = Math.round(Math.abs(delta) / 1000);\n const [value, unit] = seconds < 60 ? [seconds, \"s\"] : seconds < 3600 ? [Math.round(seconds / 60), \"m\"] : seconds < 86400 ? [Math.round(seconds / 3600), \"h\"] : [Math.round(seconds / 86400), \"d\"];\n return future ? `in ${value}${unit}` : `${value}${unit} ago`;\n}\n\n/** Render rows as an aligned table. Values are stringified defensively because\n * SQLite hands back whatever the column holds, including blobs and nulls. */\nfunction table(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return \"(no rows)\";\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];\n const cell = (value: unknown): string => (value === null || value === undefined ? \"\" : typeof value === \"object\" ? JSON.stringify(value) : String(value));\n const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));\n const line = (values: string[]): string =>\n values\n .map((value, i) => value.padEnd(widths[i]))\n .join(\" \")\n .trimEnd();\n return [line(columns), line(widths.map((w) => \"-\".repeat(w))), ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))].join(\"\\n\");\n}\n\nfunction short(id: string | null, length = 8): string {\n return id === null ? \"\" : id.length <= length ? id : `${id.slice(0, length)}…`;\n}\n\nfunction seatLabel(seat: LocalSeat): string {\n const identity = seat.username ?? short(seat.userId ?? seat.botId);\n return `${seat.playerIndex}:${identity || \"(empty)\"}${seat.type === \"bot\" ? \" (bot)\" : \"\"}`;\n}\n\nfunction timingLabel(row: { turnSeconds: number | null; budgetSeconds: number | null; incrementSeconds: number | null }): string {\n if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;\n if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? \"\" : ` +${row.incrementSeconds}s`}`;\n return \"untimed\";\n}\n\nfunction gamesTable(rows: LocalGameRow[]): Record<string, unknown>[] {\n return rows.map((row) => ({\n id: short(row.id, 8),\n code: row.shortCode,\n status: row.status,\n access: row.access,\n seats: `${row.minPlayers}-${row.maxPlayers}`,\n rated: row.rated ? \"yes\" : \"no\",\n timing: timingLabel(row),\n pending: row.pendingPlayers === null ? \"\" : JSON.stringify(row.pendingPlayers),\n updated: relative(row.updatedAt),\n }));\n}\n\n/** The `game` report: the whole session in reading order, index first, then the\n * authoritative session, then the log, then the one sentence that explains it. */\nfunction renderGame(view: LocalGameView): string {\n const out: string[] = [];\n const index = view.index;\n out.push(`Game ${view.gameId}${index === null ? \"\" : ` ${index.shortCode}`}`);\n\n if (index === null) {\n out.push(\"\", \"D1 index (no games row)\");\n } else {\n out.push(\n \"\",\n `D1 index status=${index.status} access=${index.access} rated=${index.rated ? \"yes\" : \"no\"} schema=v${index.schemaVersion}`,\n ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`,\n `config ${JSON.stringify(index.config)}`,\n `participants ${view.indexSeats.length === 0 ? \"(none)\" : view.indexSeats.map(seatLabel).join(\" \")}`,\n );\n if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);\n }\n\n out.push(\"\");\n if (view.durableObject === null) {\n out.push(\"Durable Object (none: no command or socket has touched this game yet)\");\n } else {\n out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);\n const meta = view.meta;\n if (meta === null) {\n out.push(\"DO meta (no meta row: the object exists but never initialised)\");\n } else {\n out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? \"(unset, so no start has committed)\" : short(meta.rngSeed, 12)}`);\n }\n out.push(`roster ${view.roster.length === 0 ? \"(empty)\" : view.roster.map(seatLabel).join(\" \")}`);\n out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? \"none armed\" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);\n if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(\", \")}`);\n }\n\n out.push(\"\", \"Timeline\");\n if (view.transitions.length === 0) {\n out.push(\" (no transitions: the game has not started, so there is no v0)\");\n } else {\n for (const transition of view.transitions) {\n const action = transition.action;\n const cause = action === null ? \"start\" : `${action.kind}/${action.type}${action.playerIndex === null ? \"\" : ` seat ${action.playerIndex}`}`;\n const payload = action === null || action.data === null || action.data === undefined ? \"\" : ` ${JSON.stringify(action.data)}`;\n out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);\n out.push(` state ${JSON.stringify(transition.state)}`);\n out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? \"none\" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? \"(compacted)\" : JSON.stringify(transition.frameSeats)}`);\n }\n }\n\n out.push(\"\", `Diagnosis ${view.diagnosis.summary}`);\n for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);\n return out.join(\"\\n\");\n}\n\nfunction main(): void {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n dir: { type: \"string\" },\n status: { type: \"string\" },\n limit: { type: \"string\" },\n game: { type: \"string\" },\n json: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n\n const [command, ...rest] = positionals;\n if (values.help === true || command === undefined) {\n console.log(HELP);\n return;\n }\n\n const emit = (value: unknown, text: () => string): void => {\n console.log(values.json === true ? JSON.stringify(value, null, 2) : text());\n };\n\n const store = LocalStore.open(values.dir);\n try {\n switch (command) {\n case \"games\": {\n const rows = store.games({ status: values.status, limit: values.limit === undefined ? 20 : Number(values.limit) });\n emit(rows, () => (rows.length === 0 ? \"no games in the local D1 index\" : table(gamesTable(rows))));\n return;\n }\n case \"game\": {\n const ref = rest[0];\n if (ref === undefined) throw new Error(\"game needs an id, short code, or id prefix\");\n const view = store.game(ref);\n if (view === null) throw new Error(`no game matching \"${ref}\" in the local D1 index or in any local Durable Object`);\n emit(view, () => renderGame(view));\n return;\n }\n case \"do\": {\n // Paths print relative to the state root: the absolute prefix is the\n // same on every row and pushes the useful columns off the terminal.\n const objects = store.durableObjects().map((object) => ({ class: object.className, game: object.name ?? \"(unnamed)\", doId: short(object.id, 12), file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file }));\n emit(objects, () => (objects.length === 0 ? \"no local Durable Objects: nothing has created a game session yet\" : table(objects)));\n return;\n }\n case \"players\": {\n const users = store.query(\"select id, username, display_name, is_anonymous, created_at from users order by created_at desc\");\n const bots = store.query(\"select id, username, display_name, type, schema_version from bots order by username\");\n emit({ users, bots }, () => `Users\\n${table(users)}\\n\\nBots\\n${table(bots)}`);\n return;\n }\n case \"tables\": {\n const rows = store.tables({ game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n case \"sql\": {\n const sql = rest.join(\" \");\n if (sql === \"\") throw new Error(\"sql needs a statement\");\n const rows = store.query(sql, { game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n default:\n throw new Error(`unknown command \"${command}\". Run eigen-inspect --help`);\n }\n } finally {\n store.close();\n }\n}\n\ntry {\n main();\n} catch (error) {\n console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n}\n"],"mappings":"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"}