@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/chunk-BPE452KF.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
// ../store/dist/index.js
|
|
2
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
var KV_LIMITS = {
|
|
5
|
-
/** Max UTF-8 bytes in one value. */
|
|
6
|
-
valueBytes: 16 * 1024,
|
|
7
|
-
/** Max UTF-8 bytes in one key. */
|
|
8
|
-
keyBytes: 256,
|
|
9
|
-
/** Max UTF-8 bytes in one player id. */
|
|
10
|
-
playerIdBytes: 256,
|
|
11
|
-
/** Max distinct keys one player may hold within one project. */
|
|
12
|
-
keysPerPlayer: 128,
|
|
13
|
-
/** Max rows one project may hold across all its players. */
|
|
14
|
-
rowsPerProject: 1e6
|
|
15
|
-
};
|
|
16
|
-
var PLAYER_ISSUER_RE = /^[a-z0-9._-]{1,64}$/;
|
|
17
|
-
var KV_ERRORS = {
|
|
18
|
-
badKey: "E_KV_BAD_KEY",
|
|
19
|
-
badPlayer: "E_KV_BAD_PLAYER",
|
|
20
|
-
valueTooLarge: "E_KV_VALUE_TOO_LARGE",
|
|
21
|
-
tooManyKeys: "E_KV_TOO_MANY_KEYS",
|
|
22
|
-
projectFull: "E_KV_PROJECT_FULL",
|
|
23
|
-
forbidden: "E_KV_FORBIDDEN",
|
|
24
|
-
unavailable: "E_KV_UNAVAILABLE"
|
|
25
|
-
};
|
|
26
|
-
var SAVES_SEGMENT = "/saves/";
|
|
27
|
-
var SAVE_ID_TIME_DIGITS = 17;
|
|
28
|
-
var DEFAULT_SAVE_RETAIN = 10;
|
|
29
|
-
var PRE_MIGRATION_SAVE_ID = "premigrate";
|
|
30
|
-
function mintSaveId(nowMs, rand) {
|
|
31
|
-
const t = Math.max(0, Math.floor(nowMs));
|
|
32
|
-
const suffix = (Math.floor(Math.abs(rand)) & 65535).toString(16).padStart(4, "0");
|
|
33
|
-
return `${String(t).padStart(SAVE_ID_TIME_DIGITS, "0")}-${suffix}`;
|
|
34
|
-
}
|
|
35
|
-
function saveIdCreatedAt(saveId) {
|
|
36
|
-
if (saveId.length !== SAVE_ID_TIME_DIGITS + 5) return void 0;
|
|
37
|
-
if (saveId[SAVE_ID_TIME_DIGITS] !== "-") return void 0;
|
|
38
|
-
const time = saveId.slice(0, SAVE_ID_TIME_DIGITS);
|
|
39
|
-
if (!/^[0-9]+$/.test(time)) return void 0;
|
|
40
|
-
if (!/^[0-9a-f]{4}$/.test(saveId.slice(SAVE_ID_TIME_DIGITS + 1))) return void 0;
|
|
41
|
-
return Number(time);
|
|
42
|
-
}
|
|
43
|
-
function savesPrefix(liveKey) {
|
|
44
|
-
return `${liveKey}${SAVES_SEGMENT}`;
|
|
45
|
-
}
|
|
46
|
-
function saveKey(liveKey, saveId) {
|
|
47
|
-
return `${savesPrefix(liveKey)}${saveId}`;
|
|
48
|
-
}
|
|
49
|
-
function saveIdOf(liveKey, key) {
|
|
50
|
-
const prefix = savesPrefix(liveKey);
|
|
51
|
-
if (!key.startsWith(prefix)) return void 0;
|
|
52
|
-
const rest = key.slice(prefix.length);
|
|
53
|
-
if (rest === "" || rest.includes("/")) return void 0;
|
|
54
|
-
return rest;
|
|
55
|
-
}
|
|
56
|
-
async function listSaves(store, liveKey) {
|
|
57
|
-
const keys = await store.list(savesPrefix(liveKey));
|
|
58
|
-
const out = [];
|
|
59
|
-
for (const key of keys) {
|
|
60
|
-
const saveId = saveIdOf(liveKey, key);
|
|
61
|
-
if (saveId === void 0) continue;
|
|
62
|
-
const createdAt = saveIdCreatedAt(saveId);
|
|
63
|
-
if (createdAt === void 0) continue;
|
|
64
|
-
out.push({ saveId, key, createdAt });
|
|
65
|
-
}
|
|
66
|
-
out.sort((a, b) => a.saveId < b.saveId ? 1 : a.saveId > b.saveId ? -1 : 0);
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
function selectForPruning(saves, retain) {
|
|
70
|
-
const keep = Number.isFinite(retain) ? Math.max(1, Math.floor(retain)) : 1;
|
|
71
|
-
return saves.length <= keep ? [] : saves.slice(keep);
|
|
72
|
-
}
|
|
73
|
-
async function pruneSaves(store, saves, retain, log) {
|
|
74
|
-
const doomed = selectForPruning(saves, retain);
|
|
75
|
-
let deleted = 0;
|
|
76
|
-
for (const save of doomed) {
|
|
77
|
-
try {
|
|
78
|
-
await store.delete(save.key);
|
|
79
|
-
deleted++;
|
|
80
|
-
} catch (err) {
|
|
81
|
-
log("warn", `save retention: could not delete ${save.key}`, err);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return deleted;
|
|
85
|
-
}
|
|
86
|
-
var STATIC_LIMITS = {
|
|
87
|
-
/** Bytes per file. Big enough for a wasm build or a texture atlas; a video does not belong. */
|
|
88
|
-
maxFileBytes: 32 * 1024 * 1024,
|
|
89
|
-
/** Files per deploy. */
|
|
90
|
-
maxFiles: 2e3,
|
|
91
|
-
/** Total bytes per deploy. */
|
|
92
|
-
maxTotalBytes: 256 * 1024 * 1024
|
|
93
|
-
};
|
|
94
|
-
function staticPathProblem(path2) {
|
|
95
|
-
if (path2.length === 0 || path2.length > 512) return "path must be 1-512 characters";
|
|
96
|
-
if (path2.startsWith("/")) return "path must be relative (no leading slash)";
|
|
97
|
-
if (path2.includes("\\")) return "path must use forward slashes";
|
|
98
|
-
if (/[\x00-\x1f\x7f]/.test(path2)) return "path must not contain control characters";
|
|
99
|
-
for (const segment of path2.split("/")) {
|
|
100
|
-
if (segment === "" || segment === "." || segment === "..") {
|
|
101
|
-
return "path segments must be non-empty and not . or ..";
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return void 0;
|
|
105
|
-
}
|
|
106
|
-
var DiskStore = class {
|
|
107
|
-
constructor(dir) {
|
|
108
|
-
this.dir = dir;
|
|
109
|
-
}
|
|
110
|
-
dir;
|
|
111
|
-
fileFor(key) {
|
|
112
|
-
if (!/^[A-Za-z0-9_.@/-]+$/.test(key) || key.includes("..")) {
|
|
113
|
-
throw new Error(`invalid object store key ${JSON.stringify(key)}`);
|
|
114
|
-
}
|
|
115
|
-
return path.join(this.dir, `${key}.snap`);
|
|
116
|
-
}
|
|
117
|
-
async put(key, bytes) {
|
|
118
|
-
const file = this.fileFor(key);
|
|
119
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
120
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
121
|
-
await writeFile(tmp, bytes);
|
|
122
|
-
for (let attempt = 0; ; attempt++) {
|
|
123
|
-
try {
|
|
124
|
-
await rename(tmp, file);
|
|
125
|
-
return;
|
|
126
|
-
} catch (err) {
|
|
127
|
-
const code = err.code;
|
|
128
|
-
if ((code === "EPERM" || code === "EBUSY") && attempt < 10) {
|
|
129
|
-
await new Promise((resolve) => setTimeout(resolve, 5 * (attempt + 1)));
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
async get(key) {
|
|
137
|
-
try {
|
|
138
|
-
return new Uint8Array(await readFile(this.fileFor(key)));
|
|
139
|
-
} catch (err) {
|
|
140
|
-
if (err.code === "ENOENT") return void 0;
|
|
141
|
-
throw err;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async delete(key) {
|
|
145
|
-
await rm(this.fileFor(key), { force: true });
|
|
146
|
-
}
|
|
147
|
-
async list(prefix) {
|
|
148
|
-
const out = [];
|
|
149
|
-
const walk = async (dir, rel) => {
|
|
150
|
-
let entries;
|
|
151
|
-
try {
|
|
152
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
if (err.code === "ENOENT") return;
|
|
155
|
-
throw err;
|
|
156
|
-
}
|
|
157
|
-
for (const e of entries) {
|
|
158
|
-
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
159
|
-
if (e.isDirectory()) await walk(path.join(dir, e.name), r);
|
|
160
|
-
else if (e.name.endsWith(".snap")) out.push(r.slice(0, -".snap".length));
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
await walk(this.dir, "");
|
|
164
|
-
return out.filter((k) => k.startsWith(prefix)).sort();
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
export {
|
|
169
|
-
PLAYER_ISSUER_RE,
|
|
170
|
-
KV_ERRORS,
|
|
171
|
-
DEFAULT_SAVE_RETAIN,
|
|
172
|
-
PRE_MIGRATION_SAVE_ID,
|
|
173
|
-
mintSaveId,
|
|
174
|
-
saveKey,
|
|
175
|
-
listSaves,
|
|
176
|
-
pruneSaves,
|
|
177
|
-
STATIC_LIMITS,
|
|
178
|
-
staticPathProblem,
|
|
179
|
-
DiskStore
|
|
180
|
-
};
|
package/dist/chunk-D7CDJRFF.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// src/project-file.ts
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import { readFile } from "fs/promises";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
async function readIrtioJsonName(cwd, command) {
|
|
6
|
-
const file = path.join(cwd, "irtio.json");
|
|
7
|
-
if (!existsSync(file)) return void 0;
|
|
8
|
-
let parsed;
|
|
9
|
-
try {
|
|
10
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
11
|
-
} catch (err) {
|
|
12
|
-
throw new Error(`${command}: ${file} is not valid JSON: ${String(err)}`);
|
|
13
|
-
}
|
|
14
|
-
const name = parsed?.name;
|
|
15
|
-
if (name === void 0) return void 0;
|
|
16
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
17
|
-
throw new Error(`${command}: ${file} has a "name" that is not a non-empty string`);
|
|
18
|
-
}
|
|
19
|
-
return name;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export {
|
|
23
|
-
readIrtioJsonName
|
|
24
|
-
};
|
package/dist/deploy-YVCVDVMS.js
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clientImportsRoom
|
|
3
|
-
} from "./chunk-TQU6345E.js";
|
|
4
|
-
import {
|
|
5
|
-
bundleRoom
|
|
6
|
-
} from "./chunk-KRQUAEN2.js";
|
|
7
|
-
import {
|
|
8
|
-
readIrtioJsonName
|
|
9
|
-
} from "./chunk-D7CDJRFF.js";
|
|
10
|
-
import {
|
|
11
|
-
ApiClientError,
|
|
12
|
-
createApiClient,
|
|
13
|
-
isLoginRequired
|
|
14
|
-
} from "./chunk-I37DLT7K.js";
|
|
15
|
-
import {
|
|
16
|
-
resolveControlUrlForUser
|
|
17
|
-
} from "./chunk-NVUKSP5U.js";
|
|
18
|
-
|
|
19
|
-
// src/deploy.ts
|
|
20
|
-
import { existsSync } from "fs";
|
|
21
|
-
import { mkdir, readFile, readdir, rm } from "fs/promises";
|
|
22
|
-
import * as path from "path";
|
|
23
|
-
import { createInterface } from "readline/promises";
|
|
24
|
-
import { pathToFileURL } from "url";
|
|
25
|
-
import { diffSchemas, schemaFromCanonical } from "@irtio/schema";
|
|
26
|
-
import pc from "picocolors";
|
|
27
|
-
var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
|
|
28
|
-
var MIGRATIONS_DIR = "irtio/migrations";
|
|
29
|
-
function parseDeployArgs(args) {
|
|
30
|
-
const parsed = {
|
|
31
|
-
allowBreaking: false
|
|
32
|
-
};
|
|
33
|
-
for (let i = 0; i < args.length; i++) {
|
|
34
|
-
const arg = args[i];
|
|
35
|
-
switch (arg) {
|
|
36
|
-
case "--allow-breaking":
|
|
37
|
-
parsed.allowBreaking = true;
|
|
38
|
-
break;
|
|
39
|
-
case "--project": {
|
|
40
|
-
const value = args[++i];
|
|
41
|
-
if (value === void 0) throw new Error("irtio deploy: --project needs a value");
|
|
42
|
-
parsed.project = value;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case "--name": {
|
|
46
|
-
const value = args[++i];
|
|
47
|
-
if (value === void 0 || value === "") {
|
|
48
|
-
throw new Error("irtio deploy: --name needs a value");
|
|
49
|
-
}
|
|
50
|
-
parsed.name = value;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
case "--url": {
|
|
54
|
-
const value = args[++i];
|
|
55
|
-
if (value === void 0) throw new Error("irtio deploy: --url needs a value");
|
|
56
|
-
parsed.url = value;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case "--room": {
|
|
60
|
-
const value = args[++i];
|
|
61
|
-
if (value === void 0) throw new Error("irtio deploy: --room needs a value");
|
|
62
|
-
parsed.room = value;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
case "--strategy": {
|
|
66
|
-
const value = args[++i];
|
|
67
|
-
if (value !== "drain" && value !== "migrate") {
|
|
68
|
-
throw new Error('irtio deploy: --strategy must be "drain" or "migrate"');
|
|
69
|
-
}
|
|
70
|
-
parsed.strategy = value;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
default:
|
|
74
|
-
throw new Error(`irtio deploy: unknown option ${JSON.stringify(arg)}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return parsed;
|
|
78
|
-
}
|
|
79
|
-
async function defaultAsk(question) {
|
|
80
|
-
if (!process.stdin.isTTY) return "";
|
|
81
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
-
try {
|
|
83
|
-
return await rl.question(question);
|
|
84
|
-
} finally {
|
|
85
|
-
rl.close();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function resolveEntry(cwd, room) {
|
|
89
|
-
if (room !== void 0) {
|
|
90
|
-
const file = path.resolve(cwd, room);
|
|
91
|
-
if (!existsSync(file)) throw new Error(`irtio deploy: no room file at ${file}`);
|
|
92
|
-
return file;
|
|
93
|
-
}
|
|
94
|
-
for (const candidate of ROOM_CANDIDATES) {
|
|
95
|
-
const file = path.resolve(cwd, candidate);
|
|
96
|
-
if (existsSync(file)) return file;
|
|
97
|
-
}
|
|
98
|
-
throw new Error(
|
|
99
|
-
`irtio deploy: no room file found in ${cwd}
|
|
100
|
-
looked for: ${ROOM_CANDIDATES.join(", ")}
|
|
101
|
-
pass --room <file> if it lives elsewhere`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
async function readIrtioJsonProject(cwd) {
|
|
105
|
-
const file = path.join(cwd, "irtio.json");
|
|
106
|
-
if (!existsSync(file)) return void 0;
|
|
107
|
-
let parsed;
|
|
108
|
-
try {
|
|
109
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
110
|
-
} catch (err) {
|
|
111
|
-
throw new Error(`irtio deploy: ${file} is not valid JSON: ${String(err)}`);
|
|
112
|
-
}
|
|
113
|
-
const project = parsed?.project;
|
|
114
|
-
if (project === void 0) return void 0;
|
|
115
|
-
if (typeof project !== "string" || project.length === 0) {
|
|
116
|
-
throw new Error(`irtio deploy: ${file} has a "project" that is not a non-empty string`);
|
|
117
|
-
}
|
|
118
|
-
return project;
|
|
119
|
-
}
|
|
120
|
-
async function readIrtioJsonClient(cwd) {
|
|
121
|
-
const file = path.join(cwd, "irtio.json");
|
|
122
|
-
if (!existsSync(file)) return void 0;
|
|
123
|
-
let parsed;
|
|
124
|
-
try {
|
|
125
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
126
|
-
} catch (err) {
|
|
127
|
-
throw new Error(`irtio deploy: ${file} is not valid JSON: ${String(err)}`);
|
|
128
|
-
}
|
|
129
|
-
const client = parsed?.client;
|
|
130
|
-
if (client === void 0) return void 0;
|
|
131
|
-
if (typeof client !== "string" || client.length === 0) {
|
|
132
|
-
throw new Error(`irtio deploy: ${file} has a "client" that is not a non-empty string`);
|
|
133
|
-
}
|
|
134
|
-
return client;
|
|
135
|
-
}
|
|
136
|
-
async function warnIfClientImportsRoom(cwd, entry, log) {
|
|
137
|
-
const client = await readIrtioJsonClient(cwd);
|
|
138
|
-
if (client === void 0) return;
|
|
139
|
-
const clientFile = path.resolve(cwd, client);
|
|
140
|
-
if (await clientImportsRoom(clientFile, entry)) {
|
|
141
|
-
log(
|
|
142
|
-
pc.yellow(
|
|
143
|
-
`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`
|
|
144
|
-
)
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async function loadBundledSchema(file) {
|
|
149
|
-
const mod = await import(pathToFileURL(file).href);
|
|
150
|
-
return {
|
|
151
|
-
project: mod.default.schema.project,
|
|
152
|
-
canonical: mod.default.schema.canonical,
|
|
153
|
-
schema: mod.default.schema
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function printBreaking(log, breaking, hint) {
|
|
157
|
-
const n = breaking.length;
|
|
158
|
-
log(pc.red(`deploy refused: ${n} breaking change${n === 1 ? "" : "s"}`));
|
|
159
|
-
for (const change of breaking) log(` ${change.message}`);
|
|
160
|
-
log("");
|
|
161
|
-
log(pc.yellow(hint));
|
|
162
|
-
}
|
|
163
|
-
async function findMigrationFile(cwd, version) {
|
|
164
|
-
const dir = path.join(cwd, MIGRATIONS_DIR);
|
|
165
|
-
if (!existsSync(dir)) return void 0;
|
|
166
|
-
const entries = await readdir(dir);
|
|
167
|
-
const match = entries.find((f) => f.startsWith(`${version}_`) && f.endsWith(".ts"));
|
|
168
|
-
return match ? path.join(dir, match) : void 0;
|
|
169
|
-
}
|
|
170
|
-
var DEPLOY_BUILD_DIR = path.join(".irtio", "deploy");
|
|
171
|
-
async function runDeploy(options = {}) {
|
|
172
|
-
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
173
|
-
const log = options.log ?? ((line) => console.log(line));
|
|
174
|
-
const ask = options.ask ?? defaultAsk;
|
|
175
|
-
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
176
|
-
const entry = resolveEntry(cwd, options.room);
|
|
177
|
-
await warnIfClientImportsRoom(cwd, entry, log);
|
|
178
|
-
const outDir = path.join(cwd, DEPLOY_BUILD_DIR);
|
|
179
|
-
await mkdir(outDir, { recursive: true });
|
|
180
|
-
let result;
|
|
181
|
-
try {
|
|
182
|
-
result = await bundleRoom({
|
|
183
|
-
entry,
|
|
184
|
-
outDir,
|
|
185
|
-
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
186
|
-
});
|
|
187
|
-
} catch (err) {
|
|
188
|
-
throw new Error(`irtio deploy: ${err instanceof Error ? err.message : String(err)}`);
|
|
189
|
-
}
|
|
190
|
-
log(pc.green(`bundled ${path.basename(result.file)} (${result.hash.slice(0, 12)})`));
|
|
191
|
-
const bundled = await loadBundledSchema(result.file);
|
|
192
|
-
const client = options.client ?? await createApiClient(controlUrl);
|
|
193
|
-
const projectId = options.project ?? await readIrtioJsonProject(cwd) ?? bundled.project;
|
|
194
|
-
if (projectId === void 0) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
'irtio deploy: no project id \u2014 pass --project, add "project" to irtio.json, or set it in defineSchema(...)'
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
let projectExists = true;
|
|
200
|
-
try {
|
|
201
|
-
await client.get(`/v1/projects/${projectId}`);
|
|
202
|
-
} catch (err) {
|
|
203
|
-
if (err instanceof ApiClientError && err.status === 404) projectExists = false;
|
|
204
|
-
else throw err;
|
|
205
|
-
}
|
|
206
|
-
if (!projectExists) {
|
|
207
|
-
const name = options.name ?? await readIrtioJsonName(cwd, "irtio deploy") ?? path.basename(cwd);
|
|
208
|
-
await client.post("/v1/projects", { name, id: projectId });
|
|
209
|
-
log(pc.dim(`created project ${projectId} (${name})`));
|
|
210
|
-
}
|
|
211
|
-
const deployments = await client.get(`/v1/projects/${projectId}/deployments`);
|
|
212
|
-
const newestOf = (list) => list.reduce(
|
|
213
|
-
(best, d) => best === void 0 || d.version > best.version ? d : best,
|
|
214
|
-
void 0
|
|
215
|
-
);
|
|
216
|
-
const previous = newestOf(deployments.filter((d) => d.rolledBackAt == null));
|
|
217
|
-
const newest = newestOf(deployments);
|
|
218
|
-
let allowBreaking = options.allowBreaking ?? false;
|
|
219
|
-
let migrationFile;
|
|
220
|
-
const nextVersion = (newest?.version ?? 0) + 1;
|
|
221
|
-
if (previous !== void 0) {
|
|
222
|
-
const oldSchema = schemaFromCanonical(previous.schemaJson);
|
|
223
|
-
const changes = diffSchemas(oldSchema, bundled.schema);
|
|
224
|
-
const breaking = changes.filter((c) => c.kind === "breaking");
|
|
225
|
-
const additive = changes.filter((c) => c.kind === "additive");
|
|
226
|
-
if (breaking.length > 0) {
|
|
227
|
-
if (!allowBreaking) {
|
|
228
|
-
printBreaking(log, breaking, "run: irtio migrate create <name>");
|
|
229
|
-
throw new DeployRefusedError();
|
|
230
|
-
}
|
|
231
|
-
migrationFile = await findMigrationFile(cwd, nextVersion);
|
|
232
|
-
if (migrationFile === void 0) {
|
|
233
|
-
printBreaking(
|
|
234
|
-
log,
|
|
235
|
-
breaking,
|
|
236
|
-
`--allow-breaking needs a migration for v${nextVersion} \u2014 run: irtio migrate create <name>`
|
|
237
|
-
);
|
|
238
|
-
throw new DeployRefusedError();
|
|
239
|
-
}
|
|
240
|
-
log(pc.yellow(`${breaking.length} breaking change(s), proceeding with --allow-breaking:`));
|
|
241
|
-
for (const change of breaking) log(` ${change.message}`);
|
|
242
|
-
} else {
|
|
243
|
-
allowBreaking = false;
|
|
244
|
-
}
|
|
245
|
-
if (additive.length > 0) {
|
|
246
|
-
log(pc.cyan(`${additive.length} additive change(s):`));
|
|
247
|
-
for (const change of additive) log(` ${change.message}`);
|
|
248
|
-
}
|
|
249
|
-
if (changes.length === 0) log(pc.dim("no schema changes"));
|
|
250
|
-
} else {
|
|
251
|
-
log(pc.dim("no previous deployment \u2014 first deploy for this project"));
|
|
252
|
-
}
|
|
253
|
-
let migrationKey;
|
|
254
|
-
if (migrationFile !== void 0) {
|
|
255
|
-
const migrationBundle = await bundleRoom({
|
|
256
|
-
entry: migrationFile,
|
|
257
|
-
outDir,
|
|
258
|
-
verify: false,
|
|
259
|
-
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
260
|
-
});
|
|
261
|
-
const bytes = new Uint8Array(await readFile(migrationBundle.file));
|
|
262
|
-
const uploaded = await client.postBytes(
|
|
263
|
-
`/v1/projects/${projectId}/artifacts`,
|
|
264
|
-
bytes
|
|
265
|
-
);
|
|
266
|
-
migrationKey = uploaded.key;
|
|
267
|
-
log(pc.green(`bundled and uploaded migration ${path.basename(migrationFile)}`));
|
|
268
|
-
}
|
|
269
|
-
const roomBytes = new Uint8Array(await readFile(result.file));
|
|
270
|
-
const uploadedRoom = await client.postBytes(
|
|
271
|
-
`/v1/projects/${projectId}/artifacts`,
|
|
272
|
-
roomBytes
|
|
273
|
-
);
|
|
274
|
-
log(pc.green("uploaded bundle"));
|
|
275
|
-
let origin;
|
|
276
|
-
if (previous === void 0) {
|
|
277
|
-
const answer = (await ask("production origin (localhost is always allowed) \u2014 leave blank to skip: ")).trim();
|
|
278
|
-
if (answer.length > 0) origin = answer;
|
|
279
|
-
}
|
|
280
|
-
const strategy = options.strategy ?? "drain";
|
|
281
|
-
if (strategy === "migrate") {
|
|
282
|
-
log(pc.yellow("strategy: migrate \u2014 live rooms move onto the new version now."));
|
|
283
|
-
log(
|
|
284
|
-
pc.yellow(
|
|
285
|
-
"Schema unchanged: connected clients see a short gap and a resync on the same socket."
|
|
286
|
-
)
|
|
287
|
-
);
|
|
288
|
-
log(
|
|
289
|
-
pc.yellow(
|
|
290
|
-
"Schema changed at all: EVERY connected client is disconnected with E_SCHEMA_MISMATCH and must reload."
|
|
291
|
-
)
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
let response;
|
|
295
|
-
try {
|
|
296
|
-
const created = await client.post(`/v1/projects/${projectId}/deployments`, {
|
|
297
|
-
bundleHash: result.hash,
|
|
298
|
-
bundleKey: uploadedRoom.key,
|
|
299
|
-
schemaJson: bundled.canonical,
|
|
300
|
-
schemaHash: result.schemaHash,
|
|
301
|
-
...migrationKey !== void 0 ? { migrationKey } : {},
|
|
302
|
-
...allowBreaking ? { allowBreaking: true } : {},
|
|
303
|
-
...origin !== void 0 ? { origin } : {},
|
|
304
|
-
...strategy !== "drain" ? { strategy } : {}
|
|
305
|
-
});
|
|
306
|
-
response = {
|
|
307
|
-
project: projectId,
|
|
308
|
-
version: created.version,
|
|
309
|
-
url: created.url,
|
|
310
|
-
draining: created.draining,
|
|
311
|
-
...created.applied !== void 0 ? { applied: created.applied } : {},
|
|
312
|
-
...created.rooms !== void 0 ? { rooms: created.rooms } : {}
|
|
313
|
-
};
|
|
314
|
-
} catch (err) {
|
|
315
|
-
if (err instanceof ApiClientError && err.code === "E_BREAKING_SCHEMA" && err.changes) {
|
|
316
|
-
printBreaking(
|
|
317
|
-
log,
|
|
318
|
-
err.changes,
|
|
319
|
-
err.hint ?? "run: irtio migrate create <name>"
|
|
320
|
-
);
|
|
321
|
-
throw new DeployRefusedError();
|
|
322
|
-
}
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
325
|
-
await rm(outDir, { recursive: true, force: true }).catch(() => {
|
|
326
|
-
});
|
|
327
|
-
log(pc.green(`deployed v${response.version}`));
|
|
328
|
-
log(pc.dim(`reachable at ${response.url}`));
|
|
329
|
-
if (strategy === "migrate") {
|
|
330
|
-
if (response.applied === "live") {
|
|
331
|
-
const rooms = response.rooms ?? [];
|
|
332
|
-
if (rooms.length === 0) log(pc.dim("no live rooms to migrate"));
|
|
333
|
-
for (const r of rooms) {
|
|
334
|
-
const line = ` ${r.roomId}: ${r.outcome}${r.reason ? ` \u2014 ${r.reason}` : ""}`;
|
|
335
|
-
log(r.outcome === "failed" ? pc.red(line) : pc.dim(line));
|
|
336
|
-
}
|
|
337
|
-
} else {
|
|
338
|
-
log(pc.dim("no tenant is running; rooms migrate on their next wake"));
|
|
339
|
-
}
|
|
340
|
-
} else if (previous !== void 0) {
|
|
341
|
-
if (response.applied === "live") {
|
|
342
|
-
log(
|
|
343
|
-
pc.dim(
|
|
344
|
-
`rooms already running finish on v${previous.version}; new rooms start on v${response.version}`
|
|
345
|
-
)
|
|
346
|
-
);
|
|
347
|
-
} else {
|
|
348
|
-
log(pc.dim(`rooms already running finish on v${previous.version}`));
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return response;
|
|
352
|
-
}
|
|
353
|
-
var DeployRefusedError = class extends Error {
|
|
354
|
-
name = "DeployRefusedError";
|
|
355
|
-
constructor() {
|
|
356
|
-
super("deploy refused");
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
async function deploy(args, deps = {}) {
|
|
360
|
-
const log = deps.log ?? ((line) => console.log(line));
|
|
361
|
-
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
362
|
-
try {
|
|
363
|
-
const parsed = parseDeployArgs(args);
|
|
364
|
-
await runDeploy({
|
|
365
|
-
allowBreaking: parsed.allowBreaking,
|
|
366
|
-
log,
|
|
367
|
-
...parsed.project !== void 0 ? { project: parsed.project } : {},
|
|
368
|
-
...parsed.name !== void 0 ? { name: parsed.name } : {},
|
|
369
|
-
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
370
|
-
...parsed.room !== void 0 ? { room: parsed.room } : {},
|
|
371
|
-
...parsed.strategy !== void 0 ? { strategy: parsed.strategy } : {},
|
|
372
|
-
...deps.client !== void 0 ? { client: deps.client } : {},
|
|
373
|
-
...deps.cwd !== void 0 ? { cwd: deps.cwd } : {},
|
|
374
|
-
...deps.irtioPackages !== void 0 ? { irtioPackages: deps.irtioPackages } : {}
|
|
375
|
-
});
|
|
376
|
-
} catch (err) {
|
|
377
|
-
if (err instanceof DeployRefusedError) {
|
|
378
|
-
process.exitCode = 1;
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (isLoginRequired(err)) {
|
|
382
|
-
errorLog(pc.red("not logged in"));
|
|
383
|
-
errorLog("run: irtio login");
|
|
384
|
-
process.exitCode = 1;
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
errorLog(pc.red(err instanceof Error ? err.message : String(err)));
|
|
388
|
-
process.exitCode = 1;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
export {
|
|
392
|
-
DeployRefusedError,
|
|
393
|
-
deploy,
|
|
394
|
-
parseDeployArgs,
|
|
395
|
-
runDeploy
|
|
396
|
-
};
|