schornai 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # schornai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The schorn.ai command-line application. The public npm package and executable are
4
+ both named `schornai`; service implementation remains in the separate private
5
+ workspace package `@schornio/aibox-package-service`.
6
+
7
+ Requires Node.js 24 or newer. This source adds automatic setup and service commands for the next
8
+ CLI release; publishing the new version is a separate release action.
9
+
10
+ ```sh
11
+ npx schornai
12
+ ```
13
+
14
+ Without arguments, the CLI reports an existing installation and its service
15
+ status. Otherwise it asks `Install schorn.ai ...? [y/N]` before downloading or
16
+ changing files. Only `y` (case-insensitive) proceeds; `n`, Enter, end of input or
17
+ Ctrl+C cancels. Retrying an incomplete installation asks again.
18
+ After confirmation it downloads the latest XRAG setup from
19
+ `https://schorn.ai/xrag/schornai/latest`, validates the archive and platform,
20
+ installs it and starts the service. The endpoint is still being prepared;
21
+ an unavailable download exits with an error and can be retried.
22
+ The bootstrap does not send a platform or architecture to that endpoint, so
23
+ it can currently serve only one target. A macOS ARM64 bundle cannot install
24
+ Linux; Linux needs a separately built bundle for the matching architecture.
25
+
26
+ New machines get `core`, `supabase` and `document-extractor`; Apple Silicon Macs
27
+ also get `ollama` and `transcriber-mlx`. Setup creates private instance credentials
28
+ and a self-signed localhost TLS certificate. OpenSSL is required. Linux ARM64/x64
29
+ also needs a running Docker Engine and Compose 2.24.4+; the Mac bundle supplies
30
+ Docker/Colima. Other platforms are rejected before downloading. Models, mail
31
+ delivery and a public domain/certificate are configured separately.
32
+
33
+ The default installation directory is `~/.aibox`; `SCHORNAI_HOME` changes it and
34
+ `SCHORNAI_CONFIG` selects an explicit config file for this default command.
35
+ Existing service configurations and credentials are preserved, including a
36
+ configuration created with `service init` before installing. Such machines keep
37
+ their selected services and must already have their required credentials.
38
+ Legacy Admin installations are reported without migration. A failed setup remains
39
+ retryable with the same command; existing installations are never automatically
40
+ updated or restarted. `--help` and `--version` do not install anything.
41
+
42
+ For explicit service management:
43
+
44
+ ```sh
45
+ npx schornai service init --services core,supabase,document-extractor,ollama,transcriber-mlx
46
+ npx schornai service start
47
+ npx schornai service sync
48
+ npx schornai service status
49
+ npx schornai service logs --follow
50
+ npx schornai service stop
51
+ ```
52
+
53
+ `init` creates a machine configuration without overwriting an existing one.
54
+ Service binaries and Docker images are installed by the XRAG setup bundle.
55
+ Use `--config PATH` to select another machine directory. Run
56
+ `npx schornai service --help` for the full command list.
57
+
58
+ ## Local development
59
+
60
+ From the repository root:
61
+
62
+ ```sh
63
+ npm run build:cli
64
+ npm start --workspace apps/cli -- service --help
65
+ ```
66
+
67
+ The build compiles `packages/service` and includes that runtime in the CLI's
68
+ `dist/service` directory. The published package needs no private registry or
69
+ repository checkout. The Mac launcher and XRAG installer use the same service
70
+ build independently of the public CLI.
71
+
72
+ ## Packaging and release
73
+
74
+ ```sh
75
+ npm pack --workspace apps/cli
76
+ ```
77
+
78
+ `prepack` builds the runtime. Review the package contents before publishing with
79
+ an authorized npm account. The service workspace itself is private and does
80
+ not claim the `schornai` npm name. Increase `apps/cli/package.json`'s version and
81
+ update the lockfile for subsequent releases.
82
+
83
+ See the [operator guide](../../docs/de/box-admin/index.md) for service selection,
84
+ credentials and automatic migration of old Admin installations through XRAG.
85
+
86
+ `service sync` asks the running daemon to reconcile artifacts immediately.
87
+ Sharing must be configured; the command reports that work was queued, while
88
+ `service logs --follow` and the web app's **Service** page show the result.
89
+ Automatic synchronization uses Yjs notifications and reconnects, not polling.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const help = `schorn.ai CLI
5
+
6
+ Usage: schornai
7
+ schornai service <init|start|stop|restart|status|sync|logs|run> [options]
8
+ schornai service --help
9
+
10
+ Without arguments, show the installed machine's status or ask to install schorn.ai.
11
+
12
+ News: https://schorn.ai/newsletter`;
13
+
14
+ try {
15
+ if (!args.length) {
16
+ const { runDefaultCLI } = await import("../dist/service/bootstrap.mjs");
17
+ await runDefaultCLI();
18
+ } else if (args[0] === "service") {
19
+ const { runServiceCLI } = await import("../dist/service/commands.mjs");
20
+ await runServiceCLI(args);
21
+ } else if (args[0] === "--help" || args[0] === "-h") {
22
+ console.log(help);
23
+ } else if (args[0] === "--version") {
24
+ const { readFile } = await import("node:fs/promises");
25
+ console.log(
26
+ JSON.parse(
27
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
28
+ ).version,
29
+ );
30
+ } else {
31
+ throw new Error(`Unknown command: ${args[0]}\n${help}`);
32
+ }
33
+ } catch (error) {
34
+ console.error(error instanceof Error ? error.message : "Command failed");
35
+ process.exitCode = 1;
36
+ }
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+ import { createHmac, createHash } from 'node:crypto';
3
+ import { createWriteStream } from 'node:fs';
4
+ import { stat, mkdir, writeFile, mkdtemp, chmod, rename, rm, readFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { createInterface } from 'node:readline/promises';
8
+ import { Transform } from 'node:stream';
9
+ import { pipeline } from 'node:stream/promises';
10
+ import { r as runServiceCLI, e as extractArchive } from './shared/cli-BxEHldVH.mjs';
11
+ import { i as initConfig, w as writeJson, l as loadConfig, r as resolveFrom, o as object, a as literal, d as defaultConfigPath, s as string, c as createLogger, b as run, e as binary, n as number, f as boolean } from './shared/docker-Dj7cdbkw.mjs';
12
+ import { g as generateSecuritySecrets, h as hasLegacyLayout } from './shared/installer-CWopk_6Z.mjs';
13
+ import { execFile } from 'node:child_process';
14
+ import { promisify } from 'node:util';
15
+ import 'node:timers/promises';
16
+ import 'node:http';
17
+ import 'events';
18
+ import 'fs';
19
+ import 'node:events';
20
+ import 'node:string_decoder';
21
+ import 'path';
22
+ import 'assert';
23
+ import 'buffer';
24
+ import 'zlib';
25
+ import 'node:assert';
26
+ import 'node:readline';
27
+
28
+ // FROM https://github.com/supabase/supabase/blob/794529eddf3e9d454fa2bbc169768bfd2a853d67/apps/docs/components/JwtGenerator/JwtGenerator.tsx
29
+ function generateRandomString(length) {
30
+ const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
31
+ let result = '';
32
+ /**
33
+ * The number of possible Uint8 integers is 256. Since the length of `CHARS`
34
+ * doesn't fit exactly into 256, simply taking the modulus would create an
35
+ * uneven distribution that favors the earlier characters. To make a truly
36
+ * uniform distribution, we have to discard everything above the last full
37
+ * cycle, and pick again.
38
+ *
39
+ * The minus 1 is to account for 0-indexing.
40
+ */
41
+ const MAX = Math.floor(256 / CHARS.length) * CHARS.length - 1;
42
+ const randomUInt8Array = new Uint8Array(1);
43
+ for (let i = 0; i < length; i++) {
44
+ let randomNumber;
45
+ do {
46
+ crypto.getRandomValues(randomUInt8Array);
47
+ randomNumber = randomUInt8Array[0];
48
+ /**
49
+ * Keep picking until we get a number in the valid range.
50
+ */
51
+ } while (randomNumber > MAX);
52
+ result += CHARS[randomNumber % CHARS.length];
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /** Generates the legacy HS256 API keys expected by self-hosted Supabase. */
58
+ function generateSupabaseKey(type, jwtSecret) {
59
+ if (jwtSecret.length < 32)
60
+ throw new Error("JWT_SECRET must contain at least 32 characters");
61
+ if (type !== "ANON_KEY" && type !== "SERVICE_ROLE_KEY")
62
+ throw new Error("Invalid Supabase key type");
63
+ const now = new Date();
64
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
65
+ const fiveYears = new Date(now.getFullYear() + 5, now.getMonth(), now.getDate());
66
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
67
+ const payload = Buffer.from(JSON.stringify({
68
+ role: type === "ANON_KEY" ? "anon" : "service_role",
69
+ iss: "supabase",
70
+ iat: Math.floor(today.valueOf() / 1000),
71
+ exp: Math.floor(fiveYears.valueOf() / 1000),
72
+ })).toString("base64url");
73
+ const signed = `${header}.${payload}`;
74
+ const signature = createHmac("sha256", jwtSecret)
75
+ .update(signed)
76
+ .digest("base64url");
77
+ return `${signed}.${signature}`;
78
+ }
79
+
80
+ // FROM https://github.com/supabase/supabase/blob/794529eddf3e9d454fa2bbc169768bfd2a853d67/apps/docs/components/JwtGenerator/JwtGenerator.tsx
81
+ function generateDockerEnv({ boxId = crypto.randomUUID(), samlPrivateKey, siteUrl = "http://replace-with-actual-site-url", }) {
82
+ const BOX_ID = boxId;
83
+ const POSTGRES_PASSWORD = generateRandomString(64);
84
+ const JWT_SECRET = generateRandomString(40);
85
+ const ANON_KEY = generateSupabaseKey("ANON_KEY", JWT_SECRET);
86
+ const SERVICE_ROLE_KEY = generateSupabaseKey("SERVICE_ROLE_KEY", JWT_SECRET);
87
+ const SECRET_KEY_BASE = generateRandomString(64);
88
+ const VAULT_ENC_KEY = generateRandomString(32);
89
+ const SAML_PRIVATE_KEY = samlPrivateKey;
90
+ const { REALTIME_SERVER_SECRET, SUPABASE_AUTH_HOOK_SECRET, SERVICE_API_TOKEN, } = generateSecuritySecrets();
91
+ return `
92
+ BOX_ID=${BOX_ID}
93
+
94
+ ############
95
+ # Secrets
96
+ ############
97
+
98
+ POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
99
+ JWT_SECRET=${JWT_SECRET}
100
+ ANON_KEY=${ANON_KEY}
101
+ SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
102
+ SECRET_KEY_BASE=${SECRET_KEY_BASE}
103
+ VAULT_ENC_KEY=${VAULT_ENC_KEY}
104
+ SAML_PRIVATE_KEY=${SAML_PRIVATE_KEY}
105
+ REALTIME_SERVER_SECRET=${REALTIME_SERVER_SECRET}
106
+ SUPABASE_AUTH_HOOK_SECRET=${SUPABASE_AUTH_HOOK_SECRET}
107
+ SERVICE_API_TOKEN=${SERVICE_API_TOKEN}
108
+
109
+ ############
110
+ # Database - You can change these to any PostgreSQL database that has logical replication enabled.
111
+ ############
112
+
113
+ POSTGRES_HOST=db
114
+ POSTGRES_DB=postgres
115
+ POSTGRES_PORT=5432
116
+ # default user is postgres
117
+
118
+ ############
119
+ # Supavisor -- Database pooler
120
+ ############
121
+ POOLER_PROXY_PORT_TRANSACTION=6543
122
+ POOLER_DEFAULT_POOL_SIZE=20
123
+ POOLER_MAX_CLIENT_CONN=100
124
+ POOLER_TENANT_ID=schornai-box-v1
125
+
126
+
127
+ ############
128
+ # API Proxy - Configuration for the Kong Reverse proxy.
129
+ ############
130
+
131
+ KONG_HTTP_PORT=80
132
+ KONG_HTTPS_PORT=443
133
+
134
+
135
+ ############
136
+ # API - Configuration for PostgREST.
137
+ ############
138
+
139
+ PGRST_DB_SCHEMAS=public,storage,graphql_public,pgmq_public
140
+
141
+
142
+ ############
143
+ # Auth - Configuration for the GoTrue authentication server.
144
+ ############
145
+
146
+ ## General
147
+ SITE_URL=${siteUrl}
148
+ ADDITIONAL_REDIRECT_URLS=ai.schorn.box-app-v1://**
149
+ JWT_EXPIRY=3600
150
+ DISABLE_SIGNUP=false
151
+ API_EXTERNAL_URL=${siteUrl}/auth/v1
152
+
153
+ ## Mailer Config
154
+ MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
155
+ MAILER_URLPATHS_INVITE="/auth/v1/verify"
156
+ MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
157
+ MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
158
+
159
+ ## Email auth
160
+ ENABLE_EMAIL_SIGNUP=true
161
+ ENABLE_EMAIL_AUTOCONFIRM=false
162
+ SMTP_ADMIN_EMAIL=admin@example.com
163
+ SMTP_HOST=supabase-mail
164
+ SMTP_PORT=2500
165
+ SMTP_USER=fake_mail_user
166
+ SMTP_PASS=fake_mail_password
167
+ SMTP_SENDER_NAME=fake_sender
168
+ ENABLE_ANONYMOUS_USERS=false
169
+
170
+ ## Phone auth
171
+ ENABLE_PHONE_SIGNUP=false
172
+ ENABLE_PHONE_AUTOCONFIRM=true
173
+
174
+
175
+ ############
176
+ # Studio - Configuration for the Dashboard
177
+ ############
178
+
179
+ STUDIO_DEFAULT_ORGANIZATION=Default Organization
180
+ STUDIO_DEFAULT_PROJECT=Default Project
181
+
182
+ STUDIO_PORT=3000
183
+ # replace if you intend to use Studio outside of localhost
184
+ SUPABASE_PUBLIC_URL=http://localhost:8000
185
+
186
+ # Enable webp support
187
+ IMGPROXY_ENABLE_WEBP_DETECTION=true
188
+
189
+ # Add your OpenAI API key to enable SQL Editor Assistant
190
+ OPENAI_API_KEY=
191
+
192
+ ############
193
+ # Functions - Configuration for Functions
194
+ ############
195
+ # NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet.
196
+ FUNCTIONS_VERIFY_JWT=false
197
+ `;
198
+ }
199
+
200
+ const execute = promisify(execFile);
201
+ async function fileExists(path) {
202
+ try {
203
+ await stat(path);
204
+ return true;
205
+ }
206
+ catch (error) {
207
+ if (error.code !== "ENOENT")
208
+ throw error;
209
+ return false;
210
+ }
211
+ }
212
+ function initialServices(platform, arch) {
213
+ if (!(platform === "darwin" && arch === "arm64") &&
214
+ !(platform === "linux" && ["arm64", "x64"].includes(arch)))
215
+ throw new Error(`No schorn.ai setup is supported for ${platform}/${arch}`);
216
+ return [
217
+ "core",
218
+ "supabase",
219
+ "document-extractor",
220
+ ...(platform === "darwin" ? ["ollama", "transcriber-mlx"] : []),
221
+ ];
222
+ }
223
+ /** Only for a new local instance owned by the bootstrap, never remote workers. */
224
+ async function prepareInitialSetup(configPath, signal) {
225
+ signal?.throwIfAborted();
226
+ if (!(await fileExists(configPath))) {
227
+ const config = await initConfig(configPath, initialServices(process.platform, process.arch));
228
+ await writeJson(configPath, {
229
+ ...config,
230
+ sharing: { url: "http://127.0.0.1:3001" },
231
+ });
232
+ }
233
+ const { config, root } = await loadConfig(configPath);
234
+ const envPath = resolveFrom(root, config.envFile);
235
+ if (!(await fileExists(envPath))) {
236
+ const { stdout } = await execute("openssl", ["genpkey", "-algorithm", "rsa", "-outform", "DER"], { encoding: "buffer", signal });
237
+ await mkdir(dirname(envPath), { recursive: true, mode: 0o700 });
238
+ await writeFile(envPath, generateDockerEnv({
239
+ boxId: config.id,
240
+ samlPrivateKey: stdout.toString("base64"),
241
+ siteUrl: "https://localhost",
242
+ }), { flag: "wx", mode: 0o600 });
243
+ }
244
+ const cert = join(root, "docker/volumes/api/cert");
245
+ const key = join(cert, "privkey.pem");
246
+ const certificate = join(cert, "fullkey.pem");
247
+ const hasKey = await fileExists(key);
248
+ const hasCertificate = await fileExists(certificate);
249
+ if (hasKey !== hasCertificate)
250
+ throw new Error(`Incomplete TLS certificate pair in ${cert}; restore the matching pair before retrying`);
251
+ if (hasKey)
252
+ return;
253
+ // Generate outside the destination so an OpenSSL failure leaves no partial pair.
254
+ await mkdir(dirname(cert), { recursive: true, mode: 0o700 });
255
+ const stagedCert = await mkdtemp(join(dirname(cert), ".cert-"));
256
+ try {
257
+ const stagedKey = join(stagedCert, "privkey.pem");
258
+ await execute("openssl", [
259
+ "req",
260
+ "-x509",
261
+ "-nodes",
262
+ "-newkey",
263
+ "rsa:2048",
264
+ "-keyout",
265
+ stagedKey,
266
+ "-out",
267
+ join(stagedCert, "fullkey.pem"),
268
+ "-days",
269
+ "365",
270
+ "-subj",
271
+ "/CN=localhost",
272
+ "-addext",
273
+ "subjectAltName=DNS:localhost,IP:127.0.0.1",
274
+ ], { signal });
275
+ await chmod(stagedKey, 0o600);
276
+ signal?.throwIfAborted();
277
+ await rename(stagedCert, cert);
278
+ }
279
+ finally {
280
+ await rm(stagedCert, { recursive: true, force: true });
281
+ }
282
+ }
283
+
284
+ const latestSetupUrl = "https://schorn.ai/xrag/schornai/latest";
285
+ const pendingSchema = object({ version: literal(1), fresh: boolean() });
286
+ async function confirmInstallation(root) {
287
+ const readline = createInterface({
288
+ input: process.stdin,
289
+ output: process.stdout,
290
+ });
291
+ const controller = new AbortController();
292
+ const cancel = () => controller.abort();
293
+ readline.once("close", cancel);
294
+ readline.once("SIGINT", cancel);
295
+ try {
296
+ const answer = await readline.question(`Install schorn.ai in ${root} and start its services? [y/N] `, { signal: controller.signal });
297
+ return answer.trim().toLowerCase() === "y";
298
+ }
299
+ catch (error) {
300
+ if (controller.signal.aborted)
301
+ return false;
302
+ throw error;
303
+ }
304
+ finally {
305
+ readline.close();
306
+ }
307
+ }
308
+ async function downloadSetup(file, abort) {
309
+ let url = new URL(latestSetupUrl);
310
+ const signal = AbortSignal.any([abort, AbortSignal.timeout(3_600_000)]);
311
+ for (let redirects = 0; redirects <= 5; redirects++) {
312
+ if (url.protocol !== "https:" || url.username || url.password)
313
+ throw new Error("Setup downloads require HTTPS without URL credentials");
314
+ const response = await fetch(url.href, { redirect: "manual", signal });
315
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
316
+ await response.body?.cancel();
317
+ const location = response.headers.get("location");
318
+ if (!location)
319
+ throw new Error("Setup download redirect has no location");
320
+ url = new URL(location, url);
321
+ continue;
322
+ }
323
+ if (!response.ok || !response.body) {
324
+ await response.body?.cancel();
325
+ throw new Error(`Setup download failed (HTTP ${response.status}) at ${latestSetupUrl}; retry when the setup bundle is available`);
326
+ }
327
+ const hash = createHash("sha256");
328
+ await pipeline(response.body, new Transform({
329
+ transform(chunk, _encoding, callback) {
330
+ hash.update(chunk);
331
+ callback(null, chunk);
332
+ },
333
+ }), createWriteStream(file, { flags: "wx", mode: 0o600 }), { signal });
334
+ return hash.digest("hex");
335
+ }
336
+ throw new Error("Too many setup download redirects");
337
+ }
338
+ async function acquireSetupLock(root) {
339
+ const lock = join(root, ".setup-lock");
340
+ try {
341
+ await mkdir(lock, { mode: 0o700 });
342
+ }
343
+ catch (error) {
344
+ if (error.code !== "EEXIST")
345
+ throw error;
346
+ let pid;
347
+ try {
348
+ pid = object({ pid: number().int().positive() })
349
+ .parse(JSON.parse(await readFile(join(lock, "owner.json"), "utf8"))).pid;
350
+ }
351
+ catch {
352
+ throw new Error(`Another setup is starting, or ${lock} needs manual recovery`);
353
+ }
354
+ try {
355
+ process.kill(pid, 0);
356
+ throw new Error("Another schorn.ai setup is already running");
357
+ }
358
+ catch (probe) {
359
+ if (probe.code !== "ESRCH")
360
+ throw probe;
361
+ }
362
+ await rm(lock, { recursive: true });
363
+ await mkdir(lock, { mode: 0o700 });
364
+ }
365
+ await writeJson(join(lock, "owner.json"), { pid: process.pid });
366
+ return lock;
367
+ }
368
+ async function runDefaultCLI() {
369
+ const configPath = resolve(process.env.SCHORNAI_CONFIG ?? defaultConfigPath());
370
+ const root = dirname(configPath);
371
+ const pendingFile = join(root, ".setup-incomplete.json");
372
+ const pending = (await fileExists(pendingFile))
373
+ ? pendingSchema.parse(JSON.parse(await readFile(pendingFile, "utf8")))
374
+ : undefined;
375
+ const configured = await fileExists(configPath);
376
+ const context = configured ? await loadConfig(configPath) : undefined;
377
+ const runtime = join(root, "apps/service/cli.mjs");
378
+ if (!pending &&
379
+ (context?.config.development || (context && (await fileExists(runtime))))) {
380
+ console.log(`schorn.ai is already installed in ${root}`);
381
+ await runServiceCLI(["service", "status", "--config", configPath]);
382
+ return;
383
+ }
384
+ if (!pending && (await hasLegacyLayout(root))) {
385
+ console.log(`schorn.ai is already installed in ${root} (legacy Admin installation). Use the Admin app for its status.`);
386
+ return;
387
+ }
388
+ if (!configured &&
389
+ !pending &&
390
+ ((await fileExists(runtime)) ||
391
+ (await fileExists(join(root, "docker.env"))) ||
392
+ (await fileExists(join(root, "docker")))))
393
+ throw new Error(`Existing schorn.ai files in ${root} have no machine configuration. Restore config.json before continuing.`);
394
+ initialServices(process.platform, process.arch);
395
+ if (!(await confirmInstallation(root))) {
396
+ console.log("Installation cancelled.");
397
+ return;
398
+ }
399
+ await mkdir(root, { recursive: true, mode: 0o700 });
400
+ const lock = await acquireSetupLock(root);
401
+ const controller = new AbortController();
402
+ const interrupted = () => controller.abort(new Error("Setup interrupted; run npx schornai to retry"));
403
+ process.on("SIGINT", interrupted);
404
+ process.on("SIGTERM", interrupted);
405
+ const { signal } = controller;
406
+ let temporary;
407
+ try {
408
+ temporary = await mkdtemp(join(tmpdir(), "schornai-setup-"));
409
+ console.log(`Downloading schorn.ai setup from ${latestSetupUrl}`);
410
+ const archive = join(temporary, "setup.tar");
411
+ const id = await downloadSetup(archive, signal);
412
+ const payload = join(temporary, "payload");
413
+ await mkdir(payload, { mode: 0o700 });
414
+ const inspected = await extractArchive(archive, payload, id, signal);
415
+ if (inspected.xrag.xrag_type !== "setup")
416
+ throw new Error("The latest schorn.ai download is not an XRAG setup bundle");
417
+ const manifest = object({ version: literal(1), platform: string(), arch: string() })
418
+ .parse(JSON.parse(await readFile(join(payload, "installation.json"), "utf8")));
419
+ if (manifest.platform !== process.platform ||
420
+ manifest.arch !== process.arch ||
421
+ (inspected.xrag.platform &&
422
+ inspected.xrag.platform !== manifest.platform) ||
423
+ (inspected.xrag.arch && inspected.xrag.arch !== manifest.arch))
424
+ throw new Error(`The setup bundle does not match this machine (${process.platform}/${process.arch})`);
425
+ const fresh = pending?.fresh ?? !configured;
426
+ signal.throwIfAborted();
427
+ await writeJson(pendingFile, { version: 1, fresh });
428
+ if (fresh)
429
+ await prepareInitialSetup(configPath, signal);
430
+ const installation = await loadConfig(configPath);
431
+ const log = createLogger(root, installation.environment, true);
432
+ console.log(`Installing schorn.ai in ${root}`);
433
+ await run({
434
+ name: "setup",
435
+ command: "bash",
436
+ args: [join(payload, "setup.sh")],
437
+ cwd: payload,
438
+ env: {
439
+ ...process.env,
440
+ SCHORNAI_HOME: root,
441
+ SCHORNAI_CONFIG: configPath,
442
+ SCHORNAI_NODE: process.execPath,
443
+ SCHORNAI_SERVICE_UPDATE: "1",
444
+ },
445
+ }, log, signal);
446
+ if (!(await fileExists(runtime)))
447
+ throw new Error("Setup did not install the schorn.ai service runtime");
448
+ await rm(pendingFile);
449
+ console.log("schorn.ai installed successfully");
450
+ await run({
451
+ name: "service",
452
+ command: binary(root, "node", process.execPath),
453
+ args: [runtime, "service", "start", "--config", configPath],
454
+ env: process.env,
455
+ }, log, signal);
456
+ await runServiceCLI(["service", "status", "--config", configPath]);
457
+ if (fresh)
458
+ console.log("Open https://localhost (local self-signed certificate).");
459
+ }
460
+ finally {
461
+ try {
462
+ if (temporary)
463
+ await rm(temporary, { recursive: true, force: true });
464
+ }
465
+ finally {
466
+ try {
467
+ await rm(lock, { recursive: true, force: true });
468
+ }
469
+ finally {
470
+ process.removeListener("SIGINT", interrupted);
471
+ process.removeListener("SIGTERM", interrupted);
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ export { latestSetupUrl, runDefaultCLI };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { r as runServiceCLI } from './shared/cli-BxEHldVH.mjs';
3
+ import 'node:util';
4
+ import 'node:fs';
5
+ import 'node:fs/promises';
6
+ import 'node:path';
7
+ import 'node:child_process';
8
+ import 'node:timers/promises';
9
+ import './shared/docker-Dj7cdbkw.mjs';
10
+ import 'node:os';
11
+ import 'node:crypto';
12
+ import 'node:http';
13
+ import 'node:readline';
14
+ import 'node:stream/promises';
15
+ import 'node:stream';
16
+ import 'events';
17
+ import 'fs';
18
+ import 'node:events';
19
+ import 'node:string_decoder';
20
+ import 'path';
21
+ import 'assert';
22
+ import 'buffer';
23
+ import 'zlib';
24
+ import 'node:assert';
25
+
26
+ runServiceCLI().catch((error) => {
27
+ console.error(error instanceof Error ? error.message : "Service command failed");
28
+ process.exitCode = 1;
29
+ });
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import 'node:util';
3
+ import 'node:fs';
4
+ import 'node:fs/promises';
5
+ import 'node:path';
6
+ import 'node:child_process';
7
+ import 'node:timers/promises';
8
+ import './shared/docker-Dj7cdbkw.mjs';
9
+ export { r as runServiceCLI } from './shared/cli-BxEHldVH.mjs';
10
+ import 'node:os';
11
+ import 'node:crypto';
12
+ import 'node:http';
13
+ import 'node:readline';
14
+ import 'node:stream/promises';
15
+ import 'node:stream';
16
+ import 'events';
17
+ import 'fs';
18
+ import 'node:events';
19
+ import 'node:string_decoder';
20
+ import 'path';
21
+ import 'assert';
22
+ import 'buffer';
23
+ import 'zlib';
24
+ import 'node:assert';
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import 'node:fs/promises';
3
+ import 'node:fs';
4
+ import 'node:os';
5
+ import 'node:path';
6
+ import 'node:util';
7
+ import 'node:crypto';
8
+ import './shared/docker-Dj7cdbkw.mjs';
9
+ export { h as hasLegacyLayout, i as installUpdate } from './shared/installer-CWopk_6Z.mjs';
10
+ import 'node:timers/promises';
11
+ import 'node:http';
12
+ import 'node:child_process';
13
+ import 'node:readline';
14
+ import 'node:events';