ownlab 0.0.3
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 +661 -0
- package/dist/index.js +853 -0
- package/package.json +24 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// ../../packages/shared/src/constants.ts
|
|
10
|
+
var ADAPTER_TYPE_LABELS = {
|
|
11
|
+
codex_local: "Codex",
|
|
12
|
+
claude_code: "Claude Code",
|
|
13
|
+
opencode: "OpenCode"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/config/store.ts
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path2 from "node:path";
|
|
19
|
+
|
|
20
|
+
// src/config/home.ts
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
var DEFAULT_INSTANCE_ID = "default";
|
|
24
|
+
var INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
25
|
+
function expandHomePrefix(value) {
|
|
26
|
+
if (value === "~") return os.homedir();
|
|
27
|
+
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function resolveOwnlabHomeDir() {
|
|
31
|
+
const envHome = process.env.OWNLAB_HOME?.trim();
|
|
32
|
+
if (envHome) return path.resolve(expandHomePrefix(envHome));
|
|
33
|
+
return path.resolve(os.homedir(), ".ownlab");
|
|
34
|
+
}
|
|
35
|
+
function resolveOwnlabInstanceId(override) {
|
|
36
|
+
const raw = override?.trim() || process.env.OWNLAB_INSTANCE_ID?.trim() || DEFAULT_INSTANCE_ID;
|
|
37
|
+
if (!INSTANCE_ID_RE.test(raw)) {
|
|
38
|
+
throw new Error(`Invalid instance id '${raw}'. Allowed characters: letters, numbers, '_' and '-'.`);
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
function resolveOwnlabInstanceRoot(instanceId) {
|
|
43
|
+
return path.resolve(resolveOwnlabHomeDir(), "instances", resolveOwnlabInstanceId(instanceId));
|
|
44
|
+
}
|
|
45
|
+
function resolveDefaultConfigPath(instanceId) {
|
|
46
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), "config.json");
|
|
47
|
+
}
|
|
48
|
+
function resolveDefaultEnvPath(instanceId) {
|
|
49
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), ".env");
|
|
50
|
+
}
|
|
51
|
+
function resolveDefaultDbDir(instanceId) {
|
|
52
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), "db");
|
|
53
|
+
}
|
|
54
|
+
function resolveDefaultLogDir(instanceId) {
|
|
55
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), "logs");
|
|
56
|
+
}
|
|
57
|
+
function resolveDefaultBackupDir(instanceId) {
|
|
58
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), "data", "backups");
|
|
59
|
+
}
|
|
60
|
+
function resolveDefaultCacheDir(instanceId) {
|
|
61
|
+
return path.resolve(resolveOwnlabInstanceRoot(instanceId), "cache");
|
|
62
|
+
}
|
|
63
|
+
function describeLocalInstancePaths(instanceId) {
|
|
64
|
+
const resolvedInstanceId = resolveOwnlabInstanceId(instanceId);
|
|
65
|
+
const instanceRoot = resolveOwnlabInstanceRoot(resolvedInstanceId);
|
|
66
|
+
return {
|
|
67
|
+
homeDir: resolveOwnlabHomeDir(),
|
|
68
|
+
instanceId: resolvedInstanceId,
|
|
69
|
+
instanceRoot,
|
|
70
|
+
configPath: resolveDefaultConfigPath(resolvedInstanceId),
|
|
71
|
+
envPath: resolveDefaultEnvPath(resolvedInstanceId),
|
|
72
|
+
dbDir: resolveDefaultDbDir(resolvedInstanceId),
|
|
73
|
+
logDir: resolveDefaultLogDir(resolvedInstanceId),
|
|
74
|
+
backupDir: resolveDefaultBackupDir(resolvedInstanceId),
|
|
75
|
+
cacheDir: resolveDefaultCacheDir(resolvedInstanceId)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/config/schema.ts
|
|
80
|
+
function isObject(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
function isPort(value) {
|
|
84
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
85
|
+
}
|
|
86
|
+
function validateOwnlabCliConfig(value) {
|
|
87
|
+
if (!isObject(value)) {
|
|
88
|
+
throw new Error("Config must be an object");
|
|
89
|
+
}
|
|
90
|
+
const { version, server, web, database, runtime } = value;
|
|
91
|
+
if (version !== 1) {
|
|
92
|
+
throw new Error("Config version must be 1");
|
|
93
|
+
}
|
|
94
|
+
if (!isObject(server) || typeof server.host !== "string" || !isPort(server.port)) {
|
|
95
|
+
throw new Error("Config server.host/server.port are required");
|
|
96
|
+
}
|
|
97
|
+
if (!isObject(web) || typeof web.host !== "string" || !isPort(web.port) || web.mode !== "local-next") {
|
|
98
|
+
throw new Error("Config web.host/web.port/web.mode are required");
|
|
99
|
+
}
|
|
100
|
+
if (!isObject(database) || database.mode !== "embedded-postgres" && database.mode !== "postgres" || typeof database.embeddedPostgresDataDir !== "string" || !isPort(database.embeddedPostgresPort)) {
|
|
101
|
+
throw new Error("Config database settings are invalid");
|
|
102
|
+
}
|
|
103
|
+
if (!isObject(runtime) || typeof runtime.ownlabHome !== "string" || typeof runtime.instanceId !== "string" || typeof runtime.logDir !== "string" || typeof runtime.backupDir !== "string" || typeof runtime.cacheDir !== "string") {
|
|
104
|
+
throw new Error("Config runtime settings are invalid");
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
server: {
|
|
109
|
+
host: server.host,
|
|
110
|
+
port: server.port
|
|
111
|
+
},
|
|
112
|
+
web: {
|
|
113
|
+
host: web.host,
|
|
114
|
+
port: web.port,
|
|
115
|
+
mode: "local-next"
|
|
116
|
+
},
|
|
117
|
+
database: {
|
|
118
|
+
mode: database.mode,
|
|
119
|
+
...typeof database.connectionString === "string" ? { connectionString: database.connectionString } : {},
|
|
120
|
+
embeddedPostgresDataDir: database.embeddedPostgresDataDir,
|
|
121
|
+
embeddedPostgresPort: database.embeddedPostgresPort
|
|
122
|
+
},
|
|
123
|
+
runtime: {
|
|
124
|
+
ownlabHome: runtime.ownlabHome,
|
|
125
|
+
instanceId: runtime.instanceId,
|
|
126
|
+
logDir: runtime.logDir,
|
|
127
|
+
backupDir: runtime.backupDir,
|
|
128
|
+
cacheDir: runtime.cacheDir
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/config/store.ts
|
|
134
|
+
var DEFAULT_CONFIG_BASENAME = "config.json";
|
|
135
|
+
function findConfigFileFromAncestors(startDir) {
|
|
136
|
+
let currentDir = path2.resolve(startDir);
|
|
137
|
+
while (true) {
|
|
138
|
+
const candidate = path2.resolve(currentDir, ".ownlab", DEFAULT_CONFIG_BASENAME);
|
|
139
|
+
if (fs.existsSync(candidate)) {
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
const nextDir = path2.resolve(currentDir, "..");
|
|
143
|
+
if (nextDir === currentDir) break;
|
|
144
|
+
currentDir = nextDir;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function resolveConfigPath(overridePath) {
|
|
149
|
+
if (overridePath) return path2.resolve(overridePath);
|
|
150
|
+
if (process.env.OWNLAB_CONFIG) return path2.resolve(process.env.OWNLAB_CONFIG);
|
|
151
|
+
return findConfigFileFromAncestors(process.cwd()) ?? resolveDefaultConfigPath(resolveOwnlabInstanceId());
|
|
152
|
+
}
|
|
153
|
+
function configExists(configPath) {
|
|
154
|
+
return fs.existsSync(resolveConfigPath(configPath));
|
|
155
|
+
}
|
|
156
|
+
function readConfig(configPath) {
|
|
157
|
+
const filePath = resolveConfigPath(configPath);
|
|
158
|
+
if (!fs.existsSync(filePath)) return null;
|
|
159
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
160
|
+
return validateOwnlabCliConfig(raw);
|
|
161
|
+
}
|
|
162
|
+
function writeConfig(config, configPath) {
|
|
163
|
+
const filePath = resolveConfigPath(configPath);
|
|
164
|
+
fs.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
165
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/checks/config.ts
|
|
170
|
+
function configCheck(configPath) {
|
|
171
|
+
if (!configExists(configPath)) {
|
|
172
|
+
return {
|
|
173
|
+
name: "Config file",
|
|
174
|
+
status: "fail",
|
|
175
|
+
message: "No config file found. Run `ownlab onboard --yes` first."
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
readConfig(configPath);
|
|
180
|
+
return {
|
|
181
|
+
name: "Config file",
|
|
182
|
+
status: "pass",
|
|
183
|
+
message: "Config file exists and is valid."
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
name: "Config file",
|
|
188
|
+
status: "fail",
|
|
189
|
+
message: error instanceof Error ? error.message : String(error)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/checks/env.ts
|
|
195
|
+
import fs3 from "node:fs";
|
|
196
|
+
|
|
197
|
+
// src/config/env.ts
|
|
198
|
+
import fs2 from "node:fs";
|
|
199
|
+
import path3 from "node:path";
|
|
200
|
+
var loadedEnvFiles = /* @__PURE__ */ new Set();
|
|
201
|
+
function resolveEnvFilePath(configPath) {
|
|
202
|
+
return path3.resolve(path3.dirname(resolveConfigPath(configPath)), ".env");
|
|
203
|
+
}
|
|
204
|
+
function parseEnvFile(contents) {
|
|
205
|
+
const entries = {};
|
|
206
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
207
|
+
const line = rawLine.trim();
|
|
208
|
+
if (!line || line.startsWith("#")) continue;
|
|
209
|
+
const eqIndex = line.indexOf("=");
|
|
210
|
+
if (eqIndex <= 0) continue;
|
|
211
|
+
const key = line.slice(0, eqIndex).trim();
|
|
212
|
+
let value = line.slice(eqIndex + 1).trim();
|
|
213
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
214
|
+
value = value.slice(1, -1);
|
|
215
|
+
}
|
|
216
|
+
entries[key] = value;
|
|
217
|
+
}
|
|
218
|
+
return entries;
|
|
219
|
+
}
|
|
220
|
+
function renderEnvFile(entries) {
|
|
221
|
+
return [
|
|
222
|
+
"# OwnLab environment variables",
|
|
223
|
+
"# Generated by OwnLab CLI commands",
|
|
224
|
+
...Object.entries(entries).map(([key, value]) => `${key}=${JSON.stringify(value)}`),
|
|
225
|
+
""
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
228
|
+
function resolveOwnlabEnvFile(configPath) {
|
|
229
|
+
return resolveEnvFilePath(configPath);
|
|
230
|
+
}
|
|
231
|
+
function readOwnlabEnvEntries(filePath = resolveEnvFilePath()) {
|
|
232
|
+
if (!fs2.existsSync(filePath)) return {};
|
|
233
|
+
return parseEnvFile(fs2.readFileSync(filePath, "utf8"));
|
|
234
|
+
}
|
|
235
|
+
function writeOwnlabEnvEntries(entries, filePath = resolveEnvFilePath()) {
|
|
236
|
+
fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
237
|
+
fs2.writeFileSync(filePath, renderEnvFile(entries), { mode: 384 });
|
|
238
|
+
}
|
|
239
|
+
function mergeOwnlabEnvEntries(entries, filePath = resolveEnvFilePath()) {
|
|
240
|
+
const current = readOwnlabEnvEntries(filePath);
|
|
241
|
+
const next = {
|
|
242
|
+
...current,
|
|
243
|
+
...Object.fromEntries(
|
|
244
|
+
Object.entries(entries).filter(([, value]) => typeof value === "string" && value.trim().length > 0)
|
|
245
|
+
)
|
|
246
|
+
};
|
|
247
|
+
writeOwnlabEnvEntries(next, filePath);
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
function loadOwnlabEnvFile(configPath) {
|
|
251
|
+
const filePath = resolveEnvFilePath(configPath);
|
|
252
|
+
if (loadedEnvFiles.has(filePath) || !fs2.existsSync(filePath)) return;
|
|
253
|
+
loadedEnvFiles.add(filePath);
|
|
254
|
+
const entries = readOwnlabEnvEntries(filePath);
|
|
255
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
256
|
+
if (!process.env[key]) {
|
|
257
|
+
process.env[key] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/checks/env.ts
|
|
263
|
+
function envCheck(configPath) {
|
|
264
|
+
const envFile = resolveOwnlabEnvFile(configPath);
|
|
265
|
+
if (!fs3.existsSync(envFile)) {
|
|
266
|
+
return {
|
|
267
|
+
name: ".env file",
|
|
268
|
+
status: "warn",
|
|
269
|
+
message: `No .env file found at ${envFile}. It will be created during onboard.`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
name: ".env file",
|
|
274
|
+
status: "pass",
|
|
275
|
+
message: `.env file found at ${envFile}.`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/checks/filesystem.ts
|
|
280
|
+
import fs4 from "node:fs";
|
|
281
|
+
import path4 from "node:path";
|
|
282
|
+
function checkPathWritable(targetPath) {
|
|
283
|
+
const parentDir = fs4.existsSync(targetPath) ? targetPath : path4.dirname(targetPath);
|
|
284
|
+
try {
|
|
285
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
286
|
+
fs4.accessSync(parentDir, fs4.constants.W_OK);
|
|
287
|
+
return true;
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function filesystemCheck(config) {
|
|
293
|
+
const targets = [
|
|
294
|
+
["OwnLab home", config.runtime.ownlabHome],
|
|
295
|
+
["Log dir", config.runtime.logDir],
|
|
296
|
+
["Backup dir", config.runtime.backupDir],
|
|
297
|
+
["Cache dir", config.runtime.cacheDir],
|
|
298
|
+
["Embedded Postgres dir", config.database.embeddedPostgresDataDir]
|
|
299
|
+
];
|
|
300
|
+
return targets.map(([name, targetPath]) => ({
|
|
301
|
+
name,
|
|
302
|
+
status: checkPathWritable(targetPath) ? "pass" : "fail",
|
|
303
|
+
message: checkPathWritable(targetPath) ? `${name} is writable: ${targetPath}` : `${name} is not writable: ${targetPath}`
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/checks/node.ts
|
|
308
|
+
function nodeCheck() {
|
|
309
|
+
const major = Number(process.versions.node.split(".")[0] ?? "0");
|
|
310
|
+
if (major < 20) {
|
|
311
|
+
return {
|
|
312
|
+
name: "Node.js version",
|
|
313
|
+
status: "fail",
|
|
314
|
+
message: `Node.js 20+ is required. Current version: ${process.versions.node}`
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
name: "Node.js version",
|
|
319
|
+
status: "pass",
|
|
320
|
+
message: `Node.js version is supported: ${process.versions.node}`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/checks/ports.ts
|
|
325
|
+
import net from "node:net";
|
|
326
|
+
function checkPortFree(port, host) {
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
const server = net.createServer();
|
|
329
|
+
server.once("error", () => resolve(false));
|
|
330
|
+
server.listen(port, host, () => {
|
|
331
|
+
server.close(() => resolve(true));
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async function portChecks(config) {
|
|
336
|
+
const serverFree = await checkPortFree(config.server.port, config.server.host);
|
|
337
|
+
const webFree = await checkPortFree(config.web.port, config.web.host);
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
name: "Server port",
|
|
341
|
+
status: serverFree ? "pass" : "warn",
|
|
342
|
+
message: serverFree ? `Port ${config.server.port} is available.` : `Port ${config.server.port} is already in use. This may be fine if OwnLab is already running.`
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "Web port",
|
|
346
|
+
status: webFree ? "pass" : "warn",
|
|
347
|
+
message: webFree ? `Port ${config.web.port} is available.` : `Port ${config.web.port} is already in use. This may be fine if the web app is already running.`
|
|
348
|
+
}
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/checks/server.ts
|
|
353
|
+
async function serverCheck(options) {
|
|
354
|
+
if (options.repoRoot) {
|
|
355
|
+
const serverEntry = `${options.repoRoot}/apps/server/src/index.ts`;
|
|
356
|
+
return {
|
|
357
|
+
name: "Server runtime",
|
|
358
|
+
status: "pass",
|
|
359
|
+
message: `Found OwnLab server entry at ${serverEntry}`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (options.serverPackageRoot) {
|
|
363
|
+
return {
|
|
364
|
+
name: "Server runtime",
|
|
365
|
+
status: "pass",
|
|
366
|
+
message: `Resolved packaged OwnLab server from ${options.serverPackageRoot}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
await import("@ownlabai/server");
|
|
371
|
+
return {
|
|
372
|
+
name: "Server runtime",
|
|
373
|
+
status: "pass",
|
|
374
|
+
message: `Resolved packaged OwnLab server from ${options.serverPackageRoot ?? "@ownlabai/server"}`
|
|
375
|
+
};
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
name: "Server runtime",
|
|
380
|
+
status: "fail",
|
|
381
|
+
message: "Could not resolve repo or packaged OwnLab server runtime."
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/runtime/paths.ts
|
|
386
|
+
import fs5 from "node:fs";
|
|
387
|
+
import { createRequire } from "node:module";
|
|
388
|
+
import path5 from "node:path";
|
|
389
|
+
import { fileURLToPath } from "node:url";
|
|
390
|
+
function readPackageName(packageJsonPath) {
|
|
391
|
+
try {
|
|
392
|
+
const raw = fs5.readFileSync(packageJsonPath, "utf8");
|
|
393
|
+
const parsed = JSON.parse(raw);
|
|
394
|
+
return parsed.name ?? null;
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function findCliPackageRoot(fromImportMetaUrl) {
|
|
400
|
+
let currentDir = path5.dirname(fileURLToPath(fromImportMetaUrl));
|
|
401
|
+
while (true) {
|
|
402
|
+
const packageJsonPath = path5.join(currentDir, "package.json");
|
|
403
|
+
if (fs5.existsSync(packageJsonPath) && readPackageName(packageJsonPath) === "ownlab") {
|
|
404
|
+
return currentDir;
|
|
405
|
+
}
|
|
406
|
+
const parentDir = path5.dirname(currentDir);
|
|
407
|
+
if (parentDir === currentDir) {
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
currentDir = parentDir;
|
|
411
|
+
}
|
|
412
|
+
return path5.resolve(path5.dirname(fileURLToPath(fromImportMetaUrl)), "../..");
|
|
413
|
+
}
|
|
414
|
+
function findRepoRoot(cliPackageRoot) {
|
|
415
|
+
const candidates = [
|
|
416
|
+
path5.resolve(cliPackageRoot, "../.."),
|
|
417
|
+
path5.resolve(cliPackageRoot, "../../.."),
|
|
418
|
+
process.cwd()
|
|
419
|
+
];
|
|
420
|
+
for (const candidate of candidates) {
|
|
421
|
+
const serverEntry = path5.resolve(candidate, "apps/server/src/index.ts");
|
|
422
|
+
const webPackageJson = path5.resolve(candidate, "apps/web/package.json");
|
|
423
|
+
if (fs5.existsSync(serverEntry) && fs5.existsSync(webPackageJson)) {
|
|
424
|
+
return candidate;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
function findInstalledServerPackageRoot(fromImportMetaUrl) {
|
|
430
|
+
const cliPackageRoot = findCliPackageRoot(fromImportMetaUrl);
|
|
431
|
+
const siblingCandidates = [
|
|
432
|
+
path5.resolve(cliPackageRoot, "../@ownlabai/server"),
|
|
433
|
+
path5.resolve(cliPackageRoot, "../../@ownlabai/server")
|
|
434
|
+
];
|
|
435
|
+
for (const candidate of siblingCandidates) {
|
|
436
|
+
const packageJsonPath = path5.resolve(candidate, "package.json");
|
|
437
|
+
if (fs5.existsSync(packageJsonPath) && readPackageName(packageJsonPath) === "@ownlabai/server") {
|
|
438
|
+
return candidate;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const require2 = createRequire(fromImportMetaUrl);
|
|
443
|
+
const serverEntry = require2.resolve("@ownlabai/server");
|
|
444
|
+
return path5.resolve(serverEntry, "..", "..");
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function findInstalledWebPackageRoot(fromImportMetaUrl) {
|
|
450
|
+
const cliPackageRoot = findCliPackageRoot(fromImportMetaUrl);
|
|
451
|
+
const siblingCandidates = [
|
|
452
|
+
path5.resolve(cliPackageRoot, "../@ownlabai/web"),
|
|
453
|
+
path5.resolve(cliPackageRoot, "../../@ownlabai/web")
|
|
454
|
+
];
|
|
455
|
+
for (const candidate of siblingCandidates) {
|
|
456
|
+
const packageJsonPath = path5.resolve(candidate, "package.json");
|
|
457
|
+
if (fs5.existsSync(packageJsonPath) && readPackageName(packageJsonPath) === "@ownlabai/web") {
|
|
458
|
+
return candidate;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const require2 = createRequire(fromImportMetaUrl);
|
|
463
|
+
const webPackageJson = require2.resolve("@ownlabai/web/package.json");
|
|
464
|
+
return path5.dirname(webPackageJson);
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function getInstalledWebNextBinPath(webPackageRoot) {
|
|
470
|
+
return path5.resolve(webPackageRoot, "node_modules/next/dist/bin/next");
|
|
471
|
+
}
|
|
472
|
+
function hasInstalledWebPackage(webPackageRoot) {
|
|
473
|
+
if (!webPackageRoot) return false;
|
|
474
|
+
return fs5.existsSync(path5.resolve(webPackageRoot, ".next/BUILD_ID")) && fs5.existsSync(getInstalledWebNextBinPath(webPackageRoot));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/checks/web.ts
|
|
478
|
+
function webCheck(options) {
|
|
479
|
+
if (options.repoRoot) {
|
|
480
|
+
const webPackageJson = `${options.repoRoot}/apps/web/package.json`;
|
|
481
|
+
return {
|
|
482
|
+
name: "Web runtime",
|
|
483
|
+
status: "pass",
|
|
484
|
+
message: `Found OwnLab web app at ${webPackageJson}`
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (hasInstalledWebPackage(options.webPackageRoot)) {
|
|
488
|
+
return {
|
|
489
|
+
name: "Web runtime",
|
|
490
|
+
status: "pass",
|
|
491
|
+
message: `Found packaged OwnLab web runtime at ${options.webPackageRoot}`
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
name: "Web runtime",
|
|
496
|
+
status: "fail",
|
|
497
|
+
message: "Could not find repo or installed OwnLab web runtime."
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/commands/doctor.ts
|
|
502
|
+
var STATUS_ICON = {
|
|
503
|
+
pass: "PASS",
|
|
504
|
+
warn: "WARN",
|
|
505
|
+
fail: "FAIL"
|
|
506
|
+
};
|
|
507
|
+
function printResult(result) {
|
|
508
|
+
process.stdout.write(`${STATUS_ICON[result.status]} ${result.name}: ${result.message}
|
|
509
|
+
`);
|
|
510
|
+
}
|
|
511
|
+
async function doctor(opts) {
|
|
512
|
+
const configPath = resolveConfigPath(opts.config ?? resolveDefaultConfigPath(opts.instance));
|
|
513
|
+
loadOwnlabEnvFile(configPath);
|
|
514
|
+
const cliPackageRoot = findCliPackageRoot(import.meta.url);
|
|
515
|
+
const repoRoot = findRepoRoot(cliPackageRoot);
|
|
516
|
+
const serverPackageRoot = repoRoot ? null : findInstalledServerPackageRoot(import.meta.url);
|
|
517
|
+
const webPackageRoot = repoRoot ? null : findInstalledWebPackageRoot(import.meta.url);
|
|
518
|
+
const results = [];
|
|
519
|
+
const cfg = configCheck(opts.config);
|
|
520
|
+
results.push(cfg);
|
|
521
|
+
printResult(cfg);
|
|
522
|
+
if (cfg.status === "fail") {
|
|
523
|
+
return summarize(results);
|
|
524
|
+
}
|
|
525
|
+
const env = envCheck(opts.config);
|
|
526
|
+
results.push(env);
|
|
527
|
+
printResult(env);
|
|
528
|
+
const node = nodeCheck();
|
|
529
|
+
results.push(node);
|
|
530
|
+
printResult(node);
|
|
531
|
+
const server = await serverCheck({ serverPackageRoot, repoRoot });
|
|
532
|
+
results.push(server);
|
|
533
|
+
printResult(server);
|
|
534
|
+
const web = webCheck({ webPackageRoot, repoRoot });
|
|
535
|
+
results.push(web);
|
|
536
|
+
printResult(web);
|
|
537
|
+
const config = readConfig(opts.config);
|
|
538
|
+
if (!config) {
|
|
539
|
+
return summarize(results);
|
|
540
|
+
}
|
|
541
|
+
for (const result of filesystemCheck(config)) {
|
|
542
|
+
results.push(result);
|
|
543
|
+
printResult(result);
|
|
544
|
+
}
|
|
545
|
+
for (const result of await portChecks(config)) {
|
|
546
|
+
results.push(result);
|
|
547
|
+
printResult(result);
|
|
548
|
+
}
|
|
549
|
+
return summarize(results);
|
|
550
|
+
}
|
|
551
|
+
function summarize(results) {
|
|
552
|
+
const passed = results.filter((result) => result.status === "pass").length;
|
|
553
|
+
const warned = results.filter((result) => result.status === "warn").length;
|
|
554
|
+
const failed = results.filter((result) => result.status === "fail").length;
|
|
555
|
+
process.stdout.write(`Summary: ${passed} passed, ${warned} warnings, ${failed} failed
|
|
556
|
+
`);
|
|
557
|
+
return { passed, warned, failed };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/commands/health.ts
|
|
561
|
+
async function runHealth(opts) {
|
|
562
|
+
const base = opts.url.replace(/\/$/, "");
|
|
563
|
+
const healthUrl = `${base}/health`;
|
|
564
|
+
const res = await fetch(healthUrl, {
|
|
565
|
+
signal: AbortSignal.timeout(8e3)
|
|
566
|
+
});
|
|
567
|
+
const text = await res.text();
|
|
568
|
+
if (!res.ok) {
|
|
569
|
+
process.stderr.write(
|
|
570
|
+
`OwnLab API at ${healthUrl} returned ${res.status}: ${text.slice(0, 500)}
|
|
571
|
+
`
|
|
572
|
+
);
|
|
573
|
+
process.exitCode = 1;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
process.stdout.write(`OK \u2014 ${healthUrl} (${res.status})
|
|
577
|
+
`);
|
|
578
|
+
if (text.trim()) {
|
|
579
|
+
process.stdout.write(`${text.trim()}
|
|
580
|
+
`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/commands/onboard.ts
|
|
585
|
+
import fs6 from "node:fs";
|
|
586
|
+
function toPort(value, fallback) {
|
|
587
|
+
if (value === void 0) return fallback;
|
|
588
|
+
const port = Number(value);
|
|
589
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
590
|
+
throw new Error(`Invalid port: ${value}`);
|
|
591
|
+
}
|
|
592
|
+
return port;
|
|
593
|
+
}
|
|
594
|
+
async function onboard(opts) {
|
|
595
|
+
const instanceId = resolveOwnlabInstanceId(opts.instance);
|
|
596
|
+
const paths = describeLocalInstancePaths(instanceId);
|
|
597
|
+
const serverPort = toPort(opts.serverPort, 3100);
|
|
598
|
+
const webPort = toPort(opts.webPort, 3e3);
|
|
599
|
+
const ownlabHome = resolveOwnlabHomeDir();
|
|
600
|
+
const configPath = opts.config ?? paths.configPath;
|
|
601
|
+
fs6.mkdirSync(paths.instanceRoot, { recursive: true });
|
|
602
|
+
fs6.mkdirSync(paths.logDir, { recursive: true });
|
|
603
|
+
fs6.mkdirSync(paths.backupDir, { recursive: true });
|
|
604
|
+
fs6.mkdirSync(paths.cacheDir, { recursive: true });
|
|
605
|
+
process.env.OWNLAB_HOME = ownlabHome;
|
|
606
|
+
process.env.OWNLAB_INSTANCE_ID = instanceId;
|
|
607
|
+
const config = {
|
|
608
|
+
version: 1,
|
|
609
|
+
server: {
|
|
610
|
+
host: "127.0.0.1",
|
|
611
|
+
port: serverPort
|
|
612
|
+
},
|
|
613
|
+
web: {
|
|
614
|
+
host: "127.0.0.1",
|
|
615
|
+
port: webPort,
|
|
616
|
+
mode: "local-next"
|
|
617
|
+
},
|
|
618
|
+
database: {
|
|
619
|
+
mode: process.env.DATABASE_URL?.trim() ? "postgres" : "embedded-postgres",
|
|
620
|
+
...process.env.DATABASE_URL?.trim() ? { connectionString: process.env.DATABASE_URL.trim() } : {},
|
|
621
|
+
embeddedPostgresDataDir: paths.dbDir,
|
|
622
|
+
embeddedPostgresPort: Number(process.env.OWNLAB_EMBEDDED_PG_PORT) || 54329
|
|
623
|
+
},
|
|
624
|
+
runtime: {
|
|
625
|
+
ownlabHome,
|
|
626
|
+
instanceId,
|
|
627
|
+
logDir: paths.logDir,
|
|
628
|
+
backupDir: paths.backupDir,
|
|
629
|
+
cacheDir: paths.cacheDir
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
writeConfig(config, configPath);
|
|
633
|
+
const envPath = resolveOwnlabEnvFile(configPath);
|
|
634
|
+
mergeOwnlabEnvEntries(
|
|
635
|
+
{
|
|
636
|
+
OWNLAB_HOME: ownlabHome,
|
|
637
|
+
OWNLAB_INSTANCE_ID: instanceId,
|
|
638
|
+
OWNLAB_SERVER_PORT: String(serverPort),
|
|
639
|
+
OWNLAB_SERVER_URL: `http://127.0.0.1:${serverPort}`,
|
|
640
|
+
NEXT_PUBLIC_OWNLAB_SERVER_URL: `http://127.0.0.1:${serverPort}`,
|
|
641
|
+
OWNLAB_WEB_PORT: String(webPort),
|
|
642
|
+
PORT: String(serverPort),
|
|
643
|
+
HOST: "127.0.0.1",
|
|
644
|
+
OWNLAB_EMBEDDED_PG_PORT: String(config.database.embeddedPostgresPort)
|
|
645
|
+
},
|
|
646
|
+
envPath
|
|
647
|
+
);
|
|
648
|
+
process.stdout.write(
|
|
649
|
+
[
|
|
650
|
+
"OwnLab onboarding complete.",
|
|
651
|
+
`Instance: ${instanceId}`,
|
|
652
|
+
`Config: ${configPath}`,
|
|
653
|
+
`Env: ${envPath}`,
|
|
654
|
+
`Next: pnpm ownlab run --instance ${instanceId}`,
|
|
655
|
+
""
|
|
656
|
+
].join("\n")
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/commands/run.ts
|
|
661
|
+
import { spawn } from "node:child_process";
|
|
662
|
+
import path6 from "node:path";
|
|
663
|
+
import { pathToFileURL } from "node:url";
|
|
664
|
+
function waitForUrl(url, timeoutMs = 3e4) {
|
|
665
|
+
const startedAt = Date.now();
|
|
666
|
+
return new Promise((resolve, reject) => {
|
|
667
|
+
const tick = async () => {
|
|
668
|
+
try {
|
|
669
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(2e3) });
|
|
670
|
+
if (response.ok || response.status < 500) {
|
|
671
|
+
resolve();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
677
|
+
reject(new Error(`Timed out waiting for ${url}`));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
setTimeout(tick, 500);
|
|
681
|
+
};
|
|
682
|
+
void tick();
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async function importServerEntry(repoRoot) {
|
|
686
|
+
if (repoRoot) {
|
|
687
|
+
const localEntry = path6.resolve(repoRoot, "apps/server/src/index.ts");
|
|
688
|
+
return await import(pathToFileURL(localEntry).href);
|
|
689
|
+
}
|
|
690
|
+
return await import("@ownlabai/server");
|
|
691
|
+
}
|
|
692
|
+
function spawnWeb(webPackageRoot, repoRoot, env, port) {
|
|
693
|
+
if (hasInstalledWebPackage(webPackageRoot)) {
|
|
694
|
+
const nextBinPath = getInstalledWebNextBinPath(webPackageRoot);
|
|
695
|
+
return spawn("node", [nextBinPath, "start", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
696
|
+
cwd: webPackageRoot,
|
|
697
|
+
env: {
|
|
698
|
+
...env,
|
|
699
|
+
BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: "1"
|
|
700
|
+
},
|
|
701
|
+
stdio: "inherit"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (!repoRoot) {
|
|
705
|
+
throw new Error("Could not find packaged or repo OwnLab web runtime.");
|
|
706
|
+
}
|
|
707
|
+
return spawn(
|
|
708
|
+
"pnpm",
|
|
709
|
+
[
|
|
710
|
+
"--filter",
|
|
711
|
+
"@ownlabai/web",
|
|
712
|
+
"exec",
|
|
713
|
+
"next",
|
|
714
|
+
"dev",
|
|
715
|
+
"--hostname",
|
|
716
|
+
"127.0.0.1",
|
|
717
|
+
"--port",
|
|
718
|
+
String(port)
|
|
719
|
+
],
|
|
720
|
+
{
|
|
721
|
+
cwd: repoRoot,
|
|
722
|
+
env: {
|
|
723
|
+
...env,
|
|
724
|
+
BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: "1"
|
|
725
|
+
},
|
|
726
|
+
stdio: "inherit"
|
|
727
|
+
}
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
async function runCommand(opts) {
|
|
731
|
+
if (opts.instance) {
|
|
732
|
+
process.env.OWNLAB_INSTANCE_ID = opts.instance;
|
|
733
|
+
}
|
|
734
|
+
const configPath = resolveConfigPath(opts.config);
|
|
735
|
+
process.env.OWNLAB_CONFIG = configPath;
|
|
736
|
+
if (!configExists(configPath)) {
|
|
737
|
+
await onboard({
|
|
738
|
+
config: configPath,
|
|
739
|
+
instance: opts.instance,
|
|
740
|
+
yes: true
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
loadOwnlabEnvFile(configPath);
|
|
744
|
+
const summary = await doctor({ config: configPath });
|
|
745
|
+
if (summary.failed > 0) {
|
|
746
|
+
throw new Error("Doctor reported blocking issues.");
|
|
747
|
+
}
|
|
748
|
+
const config = readConfig(configPath);
|
|
749
|
+
if (!config) {
|
|
750
|
+
throw new Error(`No config found at ${configPath}`);
|
|
751
|
+
}
|
|
752
|
+
const env = {
|
|
753
|
+
...process.env,
|
|
754
|
+
OWNLAB_HOME: config.runtime.ownlabHome,
|
|
755
|
+
OWNLAB_INSTANCE_ID: config.runtime.instanceId,
|
|
756
|
+
OWNLAB_SERVER_URL: `http://${config.server.host}:${config.server.port}`,
|
|
757
|
+
NEXT_PUBLIC_OWNLAB_SERVER_URL: `http://${config.server.host}:${config.server.port}`,
|
|
758
|
+
PORT: String(config.server.port),
|
|
759
|
+
HOST: config.server.host,
|
|
760
|
+
OWNLAB_EMBEDDED_PG_PORT: String(config.database.embeddedPostgresPort),
|
|
761
|
+
...config.database.connectionString ? { DATABASE_URL: config.database.connectionString } : {}
|
|
762
|
+
};
|
|
763
|
+
Object.assign(process.env, env);
|
|
764
|
+
const cliPackageRoot = findCliPackageRoot(import.meta.url);
|
|
765
|
+
const repoRoot = findRepoRoot(cliPackageRoot);
|
|
766
|
+
const serverPackageRoot = repoRoot ? null : findInstalledServerPackageRoot(import.meta.url);
|
|
767
|
+
const webPackageRoot = repoRoot ? null : findInstalledWebPackageRoot(import.meta.url);
|
|
768
|
+
const serverModule = await importServerEntry(repoRoot);
|
|
769
|
+
const startedServer = await serverModule.startServer({
|
|
770
|
+
host: config.server.host,
|
|
771
|
+
port: config.server.port
|
|
772
|
+
});
|
|
773
|
+
let webProcess = null;
|
|
774
|
+
let stopping = false;
|
|
775
|
+
const stopAll = async () => {
|
|
776
|
+
if (stopping) return;
|
|
777
|
+
stopping = true;
|
|
778
|
+
if (webProcess?.pid) {
|
|
779
|
+
webProcess.kill("SIGTERM");
|
|
780
|
+
}
|
|
781
|
+
await startedServer.stop();
|
|
782
|
+
};
|
|
783
|
+
process.on("SIGINT", () => {
|
|
784
|
+
void stopAll().finally(() => process.exit(0));
|
|
785
|
+
});
|
|
786
|
+
process.on("SIGTERM", () => {
|
|
787
|
+
void stopAll().finally(() => process.exit(0));
|
|
788
|
+
});
|
|
789
|
+
process.stdout.write(`OwnLab API: ${startedServer.apiUrl}
|
|
790
|
+
`);
|
|
791
|
+
if (opts.noWeb) {
|
|
792
|
+
process.stdout.write("Web startup skipped (--no-web).\n");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
webProcess = spawnWeb(webPackageRoot, repoRoot, env, config.web.port);
|
|
796
|
+
webProcess.on("exit", (code) => {
|
|
797
|
+
if (!stopping) {
|
|
798
|
+
process.stderr.write(`OwnLab web exited with code ${code ?? 0}
|
|
799
|
+
`);
|
|
800
|
+
void stopAll().finally(() => process.exit(code ?? 1));
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
const webUrl = `http://${config.web.host}:${config.web.port}`;
|
|
804
|
+
await waitForUrl(webUrl).catch((error) => {
|
|
805
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
806
|
+
`);
|
|
807
|
+
});
|
|
808
|
+
process.stdout.write(`OwnLab Web: ${webUrl}
|
|
809
|
+
`);
|
|
810
|
+
process.stdout.write("Press Ctrl+C to stop OwnLab.\n");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/index.ts
|
|
814
|
+
var __dirname = dirname(fileURLToPath3(import.meta.url));
|
|
815
|
+
var pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
|
|
816
|
+
var program = new Command();
|
|
817
|
+
program.name("ownlab").description("OwnLab CLI \u2014 operate and diagnose your local lab").version(pkg.version);
|
|
818
|
+
program.command("health").description("Check that the OwnLab API server responds (GET /health)").option(
|
|
819
|
+
"-u, --url <baseUrl>",
|
|
820
|
+
"API base URL",
|
|
821
|
+
process.env.OWNLAB_API_URL ?? "http://localhost:3100"
|
|
822
|
+
).action(async (opts) => {
|
|
823
|
+
await runHealth({ url: opts.url });
|
|
824
|
+
});
|
|
825
|
+
program.command("info").description("Print a short product hint (uses shared constants)").action(() => {
|
|
826
|
+
const sample = Object.entries(ADAPTER_TYPE_LABELS).slice(0, 3);
|
|
827
|
+
process.stdout.write(
|
|
828
|
+
`Adapter labels (sample): ${sample.map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
829
|
+
`
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
program.command("onboard").description("Create a local OwnLab instance config and default directories").option("-c, --config <path>", "Path to config file").option("-i, --instance <id>", "OwnLab instance id", "default").option("-y, --yes", "Accept defaults without prompts", false).option("--server-port <port>", "Server port", "3100").option("--web-port <port>", "Web port", "3000").action(async (opts) => {
|
|
833
|
+
await onboard(opts);
|
|
834
|
+
});
|
|
835
|
+
program.command("doctor").description("Run local OwnLab runtime checks").option("-c, --config <path>", "Path to config file").option("-i, --instance <id>", "OwnLab instance id", "default").action(async (opts) => {
|
|
836
|
+
const summary = await doctor(opts);
|
|
837
|
+
if (summary.failed > 0) {
|
|
838
|
+
process.exitCode = 1;
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
program.command("run").description("Ensure onboarding is complete and start the local OwnLab runtime").option("-c, --config <path>", "Path to config file").option("-i, --instance <id>", "OwnLab instance id", "default").option("--no-web", "Start only the server runtime").action(async (opts) => {
|
|
842
|
+
await runCommand({
|
|
843
|
+
config: opts.config,
|
|
844
|
+
instance: opts.instance,
|
|
845
|
+
noWeb: opts.web === false
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
program.parseAsync().catch((error) => {
|
|
849
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
850
|
+
`);
|
|
851
|
+
process.exit(1);
|
|
852
|
+
});
|
|
853
|
+
//# sourceMappingURL=index.js.map
|