@irtio/cli 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.
- package/dist/api.d.ts +52 -0
- package/dist/api.js +15 -0
- package/dist/bundle.js +1 -1
- package/dist/{static-deploy-BP3MDXCP.js → chunk-3HQMVCYA.js} +89 -48
- package/dist/chunk-DKWG7MGO.js +93 -0
- package/dist/{chunk-KRQUAEN2.js → chunk-OTSFRVJN.js} +12 -6
- package/dist/{chunk-I37DLT7K.js → chunk-RNAH5T4W.js} +14 -3
- package/dist/chunk-RQSJZWQC.js +452 -0
- package/dist/{chunk-NVUKSP5U.js → chunk-UPHQM6NZ.js} +12 -1
- package/dist/chunk-ZD4ND6X6.js +31 -0
- package/dist/chunk-ZK5JLUD4.js +94 -0
- package/dist/credentials.d.ts +61 -0
- package/dist/credentials.js +20 -0
- package/dist/delete-project-VENS2B44.js +118 -0
- package/dist/deploy.d.ts +149 -0
- package/dist/deploy.js +567 -0
- package/dist/{dev-7UZZGE4U.js → dev-QM26ONKS.js} +3095 -294
- package/dist/index.js +113 -28
- package/dist/init.d.ts +2 -1
- package/dist/init.js +42 -1
- package/dist/{keys-KEKO3EJ6.js → keys-JHLMEGRA.js} +49 -18
- package/dist/leaderboard-SYPSBPS3.js +352 -0
- package/dist/{login-OV2EFNTJ.js → login-2M73HBZT.js} +25 -1
- package/dist/{logs-5OUDPAIX.js → logs-2W7CPZO5.js} +42 -17
- package/dist/{migrate-UD245ULI.js → migrate-T3DZJREY.js} +44 -19
- package/dist/ratings-VG32WFDG.js +297 -0
- package/dist/{rollback-CLQVYFHW.js → rollback-SO74MVZV.js} +41 -17
- package/dist/{rooms-OJ3JLYHD.js → rooms-VI33P4RA.js} +73 -20
- package/dist/simulate.d.ts +215 -3
- package/dist/simulate.js +865 -64
- package/dist/static-deploy-KOWFKWZA.js +19 -0
- package/dist/status-HF3ZEKB7.js +219 -0
- package/dist/usage-4G23QXCH.js +213 -0
- package/dist/{whoami-S73O6KJF.js → whoami-KTMTQNHM.js} +21 -2
- package/package.json +24 -7
- package/dist/chunk-BPE452KF.js +0 -180
- package/dist/chunk-D7CDJRFF.js +0 -24
- package/dist/deploy-YVCVDVMS.js +0 -396
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logEnsured,
|
|
3
|
+
runStaticDeploy
|
|
4
|
+
} from "./chunk-3HQMVCYA.js";
|
|
5
|
+
import {
|
|
6
|
+
clientImportsRoom
|
|
7
|
+
} from "./chunk-TQU6345E.js";
|
|
8
|
+
import "./chunk-RQSJZWQC.js";
|
|
9
|
+
import {
|
|
10
|
+
ensureProject,
|
|
11
|
+
readProjectConfig
|
|
12
|
+
} from "./chunk-DKWG7MGO.js";
|
|
13
|
+
import {
|
|
14
|
+
bundleRoom
|
|
15
|
+
} from "./chunk-OTSFRVJN.js";
|
|
16
|
+
import {
|
|
17
|
+
HelpRequested,
|
|
18
|
+
helpFor,
|
|
19
|
+
helpRequested
|
|
20
|
+
} from "./chunk-ZD4ND6X6.js";
|
|
21
|
+
import {
|
|
22
|
+
ApiClientError,
|
|
23
|
+
createApiClient,
|
|
24
|
+
isLoginRequired
|
|
25
|
+
} from "./chunk-RNAH5T4W.js";
|
|
26
|
+
import {
|
|
27
|
+
resolveControlUrlForUser
|
|
28
|
+
} from "./chunk-UPHQM6NZ.js";
|
|
29
|
+
|
|
30
|
+
// src/deploy.ts
|
|
31
|
+
import { existsSync, readdirSync } from "fs";
|
|
32
|
+
import { mkdir, readFile, readdir, rm } from "fs/promises";
|
|
33
|
+
import * as path from "path";
|
|
34
|
+
import { createInterface } from "readline/promises";
|
|
35
|
+
import { pathToFileURL } from "url";
|
|
36
|
+
import { DEFAULT_ROOM_TYPE, ROOM_TYPE_RE } from "@irtio/protocol";
|
|
37
|
+
import { diffSchemas, schemaFromCanonical } from "@irtio/schema";
|
|
38
|
+
import pc from "picocolors";
|
|
39
|
+
var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
|
|
40
|
+
var MIGRATIONS_DIR = "irtio/migrations";
|
|
41
|
+
var ROOM_TYPES_DIR = "irtio/rooms";
|
|
42
|
+
var USAGE = `usage: irtio deploy [options]
|
|
43
|
+
irtio deploy --static [<dir>] [options]
|
|
44
|
+
|
|
45
|
+
Bundles the room, classifies the schema change against the last deployment, and uploads. A
|
|
46
|
+
breaking change is refused unless a migration covers it. With "static" in the project file the
|
|
47
|
+
client directory ships too, after the room.
|
|
48
|
+
|
|
49
|
+
options:
|
|
50
|
+
--allow-breaking accept a breaking schema change that a migration covers
|
|
51
|
+
--strategy drain|migrate drain (the default) lets running rooms finish on the old version;
|
|
52
|
+
migrate moves live rooms onto the new one now
|
|
53
|
+
--room <file> the room entry
|
|
54
|
+
--project <id> project id (default: the project file)
|
|
55
|
+
--name <name> the name a first deploy registers the project under
|
|
56
|
+
--no-static deploy the room alone, even with "static" configured
|
|
57
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
58
|
+
--url <control> control plane (default: your stored login)
|
|
59
|
+
-h, --help print this
|
|
60
|
+
|
|
61
|
+
irtio deploy --static uploads a BUILT client directory and nothing else. It takes --label <label>
|
|
62
|
+
plus --project, --name, --config and --url; the directory and label default to the project file.
|
|
63
|
+
`;
|
|
64
|
+
function parseDeployArgs(args) {
|
|
65
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
66
|
+
const parsed = {
|
|
67
|
+
allowBreaking: false
|
|
68
|
+
};
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
switch (arg) {
|
|
72
|
+
case "--allow-breaking":
|
|
73
|
+
parsed.allowBreaking = true;
|
|
74
|
+
break;
|
|
75
|
+
case "--no-static":
|
|
76
|
+
parsed.noStatic = true;
|
|
77
|
+
break;
|
|
78
|
+
case "--project": {
|
|
79
|
+
const value = args[++i];
|
|
80
|
+
if (value === void 0) throw new Error("irtio deploy: --project needs a value");
|
|
81
|
+
parsed.project = value;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "--name": {
|
|
85
|
+
const value = args[++i];
|
|
86
|
+
if (value === void 0 || value === "") {
|
|
87
|
+
throw new Error("irtio deploy: --name needs a value");
|
|
88
|
+
}
|
|
89
|
+
parsed.name = value;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "-c":
|
|
93
|
+
case "--config": {
|
|
94
|
+
const value = args[++i];
|
|
95
|
+
if (value === void 0 || value === "") {
|
|
96
|
+
throw new Error("irtio deploy: --config needs a value");
|
|
97
|
+
}
|
|
98
|
+
parsed.config = value;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "--url": {
|
|
102
|
+
const value = args[++i];
|
|
103
|
+
if (value === void 0) throw new Error("irtio deploy: --url needs a value");
|
|
104
|
+
parsed.url = value;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "--room": {
|
|
108
|
+
const value = args[++i];
|
|
109
|
+
if (value === void 0) throw new Error("irtio deploy: --room needs a value");
|
|
110
|
+
parsed.room = value;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "--strategy": {
|
|
114
|
+
const value = args[++i];
|
|
115
|
+
if (value !== "drain" && value !== "migrate") {
|
|
116
|
+
throw new Error('irtio deploy: --strategy must be "drain" or "migrate"');
|
|
117
|
+
}
|
|
118
|
+
parsed.strategy = value;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
throw new Error(`irtio deploy: unknown option ${JSON.stringify(arg)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
async function defaultAsk(question) {
|
|
128
|
+
if (!process.stdin.isTTY) return "";
|
|
129
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
130
|
+
try {
|
|
131
|
+
return await rl.question(question);
|
|
132
|
+
} finally {
|
|
133
|
+
rl.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function findRoomEntry(cwd, room) {
|
|
137
|
+
if (room !== void 0) {
|
|
138
|
+
const file = path.resolve(cwd, room);
|
|
139
|
+
if (!existsSync(file)) throw new Error(`irtio deploy: no room file at ${file}`);
|
|
140
|
+
return file;
|
|
141
|
+
}
|
|
142
|
+
for (const candidate of ROOM_CANDIDATES) {
|
|
143
|
+
const file = path.resolve(cwd, candidate);
|
|
144
|
+
if (existsSync(file)) return file;
|
|
145
|
+
}
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
function resolveEntry(cwd, room) {
|
|
149
|
+
const entry = findRoomEntry(cwd, room);
|
|
150
|
+
if (entry !== void 0) return entry;
|
|
151
|
+
throw new Error(
|
|
152
|
+
`irtio deploy: no room file found in ${cwd}
|
|
153
|
+
looked for: ${ROOM_CANDIDATES.join(", ")}
|
|
154
|
+
pass --room <file> if it lives elsewhere`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
function findRoomTypes(cwd, room) {
|
|
158
|
+
const dir = path.resolve(cwd, ROOM_TYPES_DIR);
|
|
159
|
+
const types = {};
|
|
160
|
+
if (existsSync(dir)) {
|
|
161
|
+
for (const name of readdirSync(dir).sort()) {
|
|
162
|
+
const ext = path.extname(name);
|
|
163
|
+
if (ext !== ".ts" && ext !== ".js") continue;
|
|
164
|
+
const type = name.slice(0, -ext.length);
|
|
165
|
+
if (!ROOM_TYPE_RE.test(type)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`irtio deploy: ${path.join(ROOM_TYPES_DIR, name)} is not a usable room type name
|
|
168
|
+
a type name is lowercase letters, digits, "_" or "-", starting with a letter or digit, at most 24 characters
|
|
169
|
+
rename the file, or move it out of ${ROOM_TYPES_DIR}/ if it is not a room entry`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
types[type] = path.join(dir, name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const explicit = findRoomEntry(cwd, room);
|
|
176
|
+
if (explicit !== void 0) {
|
|
177
|
+
if (room === void 0 && types[DEFAULT_ROOM_TYPE] !== void 0) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`irtio deploy: this project names its default room type twice, in ${ROOM_CANDIDATES[0]} and in ${ROOM_TYPES_DIR}/default.ts
|
|
180
|
+
keep one of them: a plain roomId resolves to exactly one definition`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { default: explicit, types: { ...types, [DEFAULT_ROOM_TYPE]: explicit } };
|
|
184
|
+
}
|
|
185
|
+
const fallback = types[DEFAULT_ROOM_TYPE];
|
|
186
|
+
if (fallback !== void 0) return { default: fallback, types };
|
|
187
|
+
if (Object.keys(types).length > 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`irtio deploy: ${ROOM_TYPES_DIR}/ declares ${Object.keys(types).join(", ")} but no default room type, and a roomId with no type prefix has to resolve to one
|
|
190
|
+
add ${ROOM_TYPES_DIR}/default.ts, or keep the single entry at ${ROOM_CANDIDATES[0]}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const entry = resolveEntry(cwd, room);
|
|
194
|
+
return { default: entry, types: { [DEFAULT_ROOM_TYPE]: entry } };
|
|
195
|
+
}
|
|
196
|
+
async function warnIfClientImportsRoom(cwd, entry, config, log) {
|
|
197
|
+
const client = config.client;
|
|
198
|
+
if (client === void 0) return;
|
|
199
|
+
const clientFile = path.resolve(cwd, client);
|
|
200
|
+
if (await clientImportsRoom(clientFile, entry)) {
|
|
201
|
+
log(
|
|
202
|
+
pc.yellow(
|
|
203
|
+
`irtio: WARNING: the client entry ${client} imports the room file ${path.relative(cwd, entry)} \u2014 room code must never ship to clients; move shared code (e.g. the physics world builder) into its own module both sides import`
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function loadBundledSchema(file) {
|
|
209
|
+
const mod = await import(pathToFileURL(file).href);
|
|
210
|
+
const config = mod.default.config;
|
|
211
|
+
return {
|
|
212
|
+
project: mod.default.schema.project,
|
|
213
|
+
canonical: mod.default.schema.canonical,
|
|
214
|
+
schema: mod.default.schema,
|
|
215
|
+
memoryMb: typeof config?.memoryMb === "number" ? config.memoryMb : void 0,
|
|
216
|
+
maxAwake: typeof config?.maxAwake === "number" ? config.maxAwake : void 0,
|
|
217
|
+
// The declared string, verbatim and unparsed. `defineRoom` already refused anything the
|
|
218
|
+
// grammar does not accept, and control refuses it again; the CLI's job is to carry what the
|
|
219
|
+
// bundle says rather than to have an opinion of its own about a duration.
|
|
220
|
+
retention: typeof config?.retention === "string" ? config.retention : void 0
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function printBreaking(log, breaking, hint) {
|
|
224
|
+
const n = breaking.length;
|
|
225
|
+
log(pc.red(`deploy refused: ${n} breaking change${n === 1 ? "" : "s"}`));
|
|
226
|
+
for (const change of breaking) log(` ${change.message}`);
|
|
227
|
+
log("");
|
|
228
|
+
log(pc.yellow(hint));
|
|
229
|
+
}
|
|
230
|
+
async function findMigrationFile(cwd, version) {
|
|
231
|
+
const dir = path.join(cwd, MIGRATIONS_DIR);
|
|
232
|
+
if (!existsSync(dir)) return void 0;
|
|
233
|
+
const entries = await readdir(dir);
|
|
234
|
+
const match = entries.find((f) => f.startsWith(`${version}_`) && f.endsWith(".ts"));
|
|
235
|
+
return match ? path.join(dir, match) : void 0;
|
|
236
|
+
}
|
|
237
|
+
var DEPLOY_BUILD_DIR = path.join(".irtio", "deploy");
|
|
238
|
+
async function runDeploy(options = {}) {
|
|
239
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
240
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
241
|
+
const ask = options.ask ?? defaultAsk;
|
|
242
|
+
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
243
|
+
const config = await readProjectConfig(cwd, "irtio deploy", options.config);
|
|
244
|
+
const roomTypes = findRoomTypes(cwd, options.room);
|
|
245
|
+
const entry = roomTypes.default;
|
|
246
|
+
await warnIfClientImportsRoom(cwd, entry, config, log);
|
|
247
|
+
const outDir = path.join(cwd, DEPLOY_BUILD_DIR);
|
|
248
|
+
await mkdir(outDir, { recursive: true });
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = await bundleRoom({
|
|
252
|
+
entry,
|
|
253
|
+
outDir,
|
|
254
|
+
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new Error(`irtio deploy: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
}
|
|
259
|
+
log(pc.green(`bundled ${path.basename(result.file)} (${result.hash.slice(0, 12)})`));
|
|
260
|
+
const bundled = await loadBundledSchema(result.file);
|
|
261
|
+
const client = options.client ?? await createApiClient(controlUrl);
|
|
262
|
+
const projectId = options.project ?? config.project ?? bundled.project;
|
|
263
|
+
if (projectId === void 0) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
'irtio deploy: no project id \u2014 pass --project, add "project" to irtio.json, or set it in defineSchema(...)'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const ensured = await ensureProject(client, projectId, {
|
|
269
|
+
cwd,
|
|
270
|
+
config,
|
|
271
|
+
command: "irtio deploy",
|
|
272
|
+
...options.name !== void 0 ? { name: options.name } : {}
|
|
273
|
+
});
|
|
274
|
+
logEnsured(log, projectId, ensured);
|
|
275
|
+
const deployments = await client.get(`/v1/projects/${projectId}/deployments`);
|
|
276
|
+
const newestOf = (list) => list.reduce(
|
|
277
|
+
(best, d) => best === void 0 || d.version > best.version ? d : best,
|
|
278
|
+
void 0
|
|
279
|
+
);
|
|
280
|
+
const previous = newestOf(deployments.filter((d) => d.rolledBackAt == null));
|
|
281
|
+
const newest = newestOf(deployments);
|
|
282
|
+
let allowBreaking = options.allowBreaking ?? false;
|
|
283
|
+
let migrationFile;
|
|
284
|
+
let schemaChange = "none";
|
|
285
|
+
const nextVersion = (newest?.version ?? 0) + 1;
|
|
286
|
+
if (previous !== void 0) {
|
|
287
|
+
const oldSchema = schemaFromCanonical(previous.schemaJson);
|
|
288
|
+
const changes = diffSchemas(oldSchema, bundled.schema);
|
|
289
|
+
const breaking = changes.filter((c) => c.kind === "breaking");
|
|
290
|
+
const additive = changes.filter((c) => c.kind === "additive");
|
|
291
|
+
schemaChange = breaking.length > 0 ? "breaking" : additive.length > 0 ? "additive" : "none";
|
|
292
|
+
if (breaking.length > 0) {
|
|
293
|
+
if (!allowBreaking) {
|
|
294
|
+
const hint = "run: irtio migrate create <name>";
|
|
295
|
+
printBreaking(log, breaking, hint);
|
|
296
|
+
throw new DeployRefusedError({ changes: breaking, hint });
|
|
297
|
+
}
|
|
298
|
+
migrationFile = await findMigrationFile(cwd, nextVersion);
|
|
299
|
+
if (migrationFile === void 0) {
|
|
300
|
+
const hint = `--allow-breaking needs a migration for v${nextVersion}. Run: irtio migrate create <name>`;
|
|
301
|
+
printBreaking(log, breaking, hint);
|
|
302
|
+
throw new DeployRefusedError({ changes: breaking, hint });
|
|
303
|
+
}
|
|
304
|
+
log(pc.yellow(`${breaking.length} breaking change(s), proceeding with --allow-breaking:`));
|
|
305
|
+
for (const change of breaking) log(` ${change.message}`);
|
|
306
|
+
} else {
|
|
307
|
+
allowBreaking = false;
|
|
308
|
+
}
|
|
309
|
+
if (additive.length > 0) {
|
|
310
|
+
log(pc.cyan(`${additive.length} additive change(s):`));
|
|
311
|
+
for (const change of additive) log(` ${change.message}`);
|
|
312
|
+
}
|
|
313
|
+
if (changes.length === 0) log(pc.dim("no schema changes"));
|
|
314
|
+
} else {
|
|
315
|
+
log(pc.dim("no previous deployment, so this is the first deploy for this project"));
|
|
316
|
+
}
|
|
317
|
+
let migrationKey;
|
|
318
|
+
if (migrationFile !== void 0) {
|
|
319
|
+
const migrationBundle = await bundleRoom({
|
|
320
|
+
entry: migrationFile,
|
|
321
|
+
outDir,
|
|
322
|
+
verify: false,
|
|
323
|
+
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
324
|
+
});
|
|
325
|
+
const bytes = new Uint8Array(await readFile(migrationBundle.file));
|
|
326
|
+
const uploaded = await client.postBytes(
|
|
327
|
+
`/v1/projects/${projectId}/artifacts`,
|
|
328
|
+
bytes
|
|
329
|
+
);
|
|
330
|
+
migrationKey = uploaded.key;
|
|
331
|
+
log(pc.green(`bundled and uploaded migration ${path.basename(migrationFile)}`));
|
|
332
|
+
}
|
|
333
|
+
const roomBytes = new Uint8Array(await readFile(result.file));
|
|
334
|
+
const uploadedRoom = await client.postBytes(
|
|
335
|
+
`/v1/projects/${projectId}/artifacts`,
|
|
336
|
+
roomBytes
|
|
337
|
+
);
|
|
338
|
+
log(pc.green("uploaded bundle"));
|
|
339
|
+
const extraRooms = {};
|
|
340
|
+
for (const [type, typeEntry] of Object.entries(roomTypes.types)) {
|
|
341
|
+
if (type === DEFAULT_ROOM_TYPE) continue;
|
|
342
|
+
let typeResult;
|
|
343
|
+
try {
|
|
344
|
+
typeResult = await bundleRoom({
|
|
345
|
+
entry: typeEntry,
|
|
346
|
+
outDir,
|
|
347
|
+
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`irtio deploy: room type "${type}": ${err instanceof Error ? err.message : String(err)}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (typeResult.schemaHash === void 0) {
|
|
355
|
+
throw new Error(`irtio deploy: room type "${type}" bundled without a schema hash`);
|
|
356
|
+
}
|
|
357
|
+
const typeSchema = await loadBundledSchema(typeResult.file);
|
|
358
|
+
const uploaded = await client.postBytes(
|
|
359
|
+
`/v1/projects/${projectId}/artifacts`,
|
|
360
|
+
new Uint8Array(await readFile(typeResult.file))
|
|
361
|
+
);
|
|
362
|
+
extraRooms[type] = {
|
|
363
|
+
bundleHash: typeResult.hash,
|
|
364
|
+
bundleKey: uploaded.key,
|
|
365
|
+
schemaJson: typeSchema.canonical,
|
|
366
|
+
schemaHash: typeResult.schemaHash,
|
|
367
|
+
// M5 part 3.5: this type's sizing declarations travel with its bundle, so control and the
|
|
368
|
+
// host agent can size the VM before it boots. Spread, never null: a type that declares
|
|
369
|
+
// neither sends neither, and the env generation of a project that has never declared
|
|
370
|
+
// anything is unchanged.
|
|
371
|
+
...typeSchema.memoryMb !== void 0 ? { memoryMb: typeSchema.memoryMb } : {},
|
|
372
|
+
...typeSchema.maxAwake !== void 0 ? { maxAwake: typeSchema.maxAwake } : {},
|
|
373
|
+
...typeSchema.retention !== void 0 ? { retention: typeSchema.retention } : {}
|
|
374
|
+
};
|
|
375
|
+
log(pc.green(`bundled and uploaded room type ${type} (${typeResult.hash.slice(0, 12)})`));
|
|
376
|
+
}
|
|
377
|
+
const hasExtraTypes = Object.keys(extraRooms).length > 0;
|
|
378
|
+
let origin;
|
|
379
|
+
if (previous === void 0) {
|
|
380
|
+
const answer = (await ask("production origin (localhost is always allowed) \u2014 leave blank to skip: ")).trim();
|
|
381
|
+
if (answer.length > 0) origin = answer;
|
|
382
|
+
}
|
|
383
|
+
const strategy = options.strategy ?? "drain";
|
|
384
|
+
if (strategy === "migrate") {
|
|
385
|
+
log(pc.yellow("strategy: migrate. Live rooms move onto the new version now."));
|
|
386
|
+
const physics = bundled.schema.collections.some(
|
|
387
|
+
(c) => c.physics !== void 0
|
|
388
|
+
);
|
|
389
|
+
if (schemaChange === "breaking") {
|
|
390
|
+
log(
|
|
391
|
+
pc.yellow(
|
|
392
|
+
"Breaking schema change: EVERY connected client is disconnected with E_SCHEMA_MISMATCH and must reload."
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
log(
|
|
397
|
+
pc.yellow(
|
|
398
|
+
schemaChange === "none" ? "Schema unchanged: connected clients see a short gap and a resync on the same socket." : "Additive schema change: connected clients see a short gap and a resync on the same socket, on the new schema."
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
if (physics) {
|
|
402
|
+
log(
|
|
403
|
+
pc.yellow(
|
|
404
|
+
"This schema has physics collections. A change touching one of those is treated as breaking for now, so those clients reload."
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
log(
|
|
409
|
+
pc.dim(
|
|
410
|
+
"Clients built before irtio 0.6 are still disconnected with E_SCHEMA_MISMATCH on any schema change."
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
let response;
|
|
416
|
+
try {
|
|
417
|
+
const created = await client.post(`/v1/projects/${projectId}/deployments`, {
|
|
418
|
+
bundleHash: result.hash,
|
|
419
|
+
bundleKey: uploadedRoom.key,
|
|
420
|
+
schemaJson: bundled.canonical,
|
|
421
|
+
schemaHash: result.schemaHash,
|
|
422
|
+
// M5 part 3.5: the default type's declarations, top-level beside its schema for the reason
|
|
423
|
+
// `DeploymentSpec` states — the default type is not in the `rooms` map, and putting it there
|
|
424
|
+
// would give every single-room project a `rooms` key and cold-boot the fleet.
|
|
425
|
+
...bundled.memoryMb !== void 0 ? { memoryMb: bundled.memoryMb } : {},
|
|
426
|
+
...bundled.maxAwake !== void 0 ? { maxAwake: bundled.maxAwake } : {},
|
|
427
|
+
...bundled.retention !== void 0 ? { retention: bundled.retention } : {},
|
|
428
|
+
...hasExtraTypes ? { rooms: extraRooms } : {},
|
|
429
|
+
...migrationKey !== void 0 ? { migrationKey } : {},
|
|
430
|
+
...allowBreaking ? { allowBreaking: true } : {},
|
|
431
|
+
...origin !== void 0 ? { origin } : {},
|
|
432
|
+
...strategy !== "drain" ? { strategy } : {}
|
|
433
|
+
});
|
|
434
|
+
response = {
|
|
435
|
+
project: projectId,
|
|
436
|
+
version: created.version,
|
|
437
|
+
url: created.url,
|
|
438
|
+
draining: created.draining,
|
|
439
|
+
...created.applied !== void 0 ? { applied: created.applied } : {},
|
|
440
|
+
...created.rooms !== void 0 ? { rooms: created.rooms } : {}
|
|
441
|
+
};
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (err instanceof ApiClientError && err.code === "E_BREAKING_SCHEMA" && err.changes) {
|
|
444
|
+
const changes = err.changes;
|
|
445
|
+
const hint = err.hint ?? "run: irtio migrate create <name>";
|
|
446
|
+
printBreaking(log, changes, hint);
|
|
447
|
+
throw new DeployRefusedError({ changes, hint });
|
|
448
|
+
}
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
await rm(outDir, { recursive: true, force: true }).catch(() => {
|
|
452
|
+
});
|
|
453
|
+
log(pc.green(`deployed v${response.version}`));
|
|
454
|
+
log(pc.dim(`reachable at ${response.url}`));
|
|
455
|
+
if (strategy === "migrate") {
|
|
456
|
+
if (response.applied === "live") {
|
|
457
|
+
const rooms = response.rooms ?? [];
|
|
458
|
+
if (rooms.length === 0) log(pc.dim("no live rooms to migrate"));
|
|
459
|
+
for (const r of rooms) {
|
|
460
|
+
const line = ` ${r.roomId}: ${r.outcome}${r.reason ? ` \u2014 ${r.reason}` : ""}`;
|
|
461
|
+
log(r.outcome === "failed" ? pc.red(line) : pc.dim(line));
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
log(pc.dim("no tenant is running; rooms migrate on their next wake"));
|
|
465
|
+
}
|
|
466
|
+
} else if (previous !== void 0) {
|
|
467
|
+
if (response.applied === "live") {
|
|
468
|
+
log(
|
|
469
|
+
pc.dim(
|
|
470
|
+
`rooms already running finish on v${previous.version}; new rooms start on v${response.version}`
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
} else {
|
|
474
|
+
log(pc.dim(`rooms already running finish on v${previous.version}`));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (config.static !== void 0 && options.noStatic !== true) {
|
|
478
|
+
const site = await runStaticDeploy({
|
|
479
|
+
cwd,
|
|
480
|
+
project: projectId,
|
|
481
|
+
log,
|
|
482
|
+
client,
|
|
483
|
+
controlUrl,
|
|
484
|
+
dir: config.static.dir,
|
|
485
|
+
...config.static.label !== void 0 ? { label: config.static.label } : {},
|
|
486
|
+
...options.config !== void 0 ? { config: options.config } : {}
|
|
487
|
+
});
|
|
488
|
+
return { ...response, site };
|
|
489
|
+
}
|
|
490
|
+
return response;
|
|
491
|
+
}
|
|
492
|
+
var DeployRefusedError = class extends Error {
|
|
493
|
+
name = "DeployRefusedError";
|
|
494
|
+
/** The breaking changes that caused the refusal, as the classifier reported them. */
|
|
495
|
+
changes;
|
|
496
|
+
/** The remedy, verbatim: normally `run: irtio migrate create <name>`. */
|
|
497
|
+
hint;
|
|
498
|
+
constructor(reason = {}) {
|
|
499
|
+
super("deploy refused");
|
|
500
|
+
this.changes = reason.changes ?? [];
|
|
501
|
+
this.hint = reason.hint;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
async function deploy(args, deps = {}) {
|
|
505
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
506
|
+
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
507
|
+
try {
|
|
508
|
+
const parsed = parseDeployArgs(args);
|
|
509
|
+
const cwd = path.resolve(deps.cwd ?? process.cwd());
|
|
510
|
+
const config = await readProjectConfig(cwd, "irtio deploy", parsed.config);
|
|
511
|
+
if (parsed.room === void 0 && parsed.noStatic !== true && config.static !== void 0 && findRoomEntry(cwd, void 0) === void 0) {
|
|
512
|
+
log(pc.dim("no room file, so deploying the static site only"));
|
|
513
|
+
await runStaticDeploy({
|
|
514
|
+
cwd,
|
|
515
|
+
log,
|
|
516
|
+
dir: config.static.dir,
|
|
517
|
+
...config.static.label !== void 0 ? { label: config.static.label } : {},
|
|
518
|
+
...parsed.project !== void 0 ? { project: parsed.project } : {},
|
|
519
|
+
...parsed.name !== void 0 ? { name: parsed.name } : {},
|
|
520
|
+
...parsed.config !== void 0 ? { config: parsed.config } : {},
|
|
521
|
+
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
522
|
+
...deps.client !== void 0 ? { client: deps.client } : {}
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
await runDeploy({
|
|
527
|
+
allowBreaking: parsed.allowBreaking,
|
|
528
|
+
log,
|
|
529
|
+
...parsed.project !== void 0 ? { project: parsed.project } : {},
|
|
530
|
+
...parsed.name !== void 0 ? { name: parsed.name } : {},
|
|
531
|
+
...parsed.config !== void 0 ? { config: parsed.config } : {},
|
|
532
|
+
...parsed.noStatic !== void 0 ? { noStatic: parsed.noStatic } : {},
|
|
533
|
+
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
534
|
+
...parsed.room !== void 0 ? { room: parsed.room } : {},
|
|
535
|
+
...parsed.strategy !== void 0 ? { strategy: parsed.strategy } : {},
|
|
536
|
+
...deps.client !== void 0 ? { client: deps.client } : {},
|
|
537
|
+
...deps.cwd !== void 0 ? { cwd: deps.cwd } : {},
|
|
538
|
+
...deps.irtioPackages !== void 0 ? { irtioPackages: deps.irtioPackages } : {}
|
|
539
|
+
});
|
|
540
|
+
} catch (err) {
|
|
541
|
+
if (err instanceof HelpRequested) {
|
|
542
|
+
log(err.usage);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (err instanceof DeployRefusedError) {
|
|
546
|
+
process.exitCode = 1;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (isLoginRequired(err)) {
|
|
550
|
+
errorLog(pc.red("not logged in"));
|
|
551
|
+
errorLog("run: irtio login");
|
|
552
|
+
process.exitCode = 1;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
errorLog(pc.red(err instanceof Error ? err.message : String(err)));
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
export {
|
|
560
|
+
DeployRefusedError,
|
|
561
|
+
USAGE,
|
|
562
|
+
deploy,
|
|
563
|
+
findRoomEntry,
|
|
564
|
+
findRoomTypes,
|
|
565
|
+
parseDeployArgs,
|
|
566
|
+
runDeploy
|
|
567
|
+
};
|