@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 irtio contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The deploy artifact builder: one ESM file from the room entry, import menu enforced by a
3
+ * resolver plugin, content hash over the output + canonical schema, verified by loading the
4
+ * bundle in a worker and checking the default export is a RoomDefinition.
5
+ */
6
+ /** Bare imports room code may use. Everything else (including node:*) is rejected. */
7
+ declare const ALLOWED_IMPORTS: string[];
8
+ interface BundleOptions {
9
+ /** The room entry file (must `export default defineRoom(...)`). */
10
+ readonly entry: string;
11
+ readonly outDir: string;
12
+ /**
13
+ * Where the allowed packages resolve from. Defaults to normal node resolution relative to the
14
+ * entry; the monorepo's tests point these at sibling `src/index.ts` files.
15
+ */
16
+ readonly irtioPackages?: Partial<Record<(typeof ALLOWED_IMPORTS)[number], string>>;
17
+ /** Skip the load-in-worker verification (used by watch rebuilds after the first success). */
18
+ readonly verify?: boolean;
19
+ }
20
+ interface BundleResult {
21
+ /** Absolute path of the bundled file (`room.<hash8>.mjs`). */
22
+ readonly file: string;
23
+ /** sha256(output + canonical schema), hex. Identifies the deployment version. */
24
+ readonly hash: string;
25
+ /** The builder schema hash from the definition (undefined when verify: false). */
26
+ readonly schemaHash?: string;
27
+ readonly mode?: 'tick' | 'event';
28
+ readonly warnings: readonly string[];
29
+ }
30
+ declare class BundleError extends Error {
31
+ readonly name = "BundleError";
32
+ }
33
+ /** Bundles the room entry; throws `BundleError` with the esbuild message on failure. */
34
+ declare function bundleRoom(options: BundleOptions): Promise<BundleResult>;
35
+
36
+ export { ALLOWED_IMPORTS, BundleError, type BundleOptions, type BundleResult, bundleRoom };
package/dist/bundle.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ ALLOWED_IMPORTS,
3
+ BundleError,
4
+ bundleRoom
5
+ } from "./chunk-ZWCLCYCS.js";
6
+ export {
7
+ ALLOWED_IMPORTS,
8
+ BundleError,
9
+ bundleRoom
10
+ };
@@ -0,0 +1,85 @@
1
+ import {
2
+ readCredential
3
+ } from "./chunk-UYN5PWLT.js";
4
+
5
+ // src/api-client.ts
6
+ var ApiClientError = class extends Error {
7
+ name = "ApiClientError";
8
+ status;
9
+ code;
10
+ changes;
11
+ hint;
12
+ constructor(status, body) {
13
+ super(body.message);
14
+ this.status = status;
15
+ this.code = body.code;
16
+ this.changes = body.changes;
17
+ this.hint = body.hint;
18
+ }
19
+ };
20
+ var NotLoggedInError = class extends Error {
21
+ constructor(controlUrl) {
22
+ super(`not logged in to ${controlUrl} \u2014 run: irtio login`);
23
+ this.controlUrl = controlUrl;
24
+ }
25
+ controlUrl;
26
+ name = "NotLoggedInError";
27
+ };
28
+ async function createApiClient(controlUrl) {
29
+ const credential = await readCredential(controlUrl);
30
+ if (!credential) throw new NotLoggedInError(controlUrl);
31
+ return createApiClientWithToken(controlUrl, credential.token);
32
+ }
33
+ function createApiClientWithToken(controlUrl, token) {
34
+ async function request(method, path, init = {}) {
35
+ const headers = {
36
+ Authorization: `Bearer ${token}`,
37
+ Accept: "application/json"
38
+ };
39
+ let requestBody;
40
+ if (init.bytes !== void 0) {
41
+ headers["content-type"] = "application/octet-stream";
42
+ requestBody = init.bytes;
43
+ } else if (init.body !== void 0) {
44
+ headers["content-type"] = "application/json";
45
+ requestBody = JSON.stringify(init.body);
46
+ }
47
+ const response = await fetch(`${controlUrl}${path}`, {
48
+ method,
49
+ headers,
50
+ ...requestBody !== void 0 ? { body: requestBody } : {}
51
+ });
52
+ const text = await response.text();
53
+ const parsed = text.length > 0 ? JSON.parse(text) : void 0;
54
+ if (!response.ok) {
55
+ const body = parsed !== void 0 && typeof parsed === "object" && parsed !== null && "code" in parsed && "message" in parsed ? parsed : {
56
+ code: "E_UNKNOWN",
57
+ message: text || `${method} ${path} failed with ${response.status}`
58
+ };
59
+ throw new ApiClientError(response.status, body);
60
+ }
61
+ return parsed;
62
+ }
63
+ return {
64
+ controlUrl,
65
+ get(path, query) {
66
+ const qs = query ? Object.entries(query).filter((e) => e[1] !== void 0).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : "";
67
+ return request("GET", qs ? `${path}?${qs}` : path);
68
+ },
69
+ post(path, body) {
70
+ return request("POST", path, { body });
71
+ },
72
+ postBytes(path, bytes) {
73
+ return request("POST", path, { bytes });
74
+ }
75
+ };
76
+ }
77
+ function isLoginRequired(err) {
78
+ return err instanceof NotLoggedInError || err instanceof ApiClientError && err.status === 401;
79
+ }
80
+
81
+ export {
82
+ ApiClientError,
83
+ createApiClient,
84
+ isLoginRequired
85
+ };
@@ -0,0 +1,59 @@
1
+ // src/credentials.ts
2
+ import { existsSync } from "fs";
3
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ function credentialsPath() {
7
+ if (process.env.IRT_CREDENTIALS_FILE) return process.env.IRT_CREDENTIALS_FILE;
8
+ if (process.platform === "win32") {
9
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
10
+ return path.join(appData, "irtio", "credentials.json");
11
+ }
12
+ return path.join(os.homedir(), ".config", "irtio", "credentials.json");
13
+ }
14
+ async function readCredentials() {
15
+ const file = credentialsPath();
16
+ if (!existsSync(file)) return {};
17
+ try {
18
+ const raw = await readFile(file, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
21
+ return parsed;
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ async function readCredential(controlUrl) {
27
+ const all = await readCredentials();
28
+ return all[controlUrl];
29
+ }
30
+ async function writeCredential(controlUrl, credential) {
31
+ const file = credentialsPath();
32
+ await mkdir(path.dirname(file), { recursive: true });
33
+ const all = await readCredentials();
34
+ all[controlUrl] = credential;
35
+ await writeFile(file, `${JSON.stringify(all, null, 2)}
36
+ `, { mode: 384 });
37
+ await chmod(file, 384).catch(() => {
38
+ });
39
+ return file;
40
+ }
41
+ var DEFAULT_CONTROL_URL = "https://control.irt.io";
42
+ function resolveControlUrl(flag) {
43
+ return flag ?? process.env.IRT_CONTROL_URL ?? DEFAULT_CONTROL_URL;
44
+ }
45
+ async function resolveControlUrlForUser(flag) {
46
+ const explicit = flag ?? process.env.IRT_CONTROL_URL;
47
+ if (explicit !== void 0) return explicit;
48
+ const all = await readCredentials();
49
+ const urls = Object.keys(all);
50
+ return urls.length === 1 ? urls[0] : DEFAULT_CONTROL_URL;
51
+ }
52
+
53
+ export {
54
+ credentialsPath,
55
+ readCredential,
56
+ writeCredential,
57
+ resolveControlUrl,
58
+ resolveControlUrlForUser
59
+ };
@@ -0,0 +1,146 @@
1
+ // src/bundle.ts
2
+ import { createHash } from "crypto";
3
+ import { mkdir, writeFile } from "fs/promises";
4
+ import * as path from "path";
5
+ import { pathToFileURL } from "url";
6
+ import { Worker } from "worker_threads";
7
+ import * as esbuild from "esbuild";
8
+ var ALLOWED_IMPORTS = ["@irtio/server", "@irtio/schema"];
9
+ var BundleError = class extends Error {
10
+ name = "BundleError";
11
+ };
12
+ function importMenuPlugin(allowed) {
13
+ return {
14
+ name: "irtio-import-menu",
15
+ setup(build2) {
16
+ build2.onResolve({ filter: /^[^./]/ }, (args) => {
17
+ if (args.kind === "entry-point" || path.isAbsolute(args.path)) return null;
18
+ const root = args.path.split("/")[0].startsWith("@") ? args.path.split("/").slice(0, 2).join("/") : args.path.split("/")[0];
19
+ if (allowed.has(root)) {
20
+ const redirect = allowed.get(root);
21
+ if (redirect) return { path: redirect };
22
+ return null;
23
+ }
24
+ return {
25
+ errors: [
26
+ {
27
+ text: `irtio: room code may import only ${ALLOWED_IMPORTS.join(", ")} and local files (got ${JSON.stringify(args.path)})` + (args.path.startsWith("node:") || isNodeBuiltin(root) ? " \u2014 Node built-ins are not available in the room sandbox" : "")
28
+ }
29
+ ]
30
+ };
31
+ });
32
+ }
33
+ };
34
+ }
35
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
36
+ "assert",
37
+ "buffer",
38
+ "child_process",
39
+ "crypto",
40
+ "events",
41
+ "fs",
42
+ "http",
43
+ "https",
44
+ "net",
45
+ "os",
46
+ "path",
47
+ "process",
48
+ "stream",
49
+ "timers",
50
+ "tls",
51
+ "url",
52
+ "util",
53
+ "worker_threads",
54
+ "zlib"
55
+ ]);
56
+ function isNodeBuiltin(name) {
57
+ return name.startsWith("node:") || NODE_BUILTINS.has(name);
58
+ }
59
+ async function bundleRoom(options) {
60
+ const allowed = new Map(
61
+ ALLOWED_IMPORTS.map((n) => [n, options.irtioPackages?.[n]])
62
+ );
63
+ let result;
64
+ try {
65
+ result = await esbuild.build({
66
+ entryPoints: [options.entry],
67
+ bundle: true,
68
+ format: "esm",
69
+ platform: "neutral",
70
+ target: "es2022",
71
+ write: false,
72
+ outdir: options.outDir,
73
+ plugins: [importMenuPlugin(allowed)],
74
+ logLevel: "silent",
75
+ mainFields: ["module", "main"],
76
+ conditions: ["import"]
77
+ });
78
+ } catch (err) {
79
+ const messages = err.errors?.map((e) => e.text).join("\n");
80
+ throw new BundleError(messages || String(err));
81
+ }
82
+ const out = result.outputFiles[0];
83
+ if (!out) throw new BundleError("esbuild produced no output");
84
+ const warnings = result.warnings.map((w) => w.text);
85
+ const hash = createHash("sha256").update(out.contents).digest("hex");
86
+ const file = path.join(options.outDir, `room.${hash.slice(0, 16)}.mjs`);
87
+ await mkdir(options.outDir, { recursive: true });
88
+ const banner = `// irtio room bundle ${hash}
89
+ `;
90
+ await writeFile(file, banner + out.text);
91
+ if (options.verify === false) return { file, hash, warnings };
92
+ const verified = await verifyBundle(file);
93
+ const fullHash = createHash("sha256").update(out.contents).update(verified.canonical).digest("hex");
94
+ return {
95
+ file,
96
+ hash: fullHash,
97
+ schemaHash: verified.schemaHash,
98
+ mode: verified.mode,
99
+ warnings
100
+ };
101
+ }
102
+ async function verifyBundle(file) {
103
+ const probe = `
104
+ import { parentPort } from 'node:worker_threads';
105
+ try {
106
+ const mod = await import(${JSON.stringify(pathToFileURL(file).href)});
107
+ const def = mod.default;
108
+ const ok = def && def.kind === 'irtio-room' && def.version === 1;
109
+ if (!ok) {
110
+ parentPort.postMessage({ ok: false, reason:
111
+ 'the bundle\\'s default export is not a room \u2014 end your room file with: export default defineRoom(schema, {...})' });
112
+ } else {
113
+ parentPort.postMessage({ ok: true, schemaHash: def.schema.hash, canonical: def.schema.canonical, mode: def.config.mode });
114
+ }
115
+ } catch (err) {
116
+ parentPort.postMessage({ ok: false, reason: String(err && err.stack || err) });
117
+ }
118
+ `;
119
+ const worker = new Worker(probe, { eval: true });
120
+ try {
121
+ const msg = await new Promise((resolve, reject) => {
122
+ const timer = setTimeout(
123
+ () => reject(new BundleError("bundle verification timed out")),
124
+ 1e4
125
+ );
126
+ worker.once("message", (m) => {
127
+ clearTimeout(timer);
128
+ resolve(m);
129
+ });
130
+ worker.once("error", (e) => {
131
+ clearTimeout(timer);
132
+ reject(e);
133
+ });
134
+ });
135
+ if (!msg.ok) throw new BundleError(`bundle failed to load: ${msg.reason}`);
136
+ return { schemaHash: msg.schemaHash, canonical: msg.canonical, mode: msg.mode };
137
+ } finally {
138
+ await worker.terminate();
139
+ }
140
+ }
141
+
142
+ export {
143
+ ALLOWED_IMPORTS,
144
+ BundleError,
145
+ bundleRoom
146
+ };
@@ -0,0 +1,308 @@
1
+ import {
2
+ bundleRoom
3
+ } from "./chunk-ZWCLCYCS.js";
4
+ import {
5
+ ApiClientError,
6
+ createApiClient,
7
+ isLoginRequired
8
+ } from "./chunk-J3G6AUJY.js";
9
+ import {
10
+ resolveControlUrlForUser
11
+ } from "./chunk-UYN5PWLT.js";
12
+
13
+ // src/deploy.ts
14
+ import { existsSync } from "fs";
15
+ import { mkdtemp, readFile, readdir, rm } from "fs/promises";
16
+ import { tmpdir } from "os";
17
+ import * as path from "path";
18
+ import { createInterface } from "readline/promises";
19
+ import { pathToFileURL } from "url";
20
+ import { diffSchemas, schemaFromCanonical } from "@irtio/schema";
21
+ import pc from "picocolors";
22
+ var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
23
+ var MIGRATIONS_DIR = "irtio/migrations";
24
+ function parseDeployArgs(args) {
25
+ const parsed = {
26
+ allowBreaking: false
27
+ };
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i];
30
+ switch (arg) {
31
+ case "--allow-breaking":
32
+ parsed.allowBreaking = true;
33
+ break;
34
+ case "--project": {
35
+ const value = args[++i];
36
+ if (value === void 0) throw new Error("irtio deploy: --project needs a value");
37
+ parsed.project = value;
38
+ break;
39
+ }
40
+ case "--url": {
41
+ const value = args[++i];
42
+ if (value === void 0) throw new Error("irtio deploy: --url needs a value");
43
+ parsed.url = value;
44
+ break;
45
+ }
46
+ case "--room": {
47
+ const value = args[++i];
48
+ if (value === void 0) throw new Error("irtio deploy: --room needs a value");
49
+ parsed.room = value;
50
+ break;
51
+ }
52
+ default:
53
+ throw new Error(`irtio deploy: unknown option ${JSON.stringify(arg)}`);
54
+ }
55
+ }
56
+ return parsed;
57
+ }
58
+ async function defaultAsk(question) {
59
+ if (!process.stdin.isTTY) return "";
60
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
61
+ try {
62
+ return await rl.question(question);
63
+ } finally {
64
+ rl.close();
65
+ }
66
+ }
67
+ function resolveEntry(cwd, room) {
68
+ if (room !== void 0) {
69
+ const file = path.resolve(cwd, room);
70
+ if (!existsSync(file)) throw new Error(`irtio deploy: no room file at ${file}`);
71
+ return file;
72
+ }
73
+ for (const candidate of ROOM_CANDIDATES) {
74
+ const file = path.resolve(cwd, candidate);
75
+ if (existsSync(file)) return file;
76
+ }
77
+ throw new Error(
78
+ `irtio deploy: no room file found in ${cwd}
79
+ looked for: ${ROOM_CANDIDATES.join(", ")}
80
+ pass --room <file> if it lives elsewhere`
81
+ );
82
+ }
83
+ async function readIrtioJsonProject(cwd) {
84
+ const file = path.join(cwd, "irtio.json");
85
+ if (!existsSync(file)) return void 0;
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(await readFile(file, "utf8"));
89
+ } catch (err) {
90
+ throw new Error(`irtio deploy: ${file} is not valid JSON: ${String(err)}`);
91
+ }
92
+ const project = parsed?.project;
93
+ if (project === void 0) return void 0;
94
+ if (typeof project !== "string" || project.length === 0) {
95
+ throw new Error(`irtio deploy: ${file} has a "project" that is not a non-empty string`);
96
+ }
97
+ return project;
98
+ }
99
+ async function loadBundledSchema(file) {
100
+ const mod = await import(pathToFileURL(file).href);
101
+ return {
102
+ project: mod.default.schema.project,
103
+ canonical: mod.default.schema.canonical,
104
+ schema: mod.default.schema
105
+ };
106
+ }
107
+ function printBreaking(log, breaking, hint) {
108
+ const n = breaking.length;
109
+ log(pc.red(`deploy refused: ${n} breaking change${n === 1 ? "" : "s"}`));
110
+ for (const change of breaking) log(` ${change.message}`);
111
+ log("");
112
+ log(pc.yellow(hint));
113
+ }
114
+ async function findMigrationFile(cwd, version) {
115
+ const dir = path.join(cwd, MIGRATIONS_DIR);
116
+ if (!existsSync(dir)) return void 0;
117
+ const entries = await readdir(dir);
118
+ const match = entries.find((f) => f.startsWith(`${version}_`) && f.endsWith(".ts"));
119
+ return match ? path.join(dir, match) : void 0;
120
+ }
121
+ async function runDeploy(options = {}) {
122
+ const cwd = path.resolve(options.cwd ?? process.cwd());
123
+ const log = options.log ?? ((line) => console.log(line));
124
+ const ask = options.ask ?? defaultAsk;
125
+ const controlUrl = await resolveControlUrlForUser(options.controlUrl);
126
+ const entry = resolveEntry(cwd, options.room);
127
+ const outDir = await mkdtemp(path.join(tmpdir(), "irtio-deploy-"));
128
+ let result;
129
+ try {
130
+ result = await bundleRoom({
131
+ entry,
132
+ outDir,
133
+ ...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
134
+ });
135
+ } catch (err) {
136
+ throw new Error(`irtio deploy: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+ log(pc.green(`bundled ${path.basename(result.file)} (${result.hash.slice(0, 12)})`));
139
+ const bundled = await loadBundledSchema(result.file);
140
+ const client = options.client ?? await createApiClient(controlUrl);
141
+ const projectId = options.project ?? await readIrtioJsonProject(cwd) ?? bundled.project;
142
+ if (projectId === void 0) {
143
+ throw new Error(
144
+ 'irtio deploy: no project id \u2014 pass --project, add "project" to irtio.json, or set it in defineSchema(...)'
145
+ );
146
+ }
147
+ let projectExists = true;
148
+ try {
149
+ await client.get(`/v1/projects/${projectId}`);
150
+ } catch (err) {
151
+ if (err instanceof ApiClientError && err.status === 404) projectExists = false;
152
+ else throw err;
153
+ }
154
+ if (!projectExists) {
155
+ const defaultName = path.basename(cwd);
156
+ const answer = (await ask(`project ${projectId} does not exist yet \u2014 name it [${defaultName}]: `)).trim();
157
+ const name = answer.length > 0 ? answer : defaultName;
158
+ await client.post("/v1/projects", { name, id: projectId });
159
+ log(pc.dim(`created project ${projectId} (${name})`));
160
+ }
161
+ const deployments = await client.get(
162
+ `/v1/projects/${projectId}/deployments`
163
+ );
164
+ const previous = deployments.reduce(
165
+ (best, d) => best === void 0 || d.version > best.version ? d : best,
166
+ void 0
167
+ );
168
+ let allowBreaking = options.allowBreaking ?? false;
169
+ let migrationFile;
170
+ const nextVersion = (previous?.version ?? 0) + 1;
171
+ if (previous !== void 0) {
172
+ const oldSchema = schemaFromCanonical(previous.schemaJson);
173
+ const changes = diffSchemas(oldSchema, bundled.schema);
174
+ const breaking = changes.filter((c) => c.kind === "breaking");
175
+ const additive = changes.filter((c) => c.kind === "additive");
176
+ if (breaking.length > 0) {
177
+ if (!allowBreaking) {
178
+ printBreaking(log, breaking, "run: irtio migrate create <name>");
179
+ throw new DeployRefusedError();
180
+ }
181
+ migrationFile = await findMigrationFile(cwd, nextVersion);
182
+ if (migrationFile === void 0) {
183
+ printBreaking(
184
+ log,
185
+ breaking,
186
+ `--allow-breaking needs a migration for v${nextVersion} \u2014 run: irtio migrate create <name>`
187
+ );
188
+ throw new DeployRefusedError();
189
+ }
190
+ log(pc.yellow(`${breaking.length} breaking change(s), proceeding with --allow-breaking:`));
191
+ for (const change of breaking) log(` ${change.message}`);
192
+ } else {
193
+ allowBreaking = false;
194
+ }
195
+ if (additive.length > 0) {
196
+ log(pc.cyan(`${additive.length} additive change(s):`));
197
+ for (const change of additive) log(` ${change.message}`);
198
+ }
199
+ if (changes.length === 0) log(pc.dim("no schema changes"));
200
+ } else {
201
+ log(pc.dim("no previous deployment \u2014 first deploy for this project"));
202
+ }
203
+ let migrationKey;
204
+ if (migrationFile !== void 0) {
205
+ const migrationBundle = await bundleRoom({
206
+ entry: migrationFile,
207
+ outDir,
208
+ verify: false,
209
+ ...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
210
+ });
211
+ const bytes = new Uint8Array(await readFile(migrationBundle.file));
212
+ const uploaded = await client.postBytes(
213
+ `/v1/projects/${projectId}/artifacts`,
214
+ bytes
215
+ );
216
+ migrationKey = uploaded.key;
217
+ log(pc.green(`bundled and uploaded migration ${path.basename(migrationFile)}`));
218
+ }
219
+ const roomBytes = new Uint8Array(await readFile(result.file));
220
+ const uploadedRoom = await client.postBytes(
221
+ `/v1/projects/${projectId}/artifacts`,
222
+ roomBytes
223
+ );
224
+ log(pc.green("uploaded bundle"));
225
+ let origin;
226
+ if (previous === void 0) {
227
+ const answer = (await ask("production origin (localhost is always allowed) \u2014 leave blank to skip: ")).trim();
228
+ if (answer.length > 0) origin = answer;
229
+ }
230
+ let response;
231
+ try {
232
+ const created = await client.post(`/v1/projects/${projectId}/deployments`, {
233
+ bundleHash: result.hash,
234
+ bundleKey: uploadedRoom.key,
235
+ schemaJson: bundled.canonical,
236
+ schemaHash: result.schemaHash,
237
+ ...migrationKey !== void 0 ? { migrationKey } : {},
238
+ ...allowBreaking ? { allowBreaking: true } : {},
239
+ ...origin !== void 0 ? { origin } : {}
240
+ });
241
+ response = {
242
+ project: projectId,
243
+ version: created.version,
244
+ url: created.url,
245
+ draining: created.draining
246
+ };
247
+ } catch (err) {
248
+ if (err instanceof ApiClientError && err.code === "E_BREAKING_SCHEMA" && err.changes) {
249
+ printBreaking(
250
+ log,
251
+ err.changes,
252
+ err.hint ?? "run: irtio migrate create <name>"
253
+ );
254
+ throw new DeployRefusedError();
255
+ }
256
+ throw err;
257
+ }
258
+ await rm(outDir, { recursive: true, force: true }).catch(() => {
259
+ });
260
+ log(pc.green(`deployed v${response.version}`));
261
+ log(pc.dim(`reachable at ${response.url}`));
262
+ if (previous !== void 0) {
263
+ log(pc.dim(`rooms already running finish on v${previous.version}`));
264
+ }
265
+ return response;
266
+ }
267
+ var DeployRefusedError = class extends Error {
268
+ name = "DeployRefusedError";
269
+ constructor() {
270
+ super("deploy refused");
271
+ }
272
+ };
273
+ async function deploy(args, deps = {}) {
274
+ const log = deps.log ?? ((line) => console.log(line));
275
+ const errorLog = deps.errorLog ?? ((line) => console.error(line));
276
+ try {
277
+ const parsed = parseDeployArgs(args);
278
+ await runDeploy({
279
+ allowBreaking: parsed.allowBreaking,
280
+ log,
281
+ ...parsed.project !== void 0 ? { project: parsed.project } : {},
282
+ ...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
283
+ ...parsed.room !== void 0 ? { room: parsed.room } : {},
284
+ ...deps.client !== void 0 ? { client: deps.client } : {},
285
+ ...deps.cwd !== void 0 ? { cwd: deps.cwd } : {},
286
+ ...deps.irtioPackages !== void 0 ? { irtioPackages: deps.irtioPackages } : {}
287
+ });
288
+ } catch (err) {
289
+ if (err instanceof DeployRefusedError) {
290
+ process.exitCode = 1;
291
+ return;
292
+ }
293
+ if (isLoginRequired(err)) {
294
+ errorLog(pc.red("not logged in"));
295
+ errorLog("run: irtio login");
296
+ process.exitCode = 1;
297
+ return;
298
+ }
299
+ errorLog(pc.red(err instanceof Error ? err.message : String(err)));
300
+ process.exitCode = 1;
301
+ }
302
+ }
303
+ export {
304
+ DeployRefusedError,
305
+ deploy,
306
+ parseDeployArgs,
307
+ runDeploy
308
+ };