arcway 0.1.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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -0
  3. package/client/env.js +55 -0
  4. package/client/fetcher.js +50 -0
  5. package/client/graphql.js +35 -0
  6. package/client/head.js +140 -0
  7. package/client/hooks/use-api.js +80 -0
  8. package/client/hooks/use-debounce.js +12 -0
  9. package/client/hooks/use-form.js +86 -0
  10. package/client/hooks/use-graphql.js +30 -0
  11. package/client/hooks/use-interval.js +12 -0
  12. package/client/hooks/use-mutation.js +27 -0
  13. package/client/hooks/use-query.js +45 -0
  14. package/client/hooks/web/use-click-outside.js +22 -0
  15. package/client/hooks/web/use-local-storage.js +42 -0
  16. package/client/index.js +62 -0
  17. package/client/page-loader.js +155 -0
  18. package/client/provider.js +53 -0
  19. package/client/query.js +13 -0
  20. package/client/router.jsx +303 -0
  21. package/client/ui/accordion.jsx +65 -0
  22. package/client/ui/accordion.stories.jsx +48 -0
  23. package/client/ui/alert-dialog.jsx +122 -0
  24. package/client/ui/alert-dialog.stories.jsx +44 -0
  25. package/client/ui/alert.jsx +52 -0
  26. package/client/ui/alert.stories.jsx +31 -0
  27. package/client/ui/app-shell.jsx +39 -0
  28. package/client/ui/app-shell.stories.jsx +51 -0
  29. package/client/ui/aspect-ratio.jsx +6 -0
  30. package/client/ui/aspect-ratio.stories.jsx +69 -0
  31. package/client/ui/avatar.jsx +78 -0
  32. package/client/ui/avatar.stories.jsx +62 -0
  33. package/client/ui/badge.jsx +34 -0
  34. package/client/ui/badge.stories.js +32 -0
  35. package/client/ui/breadcrumb.jsx +86 -0
  36. package/client/ui/breadcrumb.stories.jsx +43 -0
  37. package/client/ui/button-group.jsx +58 -0
  38. package/client/ui/button-group.stories.jsx +67 -0
  39. package/client/ui/button.jsx +46 -0
  40. package/client/ui/button.stories.js +72 -0
  41. package/client/ui/calendar.jsx +172 -0
  42. package/client/ui/card.jsx +57 -0
  43. package/client/ui/card.stories.jsx +33 -0
  44. package/client/ui/carousel.jsx +167 -0
  45. package/client/ui/chart.jsx +244 -0
  46. package/client/ui/checkbox.jsx +24 -0
  47. package/client/ui/checkbox.stories.js +33 -0
  48. package/client/ui/collapsible.jsx +12 -0
  49. package/client/ui/collapsible.stories.jsx +42 -0
  50. package/client/ui/combobox.jsx +223 -0
  51. package/client/ui/command.jsx +128 -0
  52. package/client/ui/context-menu.jsx +170 -0
  53. package/client/ui/context-menu.stories.jsx +35 -0
  54. package/client/ui/dialog.jsx +109 -0
  55. package/client/ui/dialog.stories.jsx +37 -0
  56. package/client/ui/direction.jsx +9 -0
  57. package/client/ui/drawer.jsx +87 -0
  58. package/client/ui/dropdown-menu.jsx +172 -0
  59. package/client/ui/dropdown-menu.stories.jsx +34 -0
  60. package/client/ui/empty.jsx +76 -0
  61. package/client/ui/empty.stories.jsx +64 -0
  62. package/client/ui/field.jsx +174 -0
  63. package/client/ui/field.stories.jsx +118 -0
  64. package/client/ui/form.jsx +17 -0
  65. package/client/ui/hooks/use-mobile.js +16 -0
  66. package/client/ui/hover-card.jsx +26 -0
  67. package/client/ui/hover-card.stories.jsx +28 -0
  68. package/client/ui/index.js +649 -0
  69. package/client/ui/input-group.jsx +116 -0
  70. package/client/ui/input-group.stories.jsx +65 -0
  71. package/client/ui/input-otp.jsx +62 -0
  72. package/client/ui/input.jsx +16 -0
  73. package/client/ui/input.stories.js +27 -0
  74. package/client/ui/item.jsx +155 -0
  75. package/client/ui/item.stories.jsx +118 -0
  76. package/client/ui/kbd.jsx +24 -0
  77. package/client/ui/kbd.stories.jsx +32 -0
  78. package/client/ui/label.jsx +16 -0
  79. package/client/ui/label.stories.js +25 -0
  80. package/client/ui/lib/utils.js +6 -0
  81. package/client/ui/main-content.jsx +30 -0
  82. package/client/ui/menubar.jsx +189 -0
  83. package/client/ui/menubar.stories.jsx +43 -0
  84. package/client/ui/native-select.jsx +34 -0
  85. package/client/ui/native-select.stories.jsx +67 -0
  86. package/client/ui/navigation-menu.jsx +120 -0
  87. package/client/ui/navigation-menu.stories.jsx +45 -0
  88. package/client/ui/pagination.jsx +92 -0
  89. package/client/ui/pagination.stories.jsx +52 -0
  90. package/client/ui/panel.jsx +66 -0
  91. package/client/ui/popover.jsx +54 -0
  92. package/client/ui/popover.stories.jsx +27 -0
  93. package/client/ui/progress.jsx +19 -0
  94. package/client/ui/progress.stories.js +34 -0
  95. package/client/ui/radio-group.jsx +33 -0
  96. package/client/ui/radio-group.stories.jsx +49 -0
  97. package/client/ui/resizable.jsx +33 -0
  98. package/client/ui/scroll-area.jsx +41 -0
  99. package/client/ui/scroll-area.stories.jsx +43 -0
  100. package/client/ui/select.jsx +145 -0
  101. package/client/ui/select.stories.jsx +80 -0
  102. package/client/ui/separator.jsx +18 -0
  103. package/client/ui/separator.stories.jsx +37 -0
  104. package/client/ui/sheet.jsx +95 -0
  105. package/client/ui/sheet.stories.jsx +56 -0
  106. package/client/ui/sidebar.jsx +544 -0
  107. package/client/ui/skeleton.jsx +8 -0
  108. package/client/ui/skeleton.stories.js +23 -0
  109. package/client/ui/slider.jsx +41 -0
  110. package/client/ui/slider.stories.js +31 -0
  111. package/client/ui/sonner.jsx +37 -0
  112. package/client/ui/spinner.jsx +14 -0
  113. package/client/ui/spinner.stories.js +16 -0
  114. package/client/ui/style-mira.css +1316 -0
  115. package/client/ui/switch.jsx +22 -0
  116. package/client/ui/switch.stories.js +44 -0
  117. package/client/ui/table.jsx +33 -0
  118. package/client/ui/table.stories.jsx +42 -0
  119. package/client/ui/tabs.jsx +63 -0
  120. package/client/ui/tabs.stories.jsx +45 -0
  121. package/client/ui/textarea.jsx +15 -0
  122. package/client/ui/textarea.stories.js +33 -0
  123. package/client/ui/theme.css +459 -0
  124. package/client/ui/toggle-group.jsx +62 -0
  125. package/client/ui/toggle-group.stories.jsx +68 -0
  126. package/client/ui/toggle.jsx +34 -0
  127. package/client/ui/toggle.stories.js +46 -0
  128. package/client/ui/tooltip.jsx +37 -0
  129. package/client/ui/tooltip.stories.jsx +32 -0
  130. package/client/ui/use-transition.js +35 -0
  131. package/client/ws.js +132 -0
  132. package/package.json +134 -0
  133. package/server/bin/cli.js +42 -0
  134. package/server/bin/commands/build.js +23 -0
  135. package/server/bin/commands/dev.js +57 -0
  136. package/server/bin/commands/docs.js +30 -0
  137. package/server/bin/commands/graphql-schema.js +32 -0
  138. package/server/bin/commands/lint.js +35 -0
  139. package/server/bin/commands/mcp.js +26 -0
  140. package/server/bin/commands/migrate.js +82 -0
  141. package/server/bin/commands/schema.js +41 -0
  142. package/server/bin/commands/seed.js +36 -0
  143. package/server/bin/commands/start.js +31 -0
  144. package/server/bin/commands/test.js +20 -0
  145. package/server/bin/solo.js +4 -0
  146. package/server/boot/index.js +150 -0
  147. package/server/boot.js +2 -0
  148. package/server/build.js +23 -0
  149. package/server/cache/drivers/memory.js +23 -0
  150. package/server/cache/drivers/redis.js +28 -0
  151. package/server/cache/index.js +69 -0
  152. package/server/config/loader.js +89 -0
  153. package/server/config/modules/api.js +17 -0
  154. package/server/config/modules/build.js +9 -0
  155. package/server/config/modules/cache.js +10 -0
  156. package/server/config/modules/database.js +29 -0
  157. package/server/config/modules/events.js +15 -0
  158. package/server/config/modules/files.js +15 -0
  159. package/server/config/modules/jobs.js +20 -0
  160. package/server/config/modules/logger.js +9 -0
  161. package/server/config/modules/mail.js +11 -0
  162. package/server/config/modules/mcp.js +9 -0
  163. package/server/config/modules/pages.js +20 -0
  164. package/server/config/modules/queue.js +10 -0
  165. package/server/config/modules/redis.js +9 -0
  166. package/server/config/modules/server.js +30 -0
  167. package/server/config/modules/session.js +9 -0
  168. package/server/config/modules/websocket.js +11 -0
  169. package/server/constants.js +67 -0
  170. package/server/context.js +15 -0
  171. package/server/db/index.js +87 -0
  172. package/server/db/schema/drivers/mysql.js +28 -0
  173. package/server/db/schema/drivers/pg.js +34 -0
  174. package/server/db/schema/drivers/sqlite.js +22 -0
  175. package/server/db/schema/index.js +78 -0
  176. package/server/db/seeds.js +22 -0
  177. package/server/discovery.js +67 -0
  178. package/server/docs/openapi.js +153 -0
  179. package/server/env.js +17 -0
  180. package/server/events/drivers/memory.js +45 -0
  181. package/server/events/drivers/redis.js +64 -0
  182. package/server/events/handler.js +67 -0
  183. package/server/events/index.js +35 -0
  184. package/server/events/pattern.js +5 -0
  185. package/server/files/drivers/local.js +83 -0
  186. package/server/files/drivers/s3.js +113 -0
  187. package/server/files/index.js +57 -0
  188. package/server/filewatcher/index.js +156 -0
  189. package/server/glob.js +6 -0
  190. package/server/graphql/discovery.js +70 -0
  191. package/server/graphql/handler.js +41 -0
  192. package/server/graphql/index.js +13 -0
  193. package/server/graphql/loaders.js +19 -0
  194. package/server/graphql/merge.js +48 -0
  195. package/server/graphql/subscriptions.js +43 -0
  196. package/server/health.js +34 -0
  197. package/server/helpers.js +9 -0
  198. package/server/index.js +55 -0
  199. package/server/internals.js +139 -0
  200. package/server/jobs/cron.js +10 -0
  201. package/server/jobs/drivers/knex-queue.js +207 -0
  202. package/server/jobs/drivers/lease.js +148 -0
  203. package/server/jobs/drivers/memory-queue.js +134 -0
  204. package/server/jobs/queue.js +27 -0
  205. package/server/jobs/runner.js +197 -0
  206. package/server/jobs/throughput.js +63 -0
  207. package/server/lib/vault/encrypt.js +40 -0
  208. package/server/lib/vault/ids.js +9 -0
  209. package/server/lib/vault/index.js +14 -0
  210. package/server/lib/vault/jwt.js +55 -0
  211. package/server/lib/vault/password.js +10 -0
  212. package/server/lint/boundaries.js +77 -0
  213. package/server/logger/index.js +130 -0
  214. package/server/mail/drivers/console.js +31 -0
  215. package/server/mail/drivers/smtp.js +34 -0
  216. package/server/mail/imap.js +105 -0
  217. package/server/mail/inbound-store.js +58 -0
  218. package/server/mail/inbound.js +79 -0
  219. package/server/mail/index.js +112 -0
  220. package/server/mcp/debug-api.js +137 -0
  221. package/server/mcp/helpers.js +30 -0
  222. package/server/mcp/index.js +77 -0
  223. package/server/mcp/runtime.js +7 -0
  224. package/server/mcp/server.js +19 -0
  225. package/server/mcp/tools/debugging.js +133 -0
  226. package/server/mcp/tools/introspection.js +87 -0
  227. package/server/middlewares/cors.js +30 -0
  228. package/server/middlewares/index.js +3 -0
  229. package/server/middlewares/require-session.js +15 -0
  230. package/server/module-loader.js +9 -0
  231. package/server/pages/build-client.js +187 -0
  232. package/server/pages/build-css.js +47 -0
  233. package/server/pages/build-manifest.js +55 -0
  234. package/server/pages/build-plugins.js +75 -0
  235. package/server/pages/build-server.js +115 -0
  236. package/server/pages/build.js +116 -0
  237. package/server/pages/discovery.js +120 -0
  238. package/server/pages/fonts.js +128 -0
  239. package/server/pages/handler.js +276 -0
  240. package/server/pages/hmr.js +176 -0
  241. package/server/pages/pages-router.js +78 -0
  242. package/server/pages/ssr.js +276 -0
  243. package/server/pages/static.js +92 -0
  244. package/server/pages/watcher.js +90 -0
  245. package/server/queue/drivers/knex.js +67 -0
  246. package/server/queue/drivers/redis.js +91 -0
  247. package/server/queue/index.js +61 -0
  248. package/server/rate-limit/consume.js +21 -0
  249. package/server/rate-limit/drivers/memory.js +24 -0
  250. package/server/rate-limit/drivers/redis.js +32 -0
  251. package/server/rate-limit/index.js +33 -0
  252. package/server/redis/index.js +67 -0
  253. package/server/ring-buffer.js +44 -0
  254. package/server/route.js +4 -0
  255. package/server/router/api-router.js +317 -0
  256. package/server/router/cors.js +31 -0
  257. package/server/router/middleware.js +91 -0
  258. package/server/router/routes.js +132 -0
  259. package/server/server.js +35 -0
  260. package/server/session/helpers.js +21 -0
  261. package/server/session/index.js +89 -0
  262. package/server/static/index.js +36 -0
  263. package/server/system-jobs/index.js +50 -0
  264. package/server/system-routes/index.js +84 -0
  265. package/server/testing/index.js +263 -0
  266. package/server/validation.js +41 -0
  267. package/server/watcher.js +34 -0
  268. package/server/web-server.js +231 -0
  269. package/server/ws/discovery.js +54 -0
  270. package/server/ws/index.js +14 -0
  271. package/server/ws/realtime.js +318 -0
  272. package/server/ws/registry.js +17 -0
  273. package/server/ws/server.js +152 -0
  274. package/server/ws/ws-router.js +335 -0
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { makeConfig } from '#server/config/loader.js';
4
+ import { loadEnvFiles } from '#server/env.js';
5
+ import { createDB } from '#server/db/index.js';
6
+ async function runSchema(outFile) {
7
+ const rootDir = process.cwd();
8
+ const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
9
+ loadEnvFiles(rootDir, mode);
10
+ try {
11
+ const config = await makeConfig(rootDir);
12
+ const db = await createDB(config.database);
13
+ await db.runMigrations();
14
+ console.log('Introspecting database schema...');
15
+ const schema = await db.introspectSchema();
16
+ if (schema.tables.length === 0) {
17
+ console.log('No tables found.');
18
+ await db.destroy();
19
+ return;
20
+ }
21
+ const markdown = db.generateSchemaMarkdown(schema);
22
+ const outputPath = path.resolve(rootDir, outFile);
23
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
24
+ await fs.writeFile(outputPath, markdown, 'utf-8');
25
+ console.log(` Tables: ${schema.tables.length}`);
26
+ console.log(`
27
+ Schema written to ${path.relative(rootDir, outputPath)}`);
28
+ await db.destroy();
29
+ } catch (err) {
30
+ console.error('Schema generation failed:', err instanceof Error ? err.message : err);
31
+ process.exit(1);
32
+ }
33
+ }
34
+ function register(program) {
35
+ program
36
+ .command('schema')
37
+ .description('Generate database schema documentation from introspected tables')
38
+ .argument('[outFile]', 'output file path', 'docs/schema.md')
39
+ .action(runSchema);
40
+ }
41
+ export default register;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { makeConfig } from '#server/config/loader.js';
3
+ import { loadEnvFiles } from '#server/env.js';
4
+ import { createDB } from '#server/db/index.js';
5
+ import { runSeeds } from '#server/db/seeds.js';
6
+ async function runSeed() {
7
+ const rootDir = process.cwd();
8
+ const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
9
+ loadEnvFiles(rootDir, mode);
10
+ try {
11
+ const config = await makeConfig(rootDir);
12
+ const db = await createDB(config.database);
13
+ await db.runMigrations();
14
+ const seedsDir = path.join(rootDir, 'seeds');
15
+ const results = await runSeeds(db, seedsDir);
16
+ if (results.length === 0) {
17
+ console.log('No seed files found.');
18
+ await db.destroy();
19
+ return;
20
+ }
21
+ console.log(`\nRunning ${results.length} seed file(s)...`);
22
+ for (const result of results) {
23
+ const icon = result.status === 'completed' ? '+' : 'x';
24
+ console.log(` [${icon}] ${result.name}`);
25
+ }
26
+ console.log('\nSeeding complete.');
27
+ await db.destroy();
28
+ } catch (err) {
29
+ console.error('Seed failed:', err instanceof Error ? err.message : err);
30
+ process.exit(1);
31
+ }
32
+ }
33
+ function register(program) {
34
+ program.command('seed').description('Run database seed files').action(runSeed);
35
+ }
36
+ export default register;
@@ -0,0 +1,31 @@
1
+ import boot from '#server/boot.js';
2
+
3
+ async function startServer(mode) {
4
+ if (mode === 'production') {
5
+ process.env.NODE_ENV = 'production';
6
+ }
7
+
8
+ const app = await boot({ mode, rootDir: process.cwd() });
9
+ app.logger.info('Arcway framework ready', { mode, port: app.port });
10
+
11
+ let shuttingDown = false;
12
+ const shutdown = async (signal) => {
13
+ if (shuttingDown) return;
14
+ shuttingDown = true;
15
+ app.logger.info(`Received ${signal}, shutting down...`);
16
+ await app.shutdown();
17
+ process.exit(0);
18
+ };
19
+
20
+ process.on('SIGINT', () => shutdown('SIGINT'));
21
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
22
+
23
+ return app;
24
+ }
25
+
26
+ function register(program) {
27
+ program.command('start').description('Start production server').action(() => startServer('production'));
28
+ }
29
+
30
+ export { startServer };
31
+ export default register;
@@ -0,0 +1,20 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ function runTest(args) {
3
+ try {
4
+ execFileSync('npx', ['vitest', 'run', ...args], {
5
+ stdio: 'inherit',
6
+ cwd: process.cwd(),
7
+ });
8
+ } catch {
9
+ process.exit(1);
10
+ }
11
+ }
12
+ function register(program) {
13
+ program
14
+ .command('test')
15
+ .description('Run domain tests via vitest')
16
+ .allowUnknownOption()
17
+ .argument('[args...]', 'additional arguments passed to vitest')
18
+ .action(runTest);
19
+ }
20
+ export default register;
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createProgram } from './cli.js';
3
+ const program = createProgram();
4
+ program.parse(process.argv);
@@ -0,0 +1,150 @@
1
+ import { makeConfig } from '../config/loader.js';
2
+ import { createDB } from '../db/index.js';
3
+ import Redis from '../redis/index.js';
4
+ import Events from '../events/index.js';
5
+ import { EventHandler } from '../events/handler.js';
6
+ import { JobRunner } from '../jobs/runner.js';
7
+ import Logger from '../logger/index.js';
8
+ import { loadEnvFiles } from '../env.js';
9
+ import { McpRouter } from '../mcp/index.js';
10
+ import Queue from '../queue/index.js';
11
+ import Cache from '../cache/index.js';
12
+ import Files from '../files/index.js';
13
+ import { Mail } from '../mail/index.js';
14
+ import WebServer from '../web-server.js';
15
+ import { FileWatcher } from '../filewatcher/index.js';
16
+ import { ApiRouter } from '../router/api-router.js';
17
+ import { PagesRouter } from '../pages/pages-router.js';
18
+ import { SystemRouter } from '../system-routes/index.js';
19
+ import { StaticRouter } from '../static/index.js';
20
+ import { WsRouter } from '../ws/ws-router.js';
21
+ import path from 'node:path';
22
+
23
+ async function boot(options) {
24
+ const rootDir = options.rootDir ?? process.cwd();
25
+ const mode = options.mode;
26
+
27
+ const envFiles = loadEnvFiles(rootDir, mode);
28
+ const config = await makeConfig(rootDir, { overrides: options.configOverrides, mode });
29
+
30
+ const log = new Logger(config.logger);
31
+ const mcpRouter = new McpRouter(config.mcp, { log });
32
+
33
+ if (envFiles.length > 0) log.info('Env files loaded', { envFiles });
34
+
35
+ const db = await createDB(config.database, { log });
36
+ await db.runMigrations();
37
+
38
+ const redis = new Redis(config.redis, { log });
39
+ await redis.connect();
40
+
41
+ const queue = new Queue(config.queue, { db, redis, log });
42
+ await queue.init();
43
+
44
+ const cache = new Cache(config.cache, { redis, log });
45
+ await cache.init();
46
+
47
+ const files = new Files(config.files, { log });
48
+ await files.init();
49
+
50
+ const mail = new Mail(config.mail, { db, log, queue });
51
+
52
+ const events = new Events(config.events, { redis, log });
53
+
54
+ const eventHandler = new EventHandler(config.events, {
55
+ events,
56
+ log,
57
+ appContext: { db, events, queue, cache, files, mail, log },
58
+ });
59
+ await eventHandler.init();
60
+
61
+ const jobRunner = new JobRunner(config.jobs, { db, queue, cache, files, mail, events, log });
62
+ await jobRunner.init();
63
+
64
+ const fileWatcher = new FileWatcher(rootDir, { log });
65
+ await fileWatcher.start();
66
+
67
+ const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher };
68
+
69
+ // ── Routers ──
70
+
71
+ const apiRouter = new ApiRouter(config.api, {
72
+ log,
73
+ fileWatcher,
74
+ appContext,
75
+ sessionConfig: config.session,
76
+ });
77
+ await apiRouter.init();
78
+
79
+ const pagesRouter = new PagesRouter(config, { rootDir, log, fileWatcher, appContext });
80
+ await pagesRouter.init();
81
+
82
+ const healthDeps = {
83
+ db,
84
+ redisClients: [{ name: 'redis', client: redis?.client }],
85
+ };
86
+ const systemRouter = new SystemRouter({ healthDeps });
87
+
88
+ const staticRouter = new StaticRouter({
89
+ outDir: config.pages?.outDir ?? path.resolve(rootDir, '.build/pages'),
90
+ publicDir: path.join(rootDir, 'public'),
91
+ });
92
+
93
+ const wsRouter = new WsRouter(config.websocket, {
94
+ apiRouter,
95
+ log,
96
+ sessionConfig: config.session,
97
+ });
98
+
99
+ const webServer = new WebServer(config, { log });
100
+
101
+ // Register handlers in priority order
102
+ mcpRouter.init({ jobRunner, apiRouter, db, mode });
103
+ webServer.use(mcpRouter, '/_mcp');
104
+ webServer.use(systemRouter, '/_system');
105
+ webServer.use(apiRouter, apiRouter.prefix);
106
+ webServer.use(wsRouter, config.websocket.path);
107
+ webServer.use(staticRouter);
108
+ webServer.use(pagesRouter);
109
+
110
+ await webServer.listen();
111
+
112
+ wsRouter.attachToServer(webServer.server);
113
+
114
+ jobRunner.start();
115
+
116
+ const shutdown = async () => {
117
+ await jobRunner.shutdown();
118
+ await pagesRouter.close();
119
+ await apiRouter.close();
120
+ await webServer.close();
121
+ await fileWatcher.close();
122
+ await events.disconnect();
123
+ await redis.disconnect();
124
+ await db.destroy();
125
+ await mcpRouter.cleanup(rootDir);
126
+ };
127
+
128
+ const app = {
129
+ config,
130
+ db,
131
+ webServer,
132
+ apiRouter,
133
+ events,
134
+ jobRunner,
135
+ server: webServer.server,
136
+ port: webServer.port,
137
+ mode,
138
+ logger: log,
139
+ mcpRuntime: mcpRouter.runtime,
140
+ fileWatcher,
141
+ systemRouter,
142
+ shutdown,
143
+ };
144
+
145
+ await mcpRouter.writeDevMetadata(rootDir, webServer.port);
146
+
147
+ return app;
148
+ }
149
+
150
+ export default boot;
package/server/boot.js ADDED
@@ -0,0 +1,2 @@
1
+ import boot from './boot/index.js';
2
+ export default boot;
@@ -0,0 +1,23 @@
1
+ import { makeConfig } from './config/loader.js';
2
+ import { buildPages } from './pages/build.js';
3
+
4
+ async function build(options) {
5
+ const rootDir = options?.rootDir ?? process.cwd();
6
+ const config = await makeConfig(rootDir).catch(() => null);
7
+ const buildTarget = config?.build?.target ?? 'node22';
8
+
9
+ let pages;
10
+ const pagesEnabled = config?.pages?.enabled !== false;
11
+ if (pagesEnabled) {
12
+ pages = await buildPages({
13
+ rootDir,
14
+ serverTarget: buildTarget,
15
+ minify: true,
16
+ fonts: config?.pages?.fonts,
17
+ });
18
+ }
19
+
20
+ return { pages };
21
+ }
22
+
23
+ export { build };
@@ -0,0 +1,23 @@
1
+ import { createCache } from 'cache-manager';
2
+ class MemoryCacheDriver {
3
+ cache;
4
+ constructor() {
5
+ this.cache = createCache();
6
+ }
7
+ async init() {}
8
+
9
+ _key(namespace, key) {
10
+ return `${namespace}:${key}`;
11
+ }
12
+ async get(namespace, key) {
13
+ const val = await this.cache.get(this._key(namespace, key));
14
+ return val ?? null;
15
+ }
16
+ async set(namespace, key, value, ttlMs) {
17
+ await this.cache.set(this._key(namespace, key), value, ttlMs);
18
+ }
19
+ async delete(namespace, key) {
20
+ await this.cache.del(this._key(namespace, key));
21
+ }
22
+ }
23
+ export default MemoryCacheDriver;
@@ -0,0 +1,28 @@
1
+ class RedisCacheDriver {
2
+ client;
3
+ prefix;
4
+ constructor(client, keyPrefix = 'arcway:') {
5
+ this.client = client;
6
+ this.prefix = `${keyPrefix}cache:`;
7
+ }
8
+ async init() {}
9
+
10
+ _key(namespace, key) {
11
+ return `${this.prefix}${namespace}:${key}`;
12
+ }
13
+ async get(namespace, key) {
14
+ return this.client.get(this._key(namespace, key));
15
+ }
16
+ async set(namespace, key, value, ttlMs) {
17
+ const redisKey = this._key(namespace, key);
18
+ if (ttlMs && ttlMs > 0) {
19
+ await this.client.set(redisKey, value, 'PX', ttlMs);
20
+ } else {
21
+ await this.client.set(redisKey, value);
22
+ }
23
+ }
24
+ async delete(namespace, key) {
25
+ await this.client.del(this._key(namespace, key));
26
+ }
27
+ }
28
+ export default RedisCacheDriver;
@@ -0,0 +1,69 @@
1
+ import MemoryCacheDriver from './drivers/memory.js';
2
+ import RedisCacheDriver from './drivers/redis.js';
3
+
4
+ class Cache {
5
+ _driver;
6
+ _log;
7
+ _namespace;
8
+
9
+ constructor(config, { redis, log } = {}) {
10
+ this._log = log;
11
+ this._namespace = config?.namespace ?? '';
12
+
13
+ if (config?.driver === 'redis') {
14
+ this._driver = new RedisCacheDriver(redis.client, redis.keyPrefix);
15
+ log?.info('Cache driver: redis');
16
+ } else {
17
+ this._driver = new MemoryCacheDriver();
18
+ log?.info('Cache driver: memory');
19
+ }
20
+ }
21
+
22
+ async init() {
23
+ await this._driver.init();
24
+ }
25
+
26
+ async get(key) {
27
+ const raw = await this._driver.get(this._namespace, key);
28
+ if (raw === null) return null;
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ this._log?.warn('Cache get: corrupt JSON, returning null', { key });
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async set(key, value, ttlMs) {
38
+ await this._driver.set(this._namespace, key, JSON.stringify(value), ttlMs);
39
+ }
40
+
41
+ async delete(key) {
42
+ await this._driver.delete(this._namespace, key);
43
+ }
44
+
45
+ async wrap(key, fn, ttlMs) {
46
+ const raw = await this._driver.get(this._namespace, key);
47
+ if (raw !== null) {
48
+ try {
49
+ return JSON.parse(raw);
50
+ } catch {
51
+ this._log?.warn('Cache wrap: corrupt JSON, recomputing', { key });
52
+ }
53
+ }
54
+ const value = await fn();
55
+ await this._driver.set(this._namespace, key, JSON.stringify(value), ttlMs);
56
+ return value;
57
+ }
58
+
59
+ /** Return a namespaced child that shares the same driver. */
60
+ withNamespace(namespace) {
61
+ const child = Object.create(Cache.prototype);
62
+ child._driver = this._driver;
63
+ child._log = this._log;
64
+ child._namespace = namespace;
65
+ return child;
66
+ }
67
+ }
68
+
69
+ export default Cache;
@@ -0,0 +1,89 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { loadModule } from '../module-loader.js';
4
+ import resolveServer from './modules/server.js';
5
+ import resolveApi from './modules/api.js';
6
+ import resolveDatabase from './modules/database.js';
7
+ import resolveLogger from './modules/logger.js';
8
+ import resolveJobs from './modules/jobs.js';
9
+ import resolveQueue from './modules/queue.js';
10
+ import resolveFiles from './modules/files.js';
11
+ import resolveRedis from './modules/redis.js';
12
+ import resolveCache from './modules/cache.js';
13
+ import resolveEvents from './modules/events.js';
14
+ import resolveMail from './modules/mail.js';
15
+ import resolveSession from './modules/session.js';
16
+ import resolvePages from './modules/pages.js';
17
+ import resolveBuild from './modules/build.js';
18
+ import resolveMcp from './modules/mcp.js';
19
+ import resolveWebsocket from './modules/websocket.js';
20
+
21
+ function deepMerge(target, source) {
22
+ const result = { ...target };
23
+ for (const [key, value] of Object.entries(source)) {
24
+ if (
25
+ value &&
26
+ typeof value === 'object' &&
27
+ !Array.isArray(value) &&
28
+ typeof result[key] === 'object' &&
29
+ result[key] !== null
30
+ ) {
31
+ result[key] = deepMerge(result[key], value);
32
+ } else if (value !== undefined) {
33
+ result[key] = value;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ const modules = [
40
+ resolveServer,
41
+ resolveApi,
42
+ resolveDatabase,
43
+ resolveLogger,
44
+ resolveJobs,
45
+ resolveQueue,
46
+ resolveCache,
47
+ resolveEvents,
48
+ resolveFiles,
49
+ resolveMail,
50
+ resolveRedis,
51
+ resolveSession,
52
+ resolvePages,
53
+ resolveBuild,
54
+ resolveMcp,
55
+ resolveWebsocket,
56
+ ];
57
+
58
+ async function makeConfig(rootDir, { overrides, mode } = {}) {
59
+ const configPath = path.join(rootDir, 'arcway.config.js');
60
+ if (!fs.existsSync(configPath)) {
61
+ throw new Error(`No arcway.config.js found in ${rootDir}`);
62
+ }
63
+
64
+ let module;
65
+ try {
66
+ module = await loadModule(configPath, import.meta.url);
67
+ } catch (err) {
68
+ throw new Error(`Failed to load arcway config at ${configPath}: ${err}`);
69
+ }
70
+
71
+ const raw = module.default;
72
+ if (!raw) {
73
+ throw new Error(`Arcway config at ${configPath} must have a default export`);
74
+ }
75
+ if (!raw.database) {
76
+ throw new Error('Arcway config must include a "database" configuration');
77
+ }
78
+
79
+ const merged = overrides ? deepMerge(raw, overrides) : raw;
80
+ const opts = { rootDir, mode };
81
+ let resolved = merged;
82
+ for (const resolve of modules) {
83
+ resolved = resolve(resolved, opts);
84
+ }
85
+
86
+ return resolved;
87
+ }
88
+
89
+ export { makeConfig };
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ enabled: true,
5
+ pathPrefix: '/api',
6
+ dir: 'api',
7
+ };
8
+
9
+ function resolve(config, { rootDir } = {}) {
10
+ const api = { ...DEFAULTS, ...config.api };
11
+ if (rootDir && api.dir && !path.isAbsolute(api.dir)) {
12
+ api.dir = path.resolve(rootDir, api.dir);
13
+ }
14
+ return { ...config, api };
15
+ }
16
+
17
+ export default resolve;
@@ -0,0 +1,9 @@
1
+ const DEFAULTS = {
2
+ target: 'node22',
3
+ };
4
+
5
+ function resolve(config) {
6
+ return { ...config, build: { ...DEFAULTS, ...config.build } };
7
+ }
8
+
9
+ export default resolve;
@@ -0,0 +1,10 @@
1
+ const DEFAULTS = {
2
+ driver: 'memory',
3
+ };
4
+
5
+ function resolve(config) {
6
+ if (!config.cache) return config;
7
+ return { ...config, cache: { ...DEFAULTS, ...config.cache } };
8
+ }
9
+
10
+ export default resolve;
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ sqliteFilename: '.build/db/arcway.db',
5
+ migrationsDir: 'migrations',
6
+ };
7
+
8
+ const CLIENT_MAP = {
9
+ sqlite: 'better-sqlite3',
10
+ postgres: 'pg',
11
+ mysql: 'mysql2',
12
+ };
13
+
14
+ function resolve(config, { rootDir } = {}) {
15
+ const db = { ...DEFAULTS, ...config.database };
16
+ // Resolve friendly client names to actual knex driver names
17
+ db.client = CLIENT_MAP[db.client] ?? db.client;
18
+ if (db.migrationsDir && !path.isAbsolute(db.migrationsDir)) {
19
+ db.migrationsDir = path.resolve(rootDir, db.migrationsDir);
20
+ }
21
+ const isSqlite = db.client === 'better-sqlite3' || db.client === 'sqlite3';
22
+ if (isSqlite && !db.connection) {
23
+ const filename = db.sqliteFilename;
24
+ db.connection = { filename: path.resolve(rootDir, filename) };
25
+ }
26
+ return { ...config, database: db };
27
+ }
28
+
29
+ export default resolve;
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ listenersDir: 'listeners',
5
+ };
6
+
7
+ function resolve(config, { rootDir } = {}) {
8
+ const events = { ...DEFAULTS, ...config.events };
9
+ if (events.listenersDir && !path.isAbsolute(events.listenersDir)) {
10
+ events.listenersDir = path.resolve(rootDir, events.listenersDir);
11
+ }
12
+ return { ...config, events };
13
+ }
14
+
15
+ export default resolve;
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ storageDir: '.build/storage',
5
+ };
6
+
7
+ function resolve(config, { rootDir } = {}) {
8
+ const files = { ...DEFAULTS, ...config.files };
9
+ if (files.storageDir && !path.isAbsolute(files.storageDir)) {
10
+ files.storageDir = path.resolve(rootDir, files.storageDir);
11
+ }
12
+ return { ...config, files };
13
+ }
14
+
15
+ export default resolve;
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ enabled: true,
5
+ backoffMs: 1000,
6
+ pollIntervalMs: 60000,
7
+ tableName: 'arcway_jobs',
8
+ leaseTable: 'arcway_job_leases',
9
+ jobsDir: 'jobs',
10
+ };
11
+
12
+ function resolve(config, { rootDir } = {}) {
13
+ const jobs = { ...DEFAULTS, ...config.jobs };
14
+ if (jobs.jobsDir && !path.isAbsolute(jobs.jobsDir)) {
15
+ jobs.jobsDir = path.resolve(rootDir, jobs.jobsDir);
16
+ }
17
+ return { ...config, jobs };
18
+ }
19
+
20
+ export default resolve;
@@ -0,0 +1,9 @@
1
+ const DEFAULTS = {
2
+ level: 'info',
3
+ };
4
+
5
+ function resolve(config) {
6
+ return { ...config, logger: { ...DEFAULTS, ...config.logger } };
7
+ }
8
+
9
+ export default resolve;
@@ -0,0 +1,11 @@
1
+ const DEFAULTS = {
2
+ driver: 'console',
3
+ enabled: true,
4
+ };
5
+
6
+ function resolve(config) {
7
+ if (!config.mail) return config;
8
+ return { ...config, mail: { ...DEFAULTS, ...config.mail } };
9
+ }
10
+
11
+ export default resolve;
@@ -0,0 +1,9 @@
1
+ const DEFAULTS = {
2
+ enabled: true,
3
+ };
4
+
5
+ function resolve(config) {
6
+ return { ...config, mcp: { ...DEFAULTS, ...config.mcp } };
7
+ }
8
+
9
+ export default resolve;