@irtio/cli 0.5.0 → 0.5.2

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/dist/index.js CHANGED
@@ -8,8 +8,10 @@ usage: irtio <command>
8
8
 
9
9
  commands:
10
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)
11
+ [--name <name>] room.test.ts and irtio.json (--tick/--event
12
+ skip the one question it otherwise asks;
13
+ --name is the project name a first deploy
14
+ registers, default the directory)
13
15
  dev [--room <file>] [--port <n>] [--no-watch] bundle the room file and run it locally
14
16
  simulate [--bots <n>] [--seconds <n>] drive N real clients at a running room and
15
17
  [--room <code>] [--url <ws://\u2026>] check the built-in invariants
@@ -18,7 +20,9 @@ commands:
18
20
  [--misprediction-max <units>] fail the run if one correction snaps a
19
21
  [--snaps-max <n>] prediction further than this, if more than
20
22
  [--corrections-max <perSec>] <n> corrections fall outside the resim
21
- window, or above this correction rate/bot
23
+ [--overruns-max <n>] window, above this correction rate/bot, or
24
+ above this many server tick overruns.
25
+ exits 0 clean, 1 violation, 2 not performed
22
26
 
23
27
  login [--url <control>] sign in to a control plane via the browser
24
28
  whoami [--url <control>] print the identity the stored credential
@@ -26,11 +30,17 @@ commands:
26
30
  deploy [--allow-breaking] [--project <id>] bundle, classify the schema change against the
27
31
  [--url <control>] [--room <file>] last deployment, and upload \u2014 refuses breaking
28
32
  [--strategy drain|migrate] changes unless a migration covers them;
29
- migrate moves LIVE rooms onto the new version
30
- now (drain, the default, lets them finish)
31
- deploy --static <dir> [--project <id>] upload a BUILT client directory to the project's
32
- [--label <label>] [--url <control>] static site and print the playable link
33
- (<label>.irt-serve.com; no build step)
33
+ [--name <name>] [--no-static] migrate moves LIVE rooms onto the new version
34
+ now (drain, the default, lets them finish).
35
+ A project that does not exist yet is created
36
+ as --name, else irtio.json's "name".
37
+ With "static" in the project file the client
38
+ directory ships too; --no-static skips it
39
+ deploy --static [<dir>] [--project <id>] upload a BUILT client directory to the project's
40
+ [--label <label>] [--url <control>] static site and print the playable link, and
41
+ [--name <name>] nothing else (<label>.irt-serve.com; no build
42
+ step). Directory and label default to the
43
+ project file's "static"
34
44
  migrate create <name> scaffold irtio/migrations/<n>_<name>.ts with the
35
45
  live breaking changes quoted in its header
36
46
  rollback <version> [--project <id>] re-promote deployment <version> and bring rooms
@@ -50,6 +60,13 @@ commands:
50
60
  rooms saves <room> list a room's save generations, newest first
51
61
  rooms restore <room> --save <id> bring a room back from a save \u2014 DISCARDS its
52
62
  current state; stops the tenant if it is live
63
+
64
+ every command takes --help (or -h) and prints its own usage, which is the authority: the lines
65
+ above are a map, and the command's own help is generated next to the code that parses it.
66
+
67
+ every command that reads the project file takes -c/--config <file> to read a different one
68
+ (dev, deploy, deploy --static, migrate create, rollback, logs, rooms, keys). Default irtio.json;
69
+ the named file must exist. That is the test.irtio.json / prod.irtio.json pattern.
53
70
  `;
54
71
  switch (command) {
55
72
  case "init": {
@@ -58,7 +75,7 @@ switch (command) {
58
75
  break;
59
76
  }
60
77
  case "dev": {
61
- const { dev } = await import("./dev-7UZZGE4U.js");
78
+ const { dev } = await import("./dev-QJOGXLKM.js");
62
79
  await dev(args);
63
80
  break;
64
81
  }
@@ -68,47 +85,47 @@ switch (command) {
68
85
  break;
69
86
  }
70
87
  case "login": {
71
- const { login } = await import("./login-OV2EFNTJ.js");
88
+ const { login } = await import("./login-3EXB4CGX.js");
72
89
  await login(args);
73
90
  break;
74
91
  }
75
92
  case "whoami": {
76
- const { whoami } = await import("./whoami-S73O6KJF.js");
93
+ const { whoami } = await import("./whoami-CI5D5RCC.js");
77
94
  await whoami(args);
78
95
  break;
79
96
  }
80
97
  case "deploy": {
81
98
  if (args.includes("--static")) {
82
- const { staticDeploy } = await import("./static-deploy-LZXTDYNY.js");
99
+ const { staticDeploy } = await import("./static-deploy-5TBH4VNA.js");
83
100
  await staticDeploy(args);
84
101
  break;
85
102
  }
86
- const { deploy } = await import("./deploy-6OM3UYNL.js");
103
+ const { deploy } = await import("./deploy-3SABPL3T.js");
87
104
  await deploy(args);
88
105
  break;
89
106
  }
90
107
  case "migrate": {
91
- const { migrate } = await import("./migrate-3R6VRW4J.js");
108
+ const { migrate } = await import("./migrate-WUO2GBMX.js");
92
109
  await migrate(args);
93
110
  break;
94
111
  }
95
112
  case "rollback": {
96
- const { rollback } = await import("./rollback-CLQVYFHW.js");
113
+ const { rollback } = await import("./rollback-GA6UY772.js");
97
114
  await rollback(args);
98
115
  break;
99
116
  }
100
117
  case "keys": {
101
- const { keys } = await import("./keys-KEKO3EJ6.js");
118
+ const { keys } = await import("./keys-XBORZAPI.js");
102
119
  await keys(args);
103
120
  break;
104
121
  }
105
122
  case "logs": {
106
- const { logs } = await import("./logs-5OUDPAIX.js");
123
+ const { logs } = await import("./logs-2EOXLNWF.js");
107
124
  await logs(args);
108
125
  break;
109
126
  }
110
127
  case "rooms": {
111
- const { rooms } = await import("./rooms-OJ3JLYHD.js");
128
+ const { rooms } = await import("./rooms-B66LQIIF.js");
112
129
  await rooms(args);
113
130
  break;
114
131
  }
package/dist/init.d.ts CHANGED
@@ -21,6 +21,8 @@ interface InitArgs {
21
21
  readonly mode?: RoomMode;
22
22
  /** `--physics`: a Rapier room with a shared world-builder and client prediction (D22). */
23
23
  readonly physics?: boolean;
24
+ /** `--name`: the project's display name. Defaults to the directory name. */
25
+ readonly name?: string;
24
26
  }
25
27
  interface RunInitOptions {
26
28
  /** Where the project lives. Defaults to `process.cwd()`. */
@@ -28,6 +30,8 @@ interface RunInitOptions {
28
30
  readonly dir?: string;
29
31
  readonly mode?: RoomMode;
30
32
  readonly physics?: boolean;
33
+ /** The project's display name, written to `irtio.json`. Defaults to the directory name. */
34
+ readonly name?: string;
31
35
  /**
32
36
  * Asks the one question. Injected so tests never touch a TTY; the default reads stdin when it
33
37
  * is a TTY and otherwise takes the default answer rather than hanging in a pipeline.
@@ -41,6 +45,8 @@ interface InitResult {
41
45
  /** Absolute path of the scaffolded project. */
42
46
  readonly dir: string;
43
47
  readonly project: string;
48
+ /** The name written to `irtio.json`; what the first deploy registers the project as. */
49
+ readonly name: string;
44
50
  readonly mode: RoomMode;
45
51
  /** Files written, relative to `dir`, in emit order. */
46
52
  readonly created: readonly string[];
@@ -53,6 +59,7 @@ interface InitResult {
53
59
  * `irtio init [dir] [--tick|--event]`. Hand-rolled like `parseDevArgs`: the CLI has no
54
60
  * argument-parsing dependency and four flags do not earn one.
55
61
  */
62
+ declare const USAGE = "usage: irtio init [dir] [options]\n\nScaffolds irtio/schema.ts, irtio/rpc.ts, irtio/room.ts, irtio/room.test.ts and irtio.json.\nNothing that already exists is ever overwritten.\n\noptions:\n [dir] where to scaffold (default: here)\n --tick a tick room, without asking\n --event an event room, without asking\n --physics scaffold a physics room and its shared world builder\n --name <name> the project name a first deploy registers (default: the directory)\n -h, --help print this\n";
56
63
  declare function parseInitArgs(args: readonly string[]): InitArgs;
57
64
  /** `p_` + 16 hex characters: the project id *is* the public key, so it is not a secret. */
58
65
  declare function generateProjectId(): string;
@@ -64,4 +71,4 @@ declare function runInit(options?: RunInitOptions): Promise<InitResult>;
64
71
  /** Thin on purpose: parse, run, and turn a thrown message into an exit code. */
65
72
  declare function init(args: readonly string[]): Promise<void>;
66
73
 
67
- export { INIT_DEPENDENCIES, type InitArgs, type InitResult, type RoomMode, type RunInitOptions, generateProjectId, init, parseInitArgs, runInit };
74
+ export { INIT_DEPENDENCIES, type InitArgs, type InitResult, type RoomMode, type RunInitOptions, USAGE, generateProjectId, init, parseInitArgs, runInit };
package/dist/init.js CHANGED
@@ -1,3 +1,9 @@
1
+ import {
2
+ HelpRequested,
3
+ helpFor,
4
+ helpRequested
5
+ } from "./chunk-TV66QHFP.js";
6
+
1
7
  // src/init.ts
2
8
  import { randomBytes } from "crypto";
3
9
  import { existsSync } from "fs";
@@ -13,10 +19,33 @@ var INIT_DEPENDENCIES = [
13
19
  ];
14
20
  var DEFAULT_TICK_RATE = 20;
15
21
  var DEFAULT_IDLE_MS = 3e4;
22
+ var USAGE = `usage: irtio init [dir] [options]
23
+
24
+ Scaffolds irtio/schema.ts, irtio/rpc.ts, irtio/room.ts, irtio/room.test.ts and irtio.json.
25
+ Nothing that already exists is ever overwritten.
26
+
27
+ options:
28
+ [dir] where to scaffold (default: here)
29
+ --tick a tick room, without asking
30
+ --event an event room, without asking
31
+ --physics scaffold a physics room and its shared world builder
32
+ --name <name> the project name a first deploy registers (default: the directory)
33
+ -h, --help print this
34
+ `;
16
35
  function parseInitArgs(args) {
36
+ if (helpRequested(args)) throw helpFor(USAGE);
17
37
  const parsed = {};
18
- for (const arg of args) {
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
19
40
  switch (arg) {
41
+ case "--name": {
42
+ const value = args[++i];
43
+ if (value === void 0 || value === "") {
44
+ throw new Error("irtio init: --name needs a value");
45
+ }
46
+ parsed.name = value;
47
+ break;
48
+ }
20
49
  case "--physics":
21
50
  parsed.physics = true;
22
51
  break;
@@ -455,11 +484,13 @@ async function runInit(options = {}) {
455
484
  }
456
485
  const configFile = path.join(dir, "irtio.json");
457
486
  let project = options.projectId ?? generateProjectId();
487
+ let name = options.name ?? (path.basename(dir) || "project");
458
488
  if (existsSync(configFile)) {
459
489
  try {
460
490
  const existing = JSON.parse(await readFile(configFile, "utf8"));
461
491
  if (typeof existing.project === "string" && existing.project !== "")
462
492
  project = existing.project;
493
+ if (typeof existing.name === "string" && existing.name !== "") name = existing.name;
463
494
  } catch {
464
495
  }
465
496
  }
@@ -469,14 +500,14 @@ async function runInit(options = {}) {
469
500
  ["irtio/world.ts", WORLD_TEMPLATE],
470
501
  ["irtio/room.ts", physicsRoomTemplate()],
471
502
  ["irtio/room.test.ts", PHYSICS_ROOM_TEST_TEMPLATE],
472
- ["irtio.json", `${JSON.stringify({ project }, null, 2)}
503
+ ["irtio.json", `${JSON.stringify({ project, name }, null, 2)}
473
504
  `]
474
505
  ] : [
475
506
  ["irtio/schema.ts", schemaTemplate(project)],
476
507
  ["irtio/rpc.ts", RPC_TEMPLATE],
477
508
  ["irtio/room.ts", roomTemplate(mode)],
478
509
  ["irtio/room.test.ts", ROOM_TEST_TEMPLATE],
479
- ["irtio.json", `${JSON.stringify({ project }, null, 2)}
510
+ ["irtio.json", `${JSON.stringify({ project, name }, null, 2)}
480
511
  `]
481
512
  ];
482
513
  const created = [];
@@ -498,7 +529,7 @@ async function runInit(options = {}) {
498
529
  if (skipped.length > 0) {
499
530
  log(pc.yellow(`kept ${skipped.join(", ")} (already there \u2014 nothing was overwritten)`));
500
531
  }
501
- log(pc.dim(`project ${project} \xB7 ${mode} mode${physics ? " \xB7 physics" : ""}`));
532
+ log(pc.dim(`project ${project} (${name}) \xB7 ${mode} mode${physics ? " \xB7 physics" : ""}`));
502
533
  const installCommand = packageJson === "absent" ? `npm i ${wantedDeps.join(" ")}` : "npm install # the irtio packages were added to your package.json";
503
534
  for (const line of nextSteps(installCommand)) log(line);
504
535
  if (physics) {
@@ -511,7 +542,7 @@ async function runInit(options = {}) {
511
542
  log(" draw from room.render (predicted + interpolated), write intents via room.state.");
512
543
  log("");
513
544
  }
514
- return { dir, project, mode, created, skipped, packageJson };
545
+ return { dir, project, name, mode, created, skipped, packageJson };
515
546
  }
516
547
  async function init(args) {
517
548
  try {
@@ -519,15 +550,21 @@ async function init(args) {
519
550
  await runInit({
520
551
  ...parsed.dir !== void 0 ? { dir: parsed.dir } : {},
521
552
  ...parsed.mode !== void 0 ? { mode: parsed.mode } : {},
522
- ...parsed.physics !== void 0 ? { physics: parsed.physics } : {}
553
+ ...parsed.physics !== void 0 ? { physics: parsed.physics } : {},
554
+ ...parsed.name !== void 0 ? { name: parsed.name } : {}
523
555
  });
524
556
  } catch (err) {
557
+ if (err instanceof HelpRequested) {
558
+ console.log(err.usage);
559
+ return;
560
+ }
525
561
  console.error(pc.red(err instanceof Error ? err.message : String(err)));
526
562
  process.exitCode = 1;
527
563
  }
528
564
  }
529
565
  export {
530
566
  INIT_DEPENDENCIES,
567
+ USAGE,
531
568
  generateProjectId,
532
569
  init,
533
570
  parseInitArgs,
@@ -1,14 +1,16 @@
1
1
  import {
2
- createApiClient,
3
- isLoginRequired
4
- } from "./chunk-I37DLT7K.js";
2
+ readProjectConfig
3
+ } from "./chunk-32QTPKVT.js";
5
4
  import {
5
+ createApiClient,
6
+ helpRequested,
7
+ isLoginRequired,
6
8
  resolveControlUrlForUser
7
- } from "./chunk-NVUKSP5U.js";
9
+ } from "./chunk-TV66QHFP.js";
8
10
 
9
11
  // src/keys.ts
10
- import { existsSync } from "fs";
11
- import { readFile } from "fs/promises";
12
+ import "fs";
13
+ import "fs/promises";
12
14
  import * as path from "path";
13
15
  import pc from "picocolors";
14
16
 
@@ -22,17 +24,6 @@ function signJwt(secret, claims) {
22
24
  }
23
25
 
24
26
  // src/keys.ts
25
- async function readIrtioJsonProject(cwd) {
26
- const file = path.join(cwd, "irtio.json");
27
- if (!existsSync(file)) return void 0;
28
- let parsed;
29
- try {
30
- parsed = JSON.parse(await readFile(file, "utf8"));
31
- } catch {
32
- throw new Error(`irtio keys: ${file} is not valid JSON`);
33
- }
34
- return typeof parsed.project === "string" && parsed.project.length > 0 ? parsed.project : void 0;
35
- }
36
27
  function flagValue(args, flag) {
37
28
  const i = args.indexOf(flag);
38
29
  if (i === -1) return void 0;
@@ -40,11 +31,15 @@ function flagValue(args, flag) {
40
31
  if (value === void 0) throw new Error(`irtio keys: ${flag} needs a value`);
41
32
  return value;
42
33
  }
34
+ function configFlag(args) {
35
+ return flagValue(args, "--config") ?? flagValue(args, "-c");
36
+ }
43
37
  async function resolveProject(args) {
44
- const project = flagValue(args, "--project") ?? await readIrtioJsonProject(process.cwd());
38
+ const config = await readProjectConfig(process.cwd(), "irtio keys", configFlag(args));
39
+ const project = flagValue(args, "--project") ?? config.project;
45
40
  if (project === void 0) {
46
41
  throw new Error(
47
- "irtio keys: no project id \u2014 pass --project or run this from a project with irtio.json"
42
+ `irtio keys: no project id \u2014 pass --project or run this from a project with ${path.basename(config.file)}`
48
43
  );
49
44
  }
50
45
  return project;
@@ -88,9 +83,40 @@ async function runJwtSecretRetire(options) {
88
83
  log(pc.green(`retired the oldest active secret; ${result.active} remain(s) active`));
89
84
  log(pc.dim("tokens signed with the retired secret stop verifying at the next placement"));
90
85
  }
86
+ var USAGE = `usage: irtio keys jwt-secret [retire] [options]
87
+ irtio keys jwt-mint --secret <s> --sub <playerId> [options]
88
+
89
+ jwt-secret mints a signing secret for the project. It is printed ONCE and never again. Running it
90
+ a second time starts a rotation, where both secrets verify; retire finishes one by dropping the
91
+ oldest.
92
+
93
+ jwt-mint signs a test token locally, with the documented recipe. The secret you pass never leaves
94
+ this machine.
95
+
96
+ jwt-secret options:
97
+ retire retire the oldest active secret for this issuer
98
+ --issuer <label> the issuer to mint or retire under
99
+ --project <id> project id (default: the project file)
100
+ -c, --config <f> the project file to read (default irtio.json)
101
+ --url <control> control plane (default: your stored login)
102
+
103
+ jwt-mint options:
104
+ --secret <s> the signing secret (required)
105
+ --sub <playerId> the player the token is for (required)
106
+ --iss <label> the issuer claim
107
+ --room <id> restrict the token to one room
108
+ --role <r> the role to join as
109
+ --ttl <sec> lifetime in seconds (default 3600)
110
+
111
+ -h, --help print this
112
+ `;
91
113
  async function keys(args, deps = {}) {
92
114
  const log = deps.log ?? ((line) => console.log(line));
93
115
  const errorLog = deps.errorLog ?? ((line) => console.error(line));
116
+ if (helpRequested(args)) {
117
+ log(USAGE);
118
+ return;
119
+ }
94
120
  try {
95
121
  const sub = args[0];
96
122
  const rest = args.slice(1);
@@ -133,7 +159,7 @@ async function keys(args, deps = {}) {
133
159
  const room = flagValue(rest, "--room");
134
160
  const role = flagValue(rest, "--role");
135
161
  const iss = flagValue(rest, "--iss") ?? DEFAULT_ISSUER;
136
- const project = flagValue(rest, "--project") ?? await readIrtioJsonProject(process.cwd());
162
+ const project = flagValue(rest, "--project") ?? (await readProjectConfig(process.cwd(), "irtio keys", configFlag(rest))).project;
137
163
  if (project === void 0) {
138
164
  throw new Error(
139
165
  "irtio keys jwt-mint: --project is required (aud must name the project the token is for)"
@@ -168,6 +194,7 @@ async function keys(args, deps = {}) {
168
194
  }
169
195
  export {
170
196
  DEFAULT_ISSUER,
197
+ USAGE,
171
198
  keys,
172
199
  runJwtSecret,
173
200
  runJwtSecretRetire
@@ -1,8 +1,11 @@
1
1
  import {
2
+ HelpRequested,
2
3
  credentialsPath,
4
+ helpFor,
5
+ helpRequested,
3
6
  resolveControlUrl,
4
7
  writeCredential
5
- } from "./chunk-NVUKSP5U.js";
8
+ } from "./chunk-TV66QHFP.js";
6
9
 
7
10
  // src/login.ts
8
11
  import { spawn } from "child_process";
@@ -10,7 +13,16 @@ import { randomBytes } from "crypto";
10
13
  import { createServer } from "http";
11
14
  import pc from "picocolors";
12
15
  var LOGIN_TIMEOUT_MS = 3 * 60 * 1e3;
16
+ var USAGE = `usage: irtio login [--url <control>]
17
+
18
+ Signs in to a control plane through your browser and stores the credential it hands back.
19
+
20
+ options:
21
+ --url <control> the control plane to sign in to (default: the last one you used)
22
+ -h, --help print this
23
+ `;
13
24
  function parseLoginArgs(args) {
25
+ if (helpRequested(args)) throw helpFor(USAGE);
14
26
  const parsed = {};
15
27
  for (let i = 0; i < args.length; i++) {
16
28
  const arg = args[i];
@@ -145,11 +157,16 @@ async function login(args) {
145
157
  console.log(pc.dim(`control plane: ${result.controlUrl}`));
146
158
  console.log(pc.dim(`credentials stored at ${result.file ?? credentialsPath()}`));
147
159
  } catch (err) {
160
+ if (err instanceof HelpRequested) {
161
+ console.log(err.usage);
162
+ return;
163
+ }
148
164
  console.error(pc.red(err instanceof Error ? err.message : String(err)));
149
165
  process.exitCode = 1;
150
166
  }
151
167
  }
152
168
  export {
169
+ USAGE,
153
170
  defaultOpenBrowser,
154
171
  login,
155
172
  parseLoginArgs,
@@ -1,18 +1,36 @@
1
1
  import {
2
- createApiClient,
3
- isLoginRequired
4
- } from "./chunk-I37DLT7K.js";
2
+ readProjectConfig
3
+ } from "./chunk-32QTPKVT.js";
5
4
  import {
5
+ HelpRequested,
6
+ createApiClient,
7
+ helpFor,
8
+ helpRequested,
9
+ isLoginRequired,
6
10
  resolveControlUrlForUser
7
- } from "./chunk-NVUKSP5U.js";
11
+ } from "./chunk-TV66QHFP.js";
8
12
 
9
13
  // src/logs.ts
10
- import { existsSync } from "fs";
11
- import { readFile } from "fs/promises";
12
- import * as path from "path";
14
+ import "fs";
15
+ import "fs/promises";
16
+ import "path";
13
17
  import pc from "picocolors";
14
18
  var POLL_MS = 2e3;
19
+ var USAGE = `usage: irtio logs [options]
20
+
21
+ Tails a project's logs, oldest line first.
22
+
23
+ options:
24
+ --follow poll every 2s, threading the cursor so nothing repeats
25
+ --since <cursor> start after this cursor (a previous row's timestamp)
26
+ --room <id> narrow to one room's stream. __tenant is the tenant's own events
27
+ --project <id> project id (default: the project file)
28
+ -c, --config <f> the project file to read (default irtio.json)
29
+ --url <control> control plane (default: your stored login)
30
+ -h, --help print this
31
+ `;
15
32
  function parseLogsArgs(args) {
33
+ if (helpRequested(args)) throw helpFor(USAGE);
16
34
  const parsed = {
17
35
  follow: false
18
36
  };
@@ -40,6 +58,14 @@ function parseLogsArgs(args) {
40
58
  parsed.project = value;
41
59
  break;
42
60
  }
61
+ case "-c":
62
+ case "--config": {
63
+ const value = args[++i];
64
+ if (value === void 0 || value === "")
65
+ throw new Error("irtio logs: --config needs a value");
66
+ parsed.config = value;
67
+ break;
68
+ }
43
69
  case "--url": {
44
70
  const value = args[++i];
45
71
  if (value === void 0) throw new Error("irtio logs: --url needs a value");
@@ -81,23 +107,13 @@ async function runLogs(options) {
81
107
  await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? POLL_MS));
82
108
  }
83
109
  }
84
- async function readIrtioJsonProject(cwd) {
85
- const file = path.join(cwd, "irtio.json");
86
- if (!existsSync(file)) return void 0;
87
- let parsed;
88
- try {
89
- parsed = JSON.parse(await readFile(file, "utf8"));
90
- } catch {
91
- throw new Error(`irtio logs: ${file} is not valid JSON`);
92
- }
93
- return typeof parsed.project === "string" && parsed.project.length > 0 ? parsed.project : void 0;
94
- }
95
110
  async function logs(args, deps = {}) {
96
111
  const log = deps.log ?? ((line) => console.log(line));
97
112
  const errorLog = deps.errorLog ?? ((line) => console.error(line));
98
113
  try {
99
114
  const parsed = parseLogsArgs(args);
100
- const project = parsed.project ?? await readIrtioJsonProject(process.cwd());
115
+ const config = await readProjectConfig(process.cwd(), "irtio logs", parsed.config);
116
+ const project = parsed.project ?? config.project;
101
117
  if (project === void 0) {
102
118
  throw new Error(
103
119
  "irtio logs: no project id \u2014 pass --project or run this from a project with irtio.json"
@@ -113,6 +129,10 @@ async function logs(args, deps = {}) {
113
129
  ...deps.client !== void 0 ? { client: deps.client } : {}
114
130
  });
115
131
  } catch (err) {
132
+ if (err instanceof HelpRequested) {
133
+ log(err.usage);
134
+ return;
135
+ }
116
136
  if (isLoginRequired(err)) {
117
137
  errorLog(pc.red("not logged in"));
118
138
  errorLog("run: irtio login");
@@ -124,6 +144,7 @@ async function logs(args, deps = {}) {
124
144
  }
125
145
  }
126
146
  export {
147
+ USAGE,
127
148
  logs,
128
149
  parseLogsArgs,
129
150
  runLogs
@@ -2,16 +2,20 @@ import {
2
2
  bundleRoom
3
3
  } from "./chunk-KRQUAEN2.js";
4
4
  import {
5
- createApiClient,
6
- isLoginRequired
7
- } from "./chunk-I37DLT7K.js";
5
+ readProjectConfig
6
+ } from "./chunk-32QTPKVT.js";
8
7
  import {
8
+ HelpRequested,
9
+ createApiClient,
10
+ helpFor,
11
+ helpRequested,
12
+ isLoginRequired,
9
13
  resolveControlUrlForUser
10
- } from "./chunk-NVUKSP5U.js";
14
+ } from "./chunk-TV66QHFP.js";
11
15
 
12
16
  // src/migrate.ts
13
17
  import { existsSync } from "fs";
14
- import { mkdir, mkdtemp, readFile, readdir, writeFile } from "fs/promises";
18
+ import { mkdir, mkdtemp, readdir, writeFile } from "fs/promises";
15
19
  import { tmpdir } from "os";
16
20
  import * as path from "path";
17
21
  import { pathToFileURL } from "url";
@@ -23,7 +27,18 @@ import {
23
27
  import pc from "picocolors";
24
28
  var MIGRATIONS_DIR = "irtio/migrations";
25
29
  var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
30
+ var USAGE = `usage: irtio migrate create <name> [-c <file>]
31
+
32
+ Scaffolds irtio/migrations/<n>_<name>.ts, with the breaking changes against the live deployment
33
+ quoted in its header so the transform has something to answer.
34
+
35
+ options:
36
+ <name> a short name for the migration, e.g. rename-hp
37
+ -c, --config <f> the project file to read (default irtio.json)
38
+ -h, --help print this
39
+ `;
26
40
  function parseMigrateArgs(args) {
41
+ if (helpRequested(args)) throw helpFor(USAGE);
27
42
  const [sub, name, ...rest] = args;
28
43
  if (sub !== "create") {
29
44
  throw new Error(
@@ -35,9 +50,20 @@ function parseMigrateArgs(args) {
35
50
  "irtio migrate create: a name is required, e.g. irtio migrate create rename-hp"
36
51
  );
37
52
  }
38
- if (rest.length > 0)
39
- throw new Error(`irtio migrate create: unexpected extra argument ${JSON.stringify(rest[0])}`);
40
- return { command: "create", args: { name } };
53
+ let config;
54
+ for (let i = 0; i < rest.length; i++) {
55
+ const arg = rest[i];
56
+ if (arg === "-c" || arg === "--config") {
57
+ const value = rest[++i];
58
+ if (value === void 0 || value === "") {
59
+ throw new Error("irtio migrate create: --config needs a value");
60
+ }
61
+ config = value;
62
+ continue;
63
+ }
64
+ throw new Error(`irtio migrate create: unexpected extra argument ${JSON.stringify(arg)}`);
65
+ }
66
+ return { command: "create", args: config === void 0 ? { name } : { name, config } };
41
67
  }
42
68
  function resolveEntry(cwd, room) {
43
69
  if (room !== void 0) {
@@ -53,12 +79,6 @@ function resolveEntry(cwd, room) {
53
79
  `irtio migrate create: no room file found in ${cwd} (looked for: ${ROOM_CANDIDATES.join(", ")})`
54
80
  );
55
81
  }
56
- async function readIrtioJsonProject(cwd) {
57
- const file = path.join(cwd, "irtio.json");
58
- if (!existsSync(file)) return void 0;
59
- const parsed = JSON.parse(await readFile(file, "utf8"));
60
- return typeof parsed.project === "string" && parsed.project.length > 0 ? parsed.project : void 0;
61
- }
62
82
  async function scanDirectoryVersion(cwd) {
63
83
  const dir = path.join(cwd, MIGRATIONS_DIR);
64
84
  if (!existsSync(dir)) return 1;
@@ -115,7 +135,8 @@ async function runMigrateCreate(options) {
115
135
  const cwd = path.resolve(options.cwd ?? process.cwd());
116
136
  const log = options.log ?? ((line) => console.log(line));
117
137
  const controlUrl = await resolveControlUrlForUser(options.controlUrl);
118
- const projectId = options.project ?? await readIrtioJsonProject(cwd);
138
+ const config = await readProjectConfig(cwd, "irtio migrate", options.config);
139
+ const projectId = options.project ?? config.project;
119
140
  let version;
120
141
  let versionSource;
121
142
  let previousSchemaJson;
@@ -191,6 +212,10 @@ async function migrate(args, deps = {}) {
191
212
  ...deps.client !== void 0 ? { client: deps.client } : {}
192
213
  });
193
214
  } catch (err) {
215
+ if (err instanceof HelpRequested) {
216
+ log(err.usage);
217
+ return;
218
+ }
194
219
  if (isLoginRequired(err)) {
195
220
  errorLog(pc.red("not logged in"));
196
221
  errorLog("run: irtio login");
@@ -202,6 +227,7 @@ async function migrate(args, deps = {}) {
202
227
  }
203
228
  }
204
229
  export {
230
+ USAGE,
205
231
  migrate,
206
232
  parseMigrateArgs,
207
233
  runMigrateCreate