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.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- 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
|
+
};
|