thatcher 1.0.4

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,95 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { getDatabase } from '@/engine'
4
+ import { getAllMetrics } from '@/lib/metrics-collector.js'
5
+ import { getDatabaseStats } from '@/lib/db-monitor.js'
6
+ import { getCurrentResources } from '@/lib/resource-monitor.js'
7
+ import { getRecentAlerts } from '@/lib/alert-manager.js'
8
+
9
+ function getLastSyncAt() {
10
+ try {
11
+ const dir = path.resolve('data')
12
+ if (!fs.existsSync(dir)) return null
13
+ const files = fs.readdirSync(dir).map(f => path.join(dir, f)).filter(p => { try { return fs.statSync(p).isFile() } catch { return false } })
14
+ if (!files.length) return null
15
+ const mt = Math.max(...files.map(p => fs.statSync(p).mtimeMs))
16
+ return new Date(mt).toISOString()
17
+ } catch { return null }
18
+ }
19
+
20
+ export const GET = async (request) => {
21
+ try {
22
+ const db = getDatabase()
23
+ const start = process.hrtime.bigint()
24
+
25
+ db.prepare('SELECT 1').get()
26
+ db.pragma('wal_checkpoint(PASSIVE)')
27
+
28
+ const dbLatency = Number(process.hrtime.bigint() - start) / 1000000
29
+ const url = new URL(request.url)
30
+ const detailed = url.searchParams.get('detailed') === 'true'
31
+
32
+ const health = {
33
+ status: 'ok',
34
+ timestamp: new Date().toISOString(),
35
+ uptime: process.uptime(),
36
+ uptime_ms: Math.round(process.uptime() * 1000),
37
+ last_sync_at: getLastSyncAt(),
38
+ database: {
39
+ connected: true,
40
+ latency: dbLatency
41
+ },
42
+ db: 'ok'
43
+ }
44
+
45
+ if (detailed) {
46
+ const { getUser, setCurrentRequest } = await import('@/engine.server')
47
+ setCurrentRequest(request)
48
+ const user = await getUser()
49
+ if (user && (user.role === 'admin' || user.role === 'partner')) {
50
+ health.metrics = getAllMetrics()
51
+ health.database.stats = getDatabaseStats()
52
+ health.resources = getCurrentResources()
53
+ health.alerts = getRecentAlerts(10)
54
+ health.memory = {
55
+ heapUsed: process.memoryUsage().heapUsed,
56
+ heapTotal: process.memoryUsage().heapTotal,
57
+ external: process.memoryUsage().external,
58
+ rss: process.memoryUsage().rss
59
+ }
60
+ }
61
+ }
62
+
63
+ return new Response(
64
+ JSON.stringify(health, null, 2),
65
+ {
66
+ status: 200,
67
+ headers: { 'Content-Type': 'application/json' }
68
+ }
69
+ )
70
+ } catch (error) {
71
+ console.error('[Health] Check failed:', error)
72
+ return new Response(
73
+ JSON.stringify({
74
+ status: 'error',
75
+ error: error.message,
76
+ timestamp: new Date().toISOString()
77
+ }),
78
+ {
79
+ status: 503,
80
+ headers: { 'Content-Type': 'application/json' }
81
+ }
82
+ )
83
+ }
84
+ }
85
+
86
+ export const HEAD = async (request) => {
87
+ try {
88
+ const db = getDatabase()
89
+ db.prepare('SELECT 1').get()
90
+ return new Response(null, { status: 200 })
91
+ } catch (error) {
92
+ console.error('[Health] Check failed:', error)
93
+ return new Response(null, { status: 503 })
94
+ }
95
+ }
@@ -0,0 +1,73 @@
1
+ import { getAllMetrics, clearMetrics } from '@/lib/metrics-collector.js'
2
+ import { getRecentAlerts } from '@/lib/alert-manager.js'
3
+ import { getDatabaseStats } from '@/lib/db-monitor.js'
4
+ import { getCurrentResources } from '@/lib/resource-monitor.js'
5
+ import { getLogs } from '@/lib/log-aggregator.js'
6
+
7
+ export const GET = async (request) => {
8
+ try {
9
+ const { requireUser, setCurrentRequest } = await import('@/engine.server')
10
+ setCurrentRequest(request)
11
+ const user = await requireUser()
12
+ if (user.role !== 'admin' && user.role !== 'partner') {
13
+ return new Response(JSON.stringify({ error: 'Permission denied' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
14
+ }
15
+
16
+ const url = new URL(request.url)
17
+ const type = url.searchParams.get('type') || 'all'
18
+
19
+ let data = {}
20
+
21
+ if (type === 'all' || type === 'metrics') {
22
+ data.metrics = getAllMetrics()
23
+ }
24
+
25
+ if (type === 'all' || type === 'alerts') {
26
+ data.alerts = getRecentAlerts(50)
27
+ }
28
+
29
+ if (type === 'all' || type === 'database') {
30
+ data.database = getDatabaseStats()
31
+ }
32
+
33
+ if (type === 'all' || type === 'resources') {
34
+ data.resources = getCurrentResources()
35
+ }
36
+
37
+ if (type === 'all' || type === 'logs') {
38
+ const level = url.searchParams.get('level')
39
+ const since = url.searchParams.get('since')
40
+ const search = url.searchParams.get('search')
41
+ const limit = parseInt(url.searchParams.get('limit')) || 100
42
+
43
+ data.logs = getLogs({ level, since, search, limit })
44
+ }
45
+
46
+ return new Response(
47
+ JSON.stringify(data, null, 2),
48
+ {
49
+ status: 200,
50
+ headers: { 'Content-Type': 'application/json' }
51
+ }
52
+ )
53
+ } catch (error) {
54
+ const status = error.statusCode || (error.code === 'UNAUTHORIZED' ? 401 : 500);
55
+ return new Response(JSON.stringify({ error: error.message }), { status, headers: { 'Content-Type': 'application/json' } });
56
+ }
57
+ }
58
+
59
+ export const DELETE = async (request) => {
60
+ try {
61
+ const { requireUser, setCurrentRequest } = await import('@/engine.server')
62
+ setCurrentRequest(request)
63
+ const user = await requireUser()
64
+ if (user.role !== 'admin') {
65
+ return new Response(JSON.stringify({ error: 'Permission denied' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
66
+ }
67
+ clearMetrics()
68
+ return new Response(JSON.stringify({ success: true, message: 'Metrics cleared' }), { status: 200, headers: { 'Content-Type': 'application/json' } })
69
+ } catch (error) {
70
+ const status = error.statusCode || (error.code === 'UNAUTHORIZED' ? 401 : 500);
71
+ return new Response(JSON.stringify({ error: error.message }), { status, headers: { 'Content-Type': 'application/json' } });
72
+ }
73
+ }
@@ -0,0 +1,18 @@
1
+ import { renderMonitoringDashboard } from '@/ui/monitoring-dashboard.js'
2
+
3
+ export const GET = async (request) => {
4
+ const { getUser, setCurrentRequest } = await import('@/engine.server')
5
+ setCurrentRequest(request)
6
+ const user = await getUser()
7
+ if (!user) {
8
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
9
+ }
10
+ if (user.role !== 'admin' && user.role !== 'partner') {
11
+ return new Response(JSON.stringify({ error: 'Permission denied' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
12
+ }
13
+ const html = renderMonitoringDashboard()
14
+ return new Response(html, {
15
+ status: 200,
16
+ headers: { 'Content-Type': 'text/html; charset=utf-8' }
17
+ })
18
+ }
package/src/cli.js ADDED
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Thatcher CLI
5
+ * Usage:
6
+ * thatcher start - Start server with auto-discovered config
7
+ * thatcher dev - Start server with hot reload
8
+ * thatcher console - Open REPL with thatcher API
9
+ * thatcher migrate - Run database migrations only
10
+ * thatcher validate - Validate configuration
11
+ * thatcher example - Generate example config
12
+ */
13
+
14
+ import { startThatcher, Thatcher } from './index.js';
15
+ import { command } from 'commander'; // We'll use simple args parsing
16
+ import * as readline from 'readline';
17
+
18
+ const args = process.argv.slice(2);
19
+ const command = args[0];
20
+
21
+ async function main() {
22
+ switch (command) {
23
+ case 'start':
24
+ case 'dev':
25
+ console.log(`[Thatcher] Starting in ${command} mode...`);
26
+ const thatcher = new Thatcher({
27
+ server: { hotReload: command === 'dev' },
28
+ });
29
+ try {
30
+ await thatcher.init();
31
+ await thatcher.startServer();
32
+ } catch (err) {
33
+ console.error('[Thatcher] Failed to start:', err.message);
34
+ if (err.stack) console.error(err.stack);
35
+ process.exit(1);
36
+ }
37
+ break;
38
+
39
+ case 'migrate':
40
+ console.log('[Thatcher] Running migrations...');
41
+ const t1 = new Thatcher({});
42
+ await t1.init();
43
+ console.log('✓ Migrations complete');
44
+ break;
45
+
46
+ case 'validate':
47
+ console.log('[Thatcher] Validating configuration...');
48
+ try {
49
+ const t2 = new Thatcher({});
50
+ await t2.init();
51
+ console.log('✓ Configuration is valid');
52
+ console.log(` Entities: ${t2.getAllEntities().length}`);
53
+ console.log(` Workflows: ${Object.keys(t2.config?.workflows || {}).length}`);
54
+ } catch (err) {
55
+ console.error('✗ Configuration error:', err.message);
56
+ process.exit(1);
57
+ }
58
+ break;
59
+
60
+ case 'console':
61
+ case 'repl':
62
+ await startRepl();
63
+ break;
64
+
65
+ case 'example':
66
+ generateExampleConfig();
67
+ break;
68
+
69
+ case '--help':
70
+ case '-h':
71
+ case 'help':
72
+ printHelp();
73
+ break;
74
+
75
+ default:
76
+ if (!command) {
77
+ printHelp();
78
+ } else {
79
+ console.error(`[Thatcher] Unknown command: ${command}`);
80
+ console.log('Run `thatcher help` for usage.');
81
+ process.exit(1);
82
+ }
83
+ }
84
+ }
85
+
86
+ async function startRepl() {
87
+ console.log('[Thatcher] Starting REPL...');
88
+ console.log('Available: thatcher (instance), create, get, list, etc.\n');
89
+
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout,
93
+ prompt: 'thatcher> ',
94
+ });
95
+
96
+ const thatcher = new Thatcher({});
97
+ await thatcher.init();
98
+
99
+ // Expose thatcher in REPL context
100
+ const context = {
101
+ thatcher,
102
+ create: async (entity, data, user) => thatcher.create(entity, data, user),
103
+ get: async (entity, id) => thatcher.get(entity, id),
104
+ list: async (entity, where) => thatcher.list(entity, where),
105
+ update: async (entity, id, data, user) => thatcher.update(entity, id, data, user),
106
+ remove: async (entity, id) => thatcher.delete(entity, id),
107
+ search: async (entity, q, where) => thatcher.search(entity, q, where),
108
+ transition: async (entityType, entityId, wf, toState, user, reason) =>
109
+ thatcher.transition(entityType, entityId, wf, toState, user, reason),
110
+ config: thatcher.config,
111
+ entities: thatcher.getAllEntities(),
112
+ };
113
+
114
+ rl.prompt();
115
+
116
+ rl.on('line', async (line) => {
117
+ const trimmed = line.trim();
118
+ if (!trimmed) {
119
+ rl.prompt();
120
+ return;
121
+ }
122
+
123
+ try {
124
+ // Simple evaluation - in production use a proper sandbox
125
+ const result = eval(trimmed);
126
+ if (result && typeof result.then === 'function') {
127
+ const resolved = await result;
128
+ console.log(JSON.stringify(resolved, null, 2));
129
+ } else if (result !== undefined) {
130
+ console.log(JSON.stringify(result, null, 2));
131
+ }
132
+ } catch (err) {
133
+ console.error('Error:', err.message);
134
+ }
135
+ rl.prompt();
136
+ });
137
+
138
+ rl.on('close', () => {
139
+ console.log('\n[Thatcher] Goodbye');
140
+ process.exit(0);
141
+ });
142
+ }
143
+
144
+ function generateExampleConfig() {
145
+ const exampleDir = path.resolve(process.cwd(), 'thatcher-example');
146
+ fs.mkdirSync(exampleDir, { recursive: true });
147
+
148
+ const config = `# Thatcher Example Configuration
149
+ # This is a minimal config to get you started
150
+ # Copy moonlanding's master-config.yml for full features
151
+
152
+ roles:
153
+ admin:
154
+ hierarchy: 0
155
+ label: Admin
156
+ permissions_scope: global
157
+ user:
158
+ hierarchy: 1
159
+ label: User
160
+ permissions_scope: assigned
161
+
162
+ permission_templates:
163
+ basic:
164
+ admin:
165
+ - list
166
+ - view
167
+ - create
168
+ - edit
169
+ - delete
170
+ user:
171
+ - list
172
+ - view
173
+
174
+ entities:
175
+ item:
176
+ label: Item
177
+ label_plural: Items
178
+ fields:
179
+ name:
180
+ type: text
181
+ required: true
182
+ description:
183
+ type: textarea
184
+ status:
185
+ type: enum
186
+ options: ['active', 'archived']
187
+ default: active
188
+
189
+ workflows:
190
+ simple_workflow:
191
+ stages:
192
+ - draft
193
+ - active
194
+ - completed
195
+
196
+ thresholds:
197
+ system:
198
+ pagination:
199
+ default_page_size: 20
200
+ max_page_size: 100
201
+ `;
202
+
203
+ fs.writeFileSync(path.join(exampleDir, 'thatcher.config.yml'), config);
204
+ console.log(`✓ Example config written to ${exampleDir}/`);
205
+ console.log(' cd', exampleDir);
206
+ console.log(' thatcher start');
207
+ }
208
+
209
+ function printHelp() {
210
+ console.log(`
211
+ Thatcher CLI - Configuration-Driven Application Framework
212
+
213
+ Usage:
214
+ thatcher <command>
215
+
216
+ Commands:
217
+ start Start the server (production mode)
218
+ dev Start the server with hot reload
219
+ migrate Run database migrations only
220
+ validate Validate configuration file
221
+ console Open interactive REPL
222
+ example Generate example configuration
223
+ help Show this help
224
+
225
+ Configuration:
226
+ Place a thatcher.config.yml or master-config.yml in the current directory.
227
+ See the documentation for full configuration options.
228
+
229
+ Environment Variables:
230
+ DATABASE_PATH Path to SQLite database (default: ./data/app.db)
231
+ PORT Server port (default: 3000)
232
+ NODE_ENV Environment (development|production)
233
+
234
+ Quick Start:
235
+ thatcher example # Generate a starter config
236
+ thatcher start # Launch the server
237
+ `);
238
+ }
239
+
240
+ main().catch((err) => {
241
+ console.error('[Thatcher] Fatal error:', err);
242
+ process.exit(1);
243
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Configuration loading and management
3
+ * Adapted from moonlanding/src/config/
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import yaml from 'js-yaml';
9
+
10
+ /**
11
+ * Load configuration from YAML file or object
12
+ */
13
+ export async function loadConfig(configSource) {
14
+ let config;
15
+
16
+ if (typeof configSource === 'string') {
17
+ // File path
18
+ const configPath = path.resolve(process.cwd(), configSource);
19
+ if (!fs.existsSync(configPath)) {
20
+ throw new Error(`Configuration file not found: ${configPath}`);
21
+ }
22
+ const content = fs.readFileSync(configPath, 'utf-8');
23
+ config = yaml.load(content);
24
+ } else if (typeof configSource === 'object') {
25
+ // Raw object
26
+ config = configSource;
27
+ } else {
28
+ throw new Error('Config must be a file path or object');
29
+ }
30
+
31
+ return config;
32
+ }
33
+
34
+ /**
35
+ * Validate required configuration sections
36
+ */
37
+ export function validateConfig(config) {
38
+ const errors = [];
39
+
40
+ if (!config) {
41
+ errors.push('Configuration is empty');
42
+ return errors;
43
+ }
44
+
45
+ // Check required sections
46
+ const requiredSections = ['entities', 'roles', 'permission_templates'];
47
+ for (const section of requiredSections) {
48
+ if (!config[section]) {
49
+ errors.push(`Missing required section: ${section}`);
50
+ }
51
+ }
52
+
53
+ // Validate roles
54
+ if (config.roles) {
55
+ const roleNames = Object.keys(config.roles);
56
+ if (roleNames.length === 0) {
57
+ errors.push('At least one role must be defined');
58
+ }
59
+ }
60
+
61
+ // Validate entities
62
+ if (config.entities) {
63
+ for (const [entityName, entityDef] of Object.entries(config.entities)) {
64
+ if (!entityDef.fields && !entityDef.children) {
65
+ errors.push(`Entity '${entityName}' must have fields or children defined`);
66
+ }
67
+ }
68
+ }
69
+
70
+ return errors;
71
+ }
72
+
73
+ /**
74
+ * Get configuration with defaults applied
75
+ */
76
+ export function getConfigWithDefaults(userConfig) {
77
+ const defaults = {
78
+ system: {
79
+ pagination: { default_page_size: 50, max_page_size: 500 },
80
+ limits: { max_upload_size: 10485760, max_query_results: 10000 },
81
+ },
82
+ thresholds: {
83
+ timing: { lockout_seconds: 300 },
84
+ ui: { skeleton_count: 3 },
85
+ polling: { chat_ms: 3000, staleness_days: 8 },
86
+ },
87
+ features: {},
88
+ notifications: {},
89
+ automation: { schedules: [] },
90
+ domains: {},
91
+ highlights: { palette: [] },
92
+ icons: {},
93
+ theme: {},
94
+ };
95
+
96
+ return deepMerge(defaults, userConfig);
97
+ }
98
+
99
+ /**
100
+ * Deep merge utility
101
+ */
102
+ function deepMerge(target, source) {
103
+ const result = { ...target };
104
+ for (const key of Object.keys(source)) {
105
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
106
+ result[key] = deepMerge(target[key] || {}, source[key]);
107
+ } else {
108
+ result[key] = source[key];
109
+ }
110
+ }
111
+ return result;
112
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Core constants for Thatcher SDK
3
+ * HTTP codes, status values, display limits, etc.
4
+ */
5
+
6
+ /**
7
+ * HTTP status codes
8
+ */
9
+ export const HTTP = {
10
+ OK: 200,
11
+ CREATED: 201,
12
+ ACCEPTED: 202,
13
+ NO_CONTENT: 204,
14
+ BAD_REQUEST: 400,
15
+ UNAUTHORIZED: 401,
16
+ FORBIDDEN: 403,
17
+ NOT_FOUND: 404,
18
+ CONFLICT: 409,
19
+ UNPROCESSABLE_ENTITY: 422,
20
+ INTERNAL_ERROR: 500,
21
+ SERVICE_UNAVAILABLE: 503,
22
+ };
23
+
24
+ /**
25
+ * Error message templates
26
+ */
27
+ export const ERROR_MESSAGES = {
28
+ notFound: (entity = 'Resource') => `${entity} not found`,
29
+ invalidRequest: (reason = 'Invalid request') => reason,
30
+ operationFailed: (operation = 'Operation') => `${operation} failed`,
31
+ permission: {
32
+ denied: 'Permission denied',
33
+ },
34
+ };
35
+
36
+ /**
37
+ * Success message templates
38
+ */
39
+ export const SUCCESS_MESSAGES = {
40
+ created: (entity = 'Item') => `${entity} created successfully`,
41
+ updated: (entity = 'Item') => `${entity} updated successfully`,
42
+ deleted: (entity = 'Item') => `${entity} deleted successfully`,
43
+ saved: 'Changes saved successfully',
44
+ };
45
+
46
+ /**
47
+ * Record status constants
48
+ */
49
+ export const RECORD_STATUS = {
50
+ ACTIVE: 'active',
51
+ DELETED: 'deleted',
52
+ ARCHIVED: 'archived',
53
+ };
54
+
55
+ /**
56
+ * Email status
57
+ */
58
+ export const EMAIL_STATUS = 'pending';
59
+
60
+ /**
61
+ * Display/UI constants
62
+ */
63
+ export const DISPLAY = {
64
+ MAX_API_CALLS_HISTORY: 100,
65
+ API_TIMEOUT_MS: 30000,
66
+ POLLING_INTERVAL_MS: 2000,
67
+ MAX_INLINE_ITEMS: 5,
68
+ MAX_UPLOAD_SIZE_MB: 100,
69
+ MAX_FILE_NAME_LENGTH: 255,
70
+ MAX_FIELD_NAME_LENGTH: 100,
71
+ TOAST_DURATION_MS: 3000,
72
+ MAX_NOTIFICATIONS: 50,
73
+ DEBOUNCE_SEARCH_MS: 300,
74
+ DEBOUNCE_FORM_CHANGE_MS: 500,
75
+ };
76
+
77
+ /**
78
+ * Validation patterns
79
+ */
80
+ export const VALIDATION = {
81
+ EMAIL_REGEX: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
82
+ PASSWORD_MIN_LENGTH: 8,
83
+ };
84
+
85
+ /**
86
+ * Log prefixes for structured logging
87
+ */
88
+ export const LOG_PREFIXES = {
89
+ API: '[API]',
90
+ DB: '[DB]',
91
+ AUTH: '[AUTH]',
92
+ CONFIG: '[Config]',
93
+ SERVICE: '[Service]',
94
+ system: '[System]',
95
+ email: '[Email]',
96
+ validation: '[Validation]',
97
+ database: '[DB]',
98
+ };
99
+
100
+ /**
101
+ * SQL type mapping
102
+ */
103
+ export const SQL_TYPES = {
104
+ id: 'TEXT PRIMARY KEY',
105
+ text: 'TEXT',
106
+ textarea: 'TEXT',
107
+ email: 'TEXT',
108
+ int: 'INTEGER',
109
+ decimal: 'REAL',
110
+ bool: 'INTEGER',
111
+ date: 'INTEGER',
112
+ timestamp: 'INTEGER',
113
+ json: 'TEXT',
114
+ image: 'TEXT',
115
+ ref: 'TEXT',
116
+ enum: 'TEXT',
117
+ };
118
+
119
+ /**
120
+ * Authentication scopes (Google, etc.)
121
+ */
122
+ export const AUTH_SCOPES = {
123
+ google: [
124
+ 'https://www.googleapis.com/auth/userinfo.email',
125
+ 'https://www.googleapis.com/auth/userinfo.profile',
126
+ ],
127
+ };