scriptorium 0.0.1 → 0.2.1

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.
Files changed (102) hide show
  1. package/README.md +31 -8
  2. package/bin/scriptorium.js +1 -1
  3. package/build/client/assets/_layout-4p0zIumj.js +1 -0
  4. package/build/client/assets/_layout-CG4jvKXd.js +1 -0
  5. package/build/client/assets/_layout-DUvno75Q.js +1 -0
  6. package/build/client/assets/_layout-tj841ZFj.js +3 -0
  7. package/build/client/assets/auth-shell-CM4lzbUg.js +1 -0
  8. package/build/client/assets/breadcrumbs-CLMk50vh.js +1 -0
  9. package/build/client/assets/{chunk-EPOLDU6W-B-j6nV8T.js → chunk-EPOLDU6W-BT1NqV1s.js} +11 -11
  10. package/build/client/assets/confirm-passkey-sqNHrDN7.js +1 -0
  11. package/build/client/assets/document-title-CaFCBnQ-.js +1 -0
  12. package/build/client/assets/entry.client-B2qwDNf_.js +13 -0
  13. package/build/client/assets/events-BGAz2o24.js +39 -0
  14. package/build/client/assets/files-247Zppti.js +1 -0
  15. package/build/client/assets/files-COflIyot.js +1 -0
  16. package/build/client/assets/files-browser-DKxCza7c.js +1 -0
  17. package/build/client/assets/files.browse-BBMm6lgA.js +1 -0
  18. package/build/client/assets/files.browse-Dq0nnEAB.js +1 -0
  19. package/build/client/assets/index-BumTGMZF.js +1 -0
  20. package/build/client/assets/index-CLO24mr0.js +1 -0
  21. package/build/client/assets/index-D-coH8u1.js +34 -0
  22. package/build/client/assets/index-DxuBmPz-.js +1 -0
  23. package/build/client/assets/jsx-runtime-u17CrQMm.js +1 -0
  24. package/build/client/assets/login-SMI1sC8T.js +1 -0
  25. package/build/client/assets/manifest-27a6038b.js +1 -0
  26. package/build/client/assets/passkeys-Cg-Qx1Gj.js +1 -0
  27. package/build/client/assets/project-events-provider-Cz1h_7Z0.js +1 -0
  28. package/build/client/assets/project-route-Cqnw3iG7.js +1 -0
  29. package/build/client/assets/projects-CcOVpbnV.js +1 -0
  30. package/build/client/assets/projects._projectId.proxy._-l0sNRNKZ.js +1 -0
  31. package/build/client/assets/projects.new-BIAajDmU.js +1 -0
  32. package/build/client/assets/register-CwUWzMvC.js +1 -0
  33. package/build/client/assets/review-browser-CkLIZsiU.js +1 -0
  34. package/build/client/assets/review._mode-CjiM88Yw.js +1 -0
  35. package/build/client/assets/review.uncommitted-DnCOhh-l.js +1 -0
  36. package/build/client/assets/root-DtlaHd7Y.js +1 -0
  37. package/build/client/assets/root-h1ZnuAzH.css +1 -0
  38. package/build/client/assets/{scroll-indicator-CFaTM_rb.js → scroll-indicator-DxagqbkL.js} +8 -8
  39. package/build/client/assets/scrollable-layout-CR2OL0dM.js +1 -0
  40. package/build/client/assets/session-route-Cd_9l4G7.js +1 -0
  41. package/build/client/assets/sessions-provider-xySz_P7U.js +1 -0
  42. package/build/client/assets/settings-CYavUXKt.js +1 -0
  43. package/build/client/assets/sidebar-BpDMNa9u.js +1 -0
  44. package/build/client/assets/use-double-check-BJPDnzvf.js +1 -0
  45. package/build/runtime/app/lib/runtime-config/cache.server.js +15 -0
  46. package/build/runtime/app/lib/runtime-config/cli.server.js +79 -0
  47. package/build/runtime/app/lib/runtime-config/docs.server.js +170 -0
  48. package/build/runtime/app/lib/runtime-config/loader.server.js +69 -0
  49. package/build/runtime/app/lib/runtime-config/schema.server.js +128 -0
  50. package/build/runtime/server/index.js +176 -0
  51. package/build/server/index.js +8989 -4661
  52. package/docs/config.md +12 -8
  53. package/drizzle/20260324172031_curved_junta/migration.sql +20 -0
  54. package/drizzle/20260324172031_curved_junta/snapshot.json +684 -0
  55. package/drizzle/20260327233106_model_usage/migration.sql +15 -0
  56. package/drizzle/20260327233106_model_usage/snapshot.json +830 -0
  57. package/drizzle/20260331000000_projects_and_model_usages/migration.sql +41 -0
  58. package/package.json +18 -5
  59. package/build/client/assets/_layout-D-bxuQBW.js +0 -1
  60. package/build/client/assets/_layout-D2eOWFrG.js +0 -1
  61. package/build/client/assets/_layout-D4m0RbUC.js +0 -1
  62. package/build/client/assets/_layout-DggtTDZp.js +0 -1
  63. package/build/client/assets/auth-shell-FEL5QDuP.js +0 -1
  64. package/build/client/assets/confirm-passkey-ntLsS7_k.js +0 -1
  65. package/build/client/assets/entry.client-BkscxSA6.js +0 -5
  66. package/build/client/assets/file-list-DBDZbrBp.js +0 -1
  67. package/build/client/assets/files-Bzkr7-i7.js +0 -1
  68. package/build/client/assets/files-browser-KNurSsen.js +0 -1
  69. package/build/client/assets/files-cnv1kfgp.js +0 -1
  70. package/build/client/assets/files.browse-9p4xl9MV.js +0 -1
  71. package/build/client/assets/git-CjSY0eSC.js +0 -1
  72. package/build/client/assets/git-T0lLjMNd.js +0 -1
  73. package/build/client/assets/git-browser-DwSOY6QN.js +0 -1
  74. package/build/client/assets/iconify-BDcX0yw8.js +0 -1
  75. package/build/client/assets/index-8zQ7Fizv.js +0 -9
  76. package/build/client/assets/index-BUN6tnpl.js +0 -1
  77. package/build/client/assets/index-BlNoCKAt.js +0 -1
  78. package/build/client/assets/index-g2QB9BbT.js +0 -1
  79. package/build/client/assets/instance-events-provider-DTYnvumj.js +0 -39
  80. package/build/client/assets/instance-route-C25LRYYo.js +0 -1
  81. package/build/client/assets/instances-CzjXhR3c.js +0 -1
  82. package/build/client/assets/instances._instanceId.sessions._sessionId.test-COZIMxji.js +0 -208
  83. package/build/client/assets/instances.new-BZi5xwEV.js +0 -1
  84. package/build/client/assets/login-Bwt79t7_.js +0 -1
  85. package/build/client/assets/magic-string.es-DjU5CGuV.js +0 -10
  86. package/build/client/assets/manifest-3c01c628.js +0 -1
  87. package/build/client/assets/message-card-DJ5C3WNp.js +0 -34
  88. package/build/client/assets/passkeys-DpNQs4bn.js +0 -1
  89. package/build/client/assets/register-DXPj5RLZ.js +0 -1
  90. package/build/client/assets/root-DxD-Skic.css +0 -1
  91. package/build/client/assets/root-DxyfhNRI.js +0 -1
  92. package/build/client/assets/scrollable-layout-CS-vPTNM.js +0 -1
  93. package/build/client/assets/session-route-oxi4s_Qg.js +0 -1
  94. package/build/client/assets/sessions-provider-D1t07kir.js +0 -1
  95. package/build/client/assets/settings-CameSNIM.js +0 -1
  96. package/build/client/assets/sidebar-DRhB33rT.js +0 -1
  97. package/build/client/assets/use-double-check-CbbarqDt.js +0 -1
  98. package/build/runtime/app/lib/runtime-config.server.js +0 -422
  99. package/build/runtime/cli/scriptorium.js +0 -21
  100. package/build/runtime/cli/serve.js +0 -101
  101. /package/build/client/assets/{message-card-BclR_JqA.css → index-BclR_JqA.css} +0 -0
  102. /package/build/client/assets/{instances._instanceId.proxy._-l0sNRNKZ.js → projects._projectId.file-references.resolve-l0sNRNKZ.js} +0 -0
@@ -0,0 +1,170 @@
1
+ import { stringify as stringifyYaml } from "yaml";
2
+ import { z } from "zod";
3
+ import { createConfigSchema, createSecretsSchema, DOCUMENTATION_CONFIG_DIR, DOCUMENTATION_DATA_DIR, DOCUMENTATION_HOME, getOptionMetadata, } from "./schema.server.js";
4
+ function unwrapSchema(schema) {
5
+ if (schema instanceof z.ZodDefault) {
6
+ return unwrapSchema(schema._def.innerType);
7
+ }
8
+ if (schema instanceof z.ZodPrefault) {
9
+ return unwrapSchema(schema._def.innerType);
10
+ }
11
+ if (schema instanceof z.ZodPipe) {
12
+ return unwrapSchema(schema._def.out);
13
+ }
14
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
15
+ return unwrapSchema(schema._def.innerType);
16
+ }
17
+ return schema;
18
+ }
19
+ function getDefaultValue(schema) {
20
+ if (schema instanceof z.ZodDefault) {
21
+ return schema._def.defaultValue;
22
+ }
23
+ if (schema instanceof z.ZodPipe) {
24
+ return getDefaultValue(schema._def.out);
25
+ }
26
+ if (schema instanceof z.ZodPrefault) {
27
+ return getDefaultValue(schema._def.innerType);
28
+ }
29
+ return undefined;
30
+ }
31
+ function getTypeName(schema) {
32
+ const unwrapped = unwrapSchema(schema);
33
+ if (unwrapped instanceof z.ZodString) {
34
+ return "string";
35
+ }
36
+ if (unwrapped instanceof z.ZodNumber) {
37
+ return "number";
38
+ }
39
+ if (unwrapped instanceof z.ZodBoolean) {
40
+ return "boolean";
41
+ }
42
+ if (unwrapped instanceof z.ZodEnum) {
43
+ return "enum";
44
+ }
45
+ return "unknown";
46
+ }
47
+ export function collectSchemaDocumentation(schema, path = []) {
48
+ const unwrapped = unwrapSchema(schema);
49
+ if (unwrapped instanceof z.ZodObject) {
50
+ return Object.entries(unwrapped.shape).flatMap(([key, child]) => collectSchemaDocumentation(child, [...path, key]));
51
+ }
52
+ const metadata = schema instanceof z.ZodPipe
53
+ ? getOptionMetadata(schema._def.out)
54
+ : getOptionMetadata(schema);
55
+ return [{
56
+ path,
57
+ type: getTypeName(schema),
58
+ description: metadata.description,
59
+ cli: metadata.cli ? `--${metadata.cli}` : undefined,
60
+ env: metadata.env,
61
+ defaultValue: getDefaultValue(schema),
62
+ }];
63
+ }
64
+ export function getRuntimeConfigurationDocumentation() {
65
+ const sources = {
66
+ env: {
67
+ APPDATA: "C:/Users/you/AppData/Roaming",
68
+ HOME: "/path/to/home",
69
+ LOCALAPPDATA: "C:/Users/you/AppData/Local",
70
+ XDG_CONFIG_HOME: "/path/to/home/.config",
71
+ XDG_DATA_HOME: "/path/to/home/.local/share",
72
+ },
73
+ cli: {},
74
+ };
75
+ return {
76
+ config: collectSchemaDocumentation(createConfigSchema(sources)),
77
+ secrets: collectSchemaDocumentation(createSecretsSchema(sources)),
78
+ };
79
+ }
80
+ function stringifyDefaultValue(value) {
81
+ if (value === undefined) {
82
+ return "";
83
+ }
84
+ return `\`${String(normalizeDocumentationValue(value))}\``;
85
+ }
86
+ function normalizeDocumentationValue(value) {
87
+ if (typeof value !== "string") {
88
+ return value;
89
+ }
90
+ return value
91
+ .replaceAll("/path/to/home/.config/scriptorium", DOCUMENTATION_CONFIG_DIR)
92
+ .replaceAll("/path/to/home/.local/share/scriptorium", DOCUMENTATION_DATA_DIR)
93
+ .replaceAll("/path/to/home", DOCUMENTATION_HOME);
94
+ }
95
+ function escapeTableCell(value) {
96
+ return (value || "").replaceAll("|", "\\|");
97
+ }
98
+ function setNestedValue(target, path, value) {
99
+ let current = target;
100
+ for (const segment of path.slice(0, -1)) {
101
+ const existing = current[segment];
102
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
103
+ current[segment] = {};
104
+ }
105
+ current = current[segment];
106
+ }
107
+ current[path.at(-1)] = value;
108
+ }
109
+ function getExampleValue(row, section) {
110
+ if (row.defaultValue !== undefined) {
111
+ return normalizeDocumentationValue(row.defaultValue);
112
+ }
113
+ if (section === "secrets") {
114
+ return "<generated on first run or set via env>";
115
+ }
116
+ return `<set ${row.path.join(".")}>`;
117
+ }
118
+ export function buildDocumentationExample(rows, section) {
119
+ const result = {};
120
+ for (const row of rows) {
121
+ setNestedValue(result, row.path, getExampleValue(row, section));
122
+ }
123
+ return stringifyYaml(result).trim();
124
+ }
125
+ function renderDocumentationTable(rows) {
126
+ const lines = [
127
+ "| Key | Type | Default | CLI | Env | Description |",
128
+ "| --- | --- | --- | --- | --- | --- |",
129
+ ];
130
+ for (const row of rows) {
131
+ lines.push(`| \`${row.path.join(".")}\` | ${row.type} | ${escapeTableCell(stringifyDefaultValue(row.defaultValue))} | ${escapeTableCell(row.cli ? `\`${row.cli}\`` : "")} | ${escapeTableCell(row.env ? `\`${row.env}\`` : "")} | ${escapeTableCell(row.description)} |`);
132
+ }
133
+ return lines.join("\n");
134
+ }
135
+ export function renderRuntimeConfigurationMarkdown() {
136
+ const documentation = getRuntimeConfigurationDocumentation();
137
+ return [
138
+ "# Configuration Reference",
139
+ "",
140
+ "Scriptorium reads non-sensitive settings from `config.yml` and secrets from `secrets.yml` in its per-user config directory.",
141
+ "CLI flags override environment variables, which override YAML values, which override schema defaults.",
142
+ "The configuration directory can be overridden with `--config-dir <path>` or `SCRIPTORIUM_CONFIG_DIR`.",
143
+ "The data directory can be overridden with `--data-dir <path>` or `SCRIPTORIUM_DATA_DIR`.",
144
+ "",
145
+ `| Platform | ${DOCUMENTATION_CONFIG_DIR} | ${DOCUMENTATION_DATA_DIR} |`,
146
+ "| --- | --- | --- |",
147
+ "| macOS | `$XDG_CONFIG_HOME/scriptorium` or `~/.config/scriptorium` | `$XDG_DATA_HOME/scriptorium` or `~/.local/share/scriptorium` |",
148
+ "| Linux | `$XDG_CONFIG_HOME/scriptorium` or `~/.config/scriptorium` | `$XDG_DATA_HOME/scriptorium` or `~/.local/share/scriptorium` |",
149
+ "| Windows | `%XDG_CONFIG_HOME%\\scriptorium` or `%USERPROFILE%\\.config\\scriptorium` | `%XDG_DATA_HOME%\\scriptorium` or `%USERPROFILE%\\.local\\share\\scriptorium` |",
150
+ "",
151
+ `Examples use \`${DOCUMENTATION_CONFIG_DIR}\` and \`${DOCUMENTATION_DATA_DIR}\` as shorthand for Scriptorium's per-user config/data directories and \`${DOCUMENTATION_HOME}\` for the user's home directory.`,
152
+ "",
153
+ "## config.yml",
154
+ "",
155
+ "```yaml",
156
+ buildDocumentationExample(documentation.config, "config"),
157
+ "```",
158
+ "",
159
+ renderDocumentationTable(documentation.config),
160
+ "",
161
+ "## secrets.yml",
162
+ "",
163
+ "```yaml",
164
+ buildDocumentationExample(documentation.secrets, "secrets"),
165
+ "```",
166
+ "",
167
+ renderDocumentationTable(documentation.secrets),
168
+ "",
169
+ ].join("\n");
170
+ }
@@ -0,0 +1,69 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import { createConfigSchema, createSecretsSchema, getRuntimeConfigPaths, } from "./schema.server.js";
6
+ const PRIVATE_FILE_MODE = 0o600;
7
+ function readYamlFile(filePath) {
8
+ if (!existsSync(filePath)) {
9
+ return {};
10
+ }
11
+ const content = readFileSync(filePath, "utf8");
12
+ const parsed = parseYaml(content);
13
+ if (parsed === null || parsed === undefined) {
14
+ return {};
15
+ }
16
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
17
+ throw new Error(`Expected ${filePath} to contain a YAML mapping.`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function writeYamlFile(filePath, value) {
22
+ mkdirSync(dirname(filePath), { recursive: true });
23
+ const content = stringifyYaml(value);
24
+ writeFileSync(filePath, content, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
25
+ try {
26
+ chmodSync(filePath, PRIVATE_FILE_MODE);
27
+ }
28
+ catch {
29
+ // Best-effort only. Some platforms ignore chmod semantics.
30
+ }
31
+ }
32
+ function ensureRuntimeConfigFiles(paths) {
33
+ mkdirSync(paths.dataDirectory, { recursive: true });
34
+ if (!existsSync(paths.configFile)) {
35
+ writeYamlFile(paths.configFile, {});
36
+ }
37
+ if (!existsSync(paths.secretsFile)) {
38
+ writeYamlFile(paths.secretsFile, {});
39
+ }
40
+ }
41
+ function ensureSessionSecret(parsedSecrets, paths, sources) {
42
+ const configuredSecret = parsedSecrets.auth.sessionSecret;
43
+ if (configuredSecret) {
44
+ return {
45
+ auth: {
46
+ sessionSecret: configuredSecret,
47
+ },
48
+ };
49
+ }
50
+ const generatedSecret = randomBytes(32).toString("hex");
51
+ const persistedSecrets = {
52
+ auth: {
53
+ sessionSecret: generatedSecret,
54
+ },
55
+ };
56
+ if (sources.env?.SESSION_SECRET === undefined) {
57
+ writeYamlFile(paths.secretsFile, persistedSecrets);
58
+ }
59
+ return persistedSecrets;
60
+ }
61
+ export function resolveRuntimeConfiguration(sources = {}) {
62
+ const paths = getRuntimeConfigPaths(sources);
63
+ ensureRuntimeConfigFiles(paths);
64
+ const rawConfig = readYamlFile(paths.configFile);
65
+ const rawSecrets = readYamlFile(paths.secretsFile);
66
+ const config = createConfigSchema(sources).parse(rawConfig);
67
+ const secrets = ensureSessionSecret(createSecretsSchema(sources).parse(rawSecrets), paths, sources);
68
+ return { config, secrets, paths };
69
+ }
@@ -0,0 +1,128 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import { z } from "zod";
4
+ export const CONFIG_FILE_NAME = "config.yml";
5
+ export const SECRETS_FILE_NAME = "secrets.yml";
6
+ export const DOCUMENTATION_HOME = "$HOME";
7
+ export const DOCUMENTATION_CONFIG_DIR = "<scriptorium_config_dir>";
8
+ export const DOCUMENTATION_DATA_DIR = "<scriptorium_data_dir>";
9
+ export function meta(schema, metadata) {
10
+ return schema.meta(metadata);
11
+ }
12
+ export function section(shape) {
13
+ return z.object(shape).prefault(() => Object.fromEntries(Object.keys(shape).map((key) => [key, undefined])));
14
+ }
15
+ export function getOptionMetadata(schema) {
16
+ const metadata = schema.meta();
17
+ if (!metadata?.description) {
18
+ throw new Error("Missing required option metadata.");
19
+ }
20
+ return metadata;
21
+ }
22
+ export function override(schema, sources) {
23
+ const metadata = getOptionMetadata(schema);
24
+ return z.transform((configValue) => {
25
+ if (metadata.cli) {
26
+ const cliValue = sources.cli?.[metadata.cli];
27
+ if (cliValue !== undefined) {
28
+ return cliValue;
29
+ }
30
+ }
31
+ if (metadata.env) {
32
+ const envValue = sources.env?.[metadata.env];
33
+ if (envValue !== undefined) {
34
+ return envValue;
35
+ }
36
+ }
37
+ return configValue;
38
+ }).pipe(schema);
39
+ }
40
+ function xdgDirectory(env, name) {
41
+ const home = env.HOME?.trim() || homedir();
42
+ if (name === "config") {
43
+ return resolve(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "scriptorium");
44
+ }
45
+ return resolve(env.XDG_DATA_HOME?.trim() || join(home, ".local/share"), "scriptorium");
46
+ }
47
+ function defaultConfigDirectory(env = process.env) {
48
+ return xdgDirectory(env, "config");
49
+ }
50
+ function defaultDataDirectory(env = process.env) {
51
+ return xdgDirectory(env, "data");
52
+ }
53
+ export function getRuntimeConfigPaths(sources = {}) {
54
+ const configDirectory = resolve(sources.configDir ||
55
+ sources.env?.SCRIPTORIUM_CONFIG_DIR?.trim() ||
56
+ defaultConfigDirectory(sources.env));
57
+ const dataDirectory = resolve(sources.dataDir ||
58
+ sources.env?.SCRIPTORIUM_DATA_DIR?.trim() ||
59
+ defaultDataDirectory(sources.env));
60
+ return {
61
+ configDirectory,
62
+ dataDirectory,
63
+ configFile: join(configDirectory, CONFIG_FILE_NAME),
64
+ secretsFile: join(configDirectory, SECRETS_FILE_NAME),
65
+ };
66
+ }
67
+ function getDefaultDatabasePath(sources) {
68
+ return join(getRuntimeConfigPaths(sources).dataDirectory, "app.db");
69
+ }
70
+ export function createConfigSchema(sources = {}) {
71
+ const defaultHomeDirectory = sources.env?.HOME?.trim() || homedir();
72
+ return z.object({
73
+ server: section({
74
+ host: override(meta(z.string().min(1).default("0.0.0.0"), {
75
+ cli: "host",
76
+ env: "SCRIPTORIUM_HOST",
77
+ description: "Host interface for the web server.",
78
+ }), sources),
79
+ port: override(meta(z.coerce.number().int().min(1).max(65535).default(5174), {
80
+ cli: "port",
81
+ env: "SCRIPTORIUM_PORT",
82
+ description: "Port for the web server.",
83
+ }), sources),
84
+ }),
85
+ workspace: section({
86
+ browserRoot: override(meta(z.string().min(1).default(defaultHomeDirectory), {
87
+ cli: "browser-root",
88
+ env: "SCRIPTORIUM_BROWSER_ROOT",
89
+ description: "Root directory exposed in the workspace browser.",
90
+ }), sources),
91
+ }),
92
+ opencode: section({
93
+ bin: override(meta(z.string().min(1).default("opencode"), {
94
+ cli: "opencode-bin",
95
+ env: "OPENCODE_BIN",
96
+ description: "OpenCode executable name or path.",
97
+ }), sources),
98
+ url: override(meta(z.string().url().optional(), {
99
+ cli: "opencode-url",
100
+ env: "SCRIPTORIUM_OPENCODE_URL",
101
+ description: "OpenCode server base URL. When set, Scriptorium connects to this server instead of launching its own shared OpenCode process.",
102
+ }), sources),
103
+ }),
104
+ network: section({
105
+ tailscale: override(meta(z.coerce.boolean().default(false), {
106
+ cli: "tailscale",
107
+ description: "Expose the app with tailscale serve.",
108
+ }), sources),
109
+ }),
110
+ database: section({
111
+ path: override(meta(z.string().min(1).default(getDefaultDatabasePath(sources)), {
112
+ cli: "db-path",
113
+ env: "SCRIPTORIUM_DB_PATH",
114
+ description: "Path to the SQLite database file.",
115
+ }), sources),
116
+ }),
117
+ });
118
+ }
119
+ export function createSecretsSchema(sources = {}) {
120
+ return z.object({
121
+ auth: section({
122
+ sessionSecret: override(meta(z.string().min(32).optional(), {
123
+ env: "SESSION_SECRET",
124
+ description: "Session signing secret.",
125
+ }), sources),
126
+ }),
127
+ });
128
+ }
@@ -0,0 +1,176 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import compression from "compression";
15
+ import express from "express";
16
+ import morgan from "morgan";
17
+ import { createRequestHandler } from "@react-router/express";
18
+ import { initializeRuntimeConfiguration } from "../app/lib/runtime-config/cache.server.js";
19
+ import { parseRuntimeCliArgs, renderRuntimeConfigurationHelp, } from "../app/lib/runtime-config/cli.server.js";
20
+ const MODE = process.env.NODE_ENV ?? "production";
21
+ const IS_DEV = MODE === "development";
22
+ function findPackageRoot() {
23
+ let currentDirectory = path.dirname(fileURLToPath(import.meta.url));
24
+ while (true) {
25
+ if (existsSync(path.join(currentDirectory, "package.json"))) {
26
+ return currentDirectory;
27
+ }
28
+ const parentDirectory = path.dirname(currentDirectory);
29
+ if (parentDirectory === currentDirectory) {
30
+ throw new Error("Unable to locate package root.");
31
+ }
32
+ currentDirectory = parentDirectory;
33
+ }
34
+ }
35
+ function getBuildPaths(packageRoot) {
36
+ return {
37
+ clientDirectory: path.join(packageRoot, "build/client"),
38
+ clientAssetsDirectory: path.join(packageRoot, "build/client/assets"),
39
+ serverBuildPath: path.join(packageRoot, "build/server/index.js"),
40
+ };
41
+ }
42
+ function assertBuildExists(packageRoot) {
43
+ const paths = getBuildPaths(packageRoot);
44
+ if (!existsSync(paths.serverBuildPath) || !existsSync(paths.clientDirectory)) {
45
+ throw new Error("Scriptorium has not been built yet. Run `npm run build` before `npm start`, or use `npm run dev` for development.");
46
+ }
47
+ return paths;
48
+ }
49
+ function startTailscale(port) {
50
+ try {
51
+ execSync(`tailscale serve --bg http://localhost:${port}`, { stdio: "inherit" });
52
+ }
53
+ catch {
54
+ // tailscale not available - ignore
55
+ }
56
+ }
57
+ function stopTailscale() {
58
+ try {
59
+ execSync("tailscale serve --https=443 off", { stdio: "ignore" });
60
+ }
61
+ catch {
62
+ // tailscale not available - ignore
63
+ }
64
+ }
65
+ function logServerAddresses(host, port) {
66
+ const networkAddress = Object.values(os.networkInterfaces())
67
+ .flat()
68
+ .find((entry) => String(entry?.family).includes("4") && !entry?.internal)?.address;
69
+ if (networkAddress) {
70
+ process.stdout.write(`[scriptorium] http://${host}:${port} (http://${networkAddress}:${port})\n`);
71
+ return;
72
+ }
73
+ process.stdout.write(`[scriptorium] http://${host}:${port}\n`);
74
+ }
75
+ async function createApp(runtime, packageRoot) {
76
+ const app = express();
77
+ app.disable("x-powered-by");
78
+ app.use(compression());
79
+ app.use(morgan("tiny", {
80
+ skip: (req) => req.path === "/session-read-status/ack",
81
+ }));
82
+ if (IS_DEV) {
83
+ const hmrPort = runtime.config.server.port >= 65535
84
+ ? runtime.config.server.port - 1
85
+ : runtime.config.server.port + 1;
86
+ const vite = await import("vite");
87
+ const viteDevServer = await vite.createServer({
88
+ appType: "custom",
89
+ server: {
90
+ hmr: {
91
+ port: hmrPort,
92
+ },
93
+ middlewareMode: true,
94
+ },
95
+ });
96
+ app.use(viteDevServer.middlewares);
97
+ app.use(async (req, res, next) => {
98
+ try {
99
+ const build = await viteDevServer.ssrLoadModule("virtual:react-router/server-build");
100
+ const handler = createRequestHandler({
101
+ build,
102
+ mode: MODE,
103
+ });
104
+ return handler(req, res, next);
105
+ }
106
+ catch (error) {
107
+ if (error instanceof Error) {
108
+ viteDevServer.ssrFixStacktrace(error);
109
+ }
110
+ return next(error);
111
+ }
112
+ });
113
+ return app;
114
+ }
115
+ const buildPaths = assertBuildExists(packageRoot);
116
+ const build = await import(__rewriteRelativeImportExtension(pathToFileURL(buildPaths.serverBuildPath).href, true));
117
+ app.use("/assets", express.static(buildPaths.clientAssetsDirectory, {
118
+ immutable: true,
119
+ maxAge: "1y",
120
+ }));
121
+ app.use(express.static(buildPaths.clientDirectory));
122
+ app.all("*", createRequestHandler({
123
+ build,
124
+ mode: MODE,
125
+ }));
126
+ return app;
127
+ }
128
+ export async function main(args = process.argv.slice(2)) {
129
+ const parsedCli = parseRuntimeCliArgs(args);
130
+ if (parsedCli.help) {
131
+ process.stdout.write(`${renderRuntimeConfigurationHelp()}\n`);
132
+ return;
133
+ }
134
+ const runtime = initializeRuntimeConfiguration({
135
+ cli: parsedCli.cli,
136
+ configDir: parsedCli.configDir,
137
+ dataDir: parsedCli.dataDir,
138
+ env: process.env,
139
+ });
140
+ const packageRoot = findPackageRoot();
141
+ const app = await createApp(runtime, packageRoot);
142
+ const host = runtime.config.server.host;
143
+ const port = runtime.config.server.port;
144
+ const server = await new Promise((resolve, reject) => {
145
+ const httpServer = app.listen(port, host, () => resolve(httpServer));
146
+ httpServer.once("error", reject);
147
+ });
148
+ if (runtime.config.network.tailscale) {
149
+ startTailscale(port);
150
+ }
151
+ logServerAddresses(host, port);
152
+ let shuttingDown = false;
153
+ const shutdown = () => {
154
+ if (shuttingDown) {
155
+ return;
156
+ }
157
+ shuttingDown = true;
158
+ if (runtime.config.network.tailscale) {
159
+ stopTailscale();
160
+ }
161
+ server.close((error) => {
162
+ if (error) {
163
+ process.stderr.write(`${error.message}\n`);
164
+ process.exitCode = 1;
165
+ }
166
+ });
167
+ };
168
+ server.once("close", () => {
169
+ if (runtime.config.network.tailscale) {
170
+ stopTailscale();
171
+ }
172
+ });
173
+ process.once("SIGINT", shutdown);
174
+ process.once("SIGTERM", shutdown);
175
+ }
176
+ await main();