nucleus-core-ts 0.10.145 → 0.10.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -177
- package/bin/cli.ts +136 -136
- package/bin/lib/commands/add.ts +248 -248
- package/bin/lib/commands/dev.ts +213 -213
- package/bin/lib/commands/doctor.ts +91 -91
- package/bin/lib/commands/init.ts +147 -147
- package/bin/lib/commands/validate.ts +137 -137
- package/bin/lib/config-builder.ts +84 -84
- package/bin/lib/env.ts +85 -85
- package/bin/lib/presets.ts +124 -124
- package/bin/lib/schema.ts +132 -132
- package/bin/lib/templates.ts +154 -154
- package/bin/lib/ui.ts +126 -126
- package/dist/.build-ok +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -1
- package/dist/fe/components/AuthGuard/store.js +11 -11
- package/dist/fe/components/VerificationFlowPage/components/PendingTab.js +107 -107
- package/dist/fe/components/VerificationFlowPage/components/PropertiesPanel.js +6 -6
- package/dist/fe/components/VerificationFlowPage/components/VerificationEntityList.js +58 -58
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +258 -258
- package/dist/fe/components/VerificationFlowPage/components/VerificationStatusCard.js +19 -19
- package/dist/fe/components/VerificationFlowPage/components/decisionPlan.js +108 -108
- package/dist/fe/components/VerificationFlowPage/components/recordLabels.js +27 -27
- package/dist/fe/components/VerificationFlowPage/components/supersededCard.js +9 -9
- package/dist/fe/components/VerificationFlowPage/components/useEntityLabels.js +8 -8
- package/dist/fe/components/VerificationFlowPage/index.js +17 -17
- package/dist/fe/components/VerificationFlowPage/labels.js +36 -36
- package/dist/fe/components/VerificationFlowPage/model2/ActionRows.js +212 -212
- package/dist/fe/components/VerificationFlowPage/model2/AssigneeRows.js +37 -37
- package/dist/fe/components/VerificationFlowPage/model2/FlowDiagram.js +175 -175
- package/dist/fe/components/VerificationFlowPage/model2/OutcomeRows.js +49 -49
- package/dist/fe/components/VerificationFlowPage/model2/ReadinessCard.js +65 -65
- package/dist/fe/components/VerificationFlowPage/model2/StepCard.js +15 -15
- package/dist/fe/components/VerificationFlowPage/model2/StepListEditor.js +75 -75
- package/dist/fe/components/VerificationFlowPage/model2/editorGraph.js +190 -190
- package/dist/fe/components/VerificationFlowPage/model2/readinessText.js +4 -4
- package/dist/fe/components/VerificationFlowPage/model2/theme.js +17 -17
- package/dist/fe/components/VerificationFlowPage/stepOrder.js +19 -19
- package/dist/fe/components/VerificationFlowPage/store/index.js +76 -76
- package/dist/fe/components/VerificationFlowPage/theme/index.js +7 -7
- package/dist/fe/components/VerificationFlowPage/types/index.js +7 -7
- package/dist/fe/components/VerificationPanel/components/PendingVerifications.js +30 -30
- package/dist/fe/components/VerificationPanel/components/VerificationPanel.js +25 -25
- package/dist/fe/utils/newId.js +20 -20
- package/dist/index.js +5 -5
- package/dist/server.js +4 -4
- package/dist/src/Client/Proxy/httpProxy.d.ts +37 -5
- package/dist/src/Client/Proxy/httpProxy.js +145 -77
- package/dist/src/Client/Proxy/index.d.ts +3 -0
- package/dist/src/Client/Proxy/index.js +2 -0
- package/dist/src/Client/Proxy/refreshOutcome.d.ts +40 -0
- package/dist/src/Client/Proxy/refreshOutcome.js +54 -0
- package/dist/src/Client/Proxy/synthesizedAuth.d.ts +18 -3
- package/dist/src/Client/Proxy/synthesizedAuth.js +23 -10
- package/dist/src/Client/Proxy/types.d.ts +13 -0
- package/dist/src/Client/Proxy/wsProxy.d.ts +1 -0
- package/dist/src/Client/Proxy/wsProxy.js +35 -7
- package/dist/src/Client/PubSub/usePubSub.js +101 -99
- package/dist/src/Client/PubSub/waitForReturn.js +29 -29
- package/dist/src/Client/Upload/progress.js +31 -31
- package/dist/src/Client/Upload/types.js +7 -7
- package/dist/src/Client/Upload/upload.js +21 -21
- package/dist/src/Nucleus/routes/auth/refresh/sessionBinding.d.ts +9 -0
- package/dist/src/Services/Auth/SessionStore/Read/index.d.ts +25 -0
- package/infra/templates/backend/package.json +22 -22
- package/infra/templates/backend/src/index.ts +33 -33
- package/package.json +1 -1
- package/public/components/docs/content/advanced.ts +506 -506
- package/public/components/docs/content/gettingStarted.ts +411 -411
- package/schemas/nucleus.tables.schema.json +19 -19
- package/schemas/table.schema.json +509 -509
- package/scripts/generate-schema.test.ts +399 -399
- package/scripts/generate-schema.ts +810 -810
- package/scripts/importCase.test.ts +174 -174
- package/scripts/scaffoldParity.test.ts +126 -126
- package/scripts/version.ts +127 -127
- package/src/Services/Logger/auditTaxonomy.ts +154 -154
- package/src/Services/SchemaTables/resolveEntities.ts +214 -214
- package/src/system.tables.json +6035 -6035
package/bin/lib/commands/add.ts
CHANGED
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `nucleus add <thing>` — extend an existing project's config.json in place.
|
|
3
|
-
*
|
|
4
|
-
* nucleus add entity interactively add one or more entities
|
|
5
|
-
* nucleus add feature <name> enable a feature block (storage, chat,
|
|
6
|
-
* payment, notification, verification, audit,
|
|
7
|
-
* captcha, pubsub) with schema-valid defaults
|
|
8
|
-
* nucleus add oauth <provider> add an OAuth provider to authentication
|
|
9
|
-
*
|
|
10
|
-
* After editing, it re-validates and (for schema-affecting changes) regenerates
|
|
11
|
-
* the Drizzle schema so `nucleus dev` picks the change up.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { spawnSync } from 'node:child_process'
|
|
15
|
-
import { dirname, join, relative } from 'node:path'
|
|
16
|
-
import { buildEntity } from '../config-builder'
|
|
17
|
-
import { loadConfigSchema, validateAgainstSchema } from '../schema'
|
|
18
|
-
import { c, fail, info, log, ok, prompts, step, warn } from '../ui'
|
|
19
|
-
import { crossFieldChecks, findConfigPath } from './validate'
|
|
20
|
-
|
|
21
|
-
type Cfg = Record<string, unknown>
|
|
22
|
-
|
|
23
|
-
/** Feature blocks keyed by name — each returns a schema-valid default config fragment. */
|
|
24
|
-
const FEATURES: Record<string, { summary: string; apply: (cfg: Cfg) => void | Promise<void> }> = {
|
|
25
|
-
storage: {
|
|
26
|
-
summary: 'file storage + CDN (upload/serve, ownership-scoped)',
|
|
27
|
-
apply: (cfg) => {
|
|
28
|
-
cfg.storage = {
|
|
29
|
-
enabled: true,
|
|
30
|
-
provider: 'local',
|
|
31
|
-
local: { basePath: './.storage' },
|
|
32
|
-
cdn: { enabled: true },
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
chat: {
|
|
37
|
-
summary: 'realtime chat channels + messages',
|
|
38
|
-
apply: (cfg) => {
|
|
39
|
-
cfg.chat = { enabled: true, attachments: { enabled: false } }
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
notification: {
|
|
43
|
-
summary: 'notification channels (email/telegram/webhook)',
|
|
44
|
-
apply: (cfg) => {
|
|
45
|
-
cfg.notification = { enabled: true, channels: { email: false } }
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
verification: {
|
|
49
|
-
summary: 'signed verification / approval flows',
|
|
50
|
-
apply: (cfg) => {
|
|
51
|
-
cfg.verification = { enabled: true }
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
audit: {
|
|
55
|
-
summary: 'audit logging of mutations',
|
|
56
|
-
apply: (cfg) => {
|
|
57
|
-
cfg.audit = { enabled: true }
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
captcha: {
|
|
61
|
-
summary: 'CAPTCHA challenge on sensitive endpoints',
|
|
62
|
-
apply: (cfg) => {
|
|
63
|
-
cfg.captcha = { enabled: true, provider: 'turnstile', secretKey: 'CAPTCHA_SECRET_KEY' }
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
pubsub: {
|
|
67
|
-
summary: 'Dapr pub/sub event fan-out',
|
|
68
|
-
apply: (cfg) => {
|
|
69
|
-
cfg.pubsub = { enabled: true }
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
payment: {
|
|
73
|
-
summary: 'payment provider (iyzico | stripe)',
|
|
74
|
-
apply: async (cfg) => {
|
|
75
|
-
const p = prompts()
|
|
76
|
-
try {
|
|
77
|
-
const provider = await p.choose(' Provider', ['iyzico', 'stripe'], 0)
|
|
78
|
-
const block: Cfg = { enabled: true, provider }
|
|
79
|
-
if (provider === 'iyzico')
|
|
80
|
-
block.iyzico = {
|
|
81
|
-
apiKey: 'IYZICO_API_KEY',
|
|
82
|
-
secretKey: 'IYZICO_SECRET_KEY',
|
|
83
|
-
baseUrl: 'IYZICO_BASE_URL',
|
|
84
|
-
}
|
|
85
|
-
else
|
|
86
|
-
block.stripe = { secretKey: 'STRIPE_SECRET_KEY', webhookSecret: 'STRIPE_WEBHOOK_SECRET' }
|
|
87
|
-
cfg.payment = block
|
|
88
|
-
} finally {
|
|
89
|
-
p.close()
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const OAUTH_PROVIDERS = ['google', 'microsoft', 'github', 'apple']
|
|
96
|
-
|
|
97
|
-
async function readConfig(): Promise<{ path: string; cfg: Cfg } | null> {
|
|
98
|
-
const path = findConfigPath()
|
|
99
|
-
if (!path) {
|
|
100
|
-
fail('No config.json found here — run this from a Nucleus project (or `nucleus init` first)')
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
return { path, cfg: JSON.parse(await Bun.file(path).text()) as Cfg }
|
|
105
|
-
} catch {
|
|
106
|
-
fail(`Config ${path} is not valid JSON`)
|
|
107
|
-
return null
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Persist, validate, and (if schema-affecting) regenerate Drizzle. */
|
|
112
|
-
async function persist(path: string, cfg: Cfg, regen: boolean): Promise<number> {
|
|
113
|
-
const schema = await loadConfigSchema()
|
|
114
|
-
const res = validateAgainstSchema(cfg, schema)
|
|
115
|
-
const cross = crossFieldChecks(cfg)
|
|
116
|
-
if (res.errors.length + cross.length > 0) {
|
|
117
|
-
for (const e of [...res.errors, ...cross]) fail(`${c.bold(e.path || '(root)')} — ${e.message}`)
|
|
118
|
-
fail('Change would make the config invalid — nothing written')
|
|
119
|
-
return 1
|
|
120
|
-
}
|
|
121
|
-
await Bun.write(path, `${JSON.stringify(cfg, null, 2)}\n`)
|
|
122
|
-
ok(`Updated ${c.cyan(relative(process.cwd(), path))}`)
|
|
123
|
-
|
|
124
|
-
if (regen) {
|
|
125
|
-
const outDir = join(dirname(path), 'drizzle')
|
|
126
|
-
step(`Regenerating Drizzle schema → ${c.dim(relative(process.cwd(), outDir))}`)
|
|
127
|
-
const r = spawnSync('bunx', ['nucleus-core-ts', 'generate', path, outDir], { stdio: 'inherit' })
|
|
128
|
-
if (r.status !== 0) {
|
|
129
|
-
warn('Schema regeneration failed — run `bunx nucleus-core-ts generate` manually')
|
|
130
|
-
return 1
|
|
131
|
-
}
|
|
132
|
-
ok('Schema regenerated')
|
|
133
|
-
}
|
|
134
|
-
return 0
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function addEntity(): Promise<number> {
|
|
138
|
-
const loaded = await readConfig()
|
|
139
|
-
if (!loaded) return 1
|
|
140
|
-
const { path, cfg } = loaded
|
|
141
|
-
const entities = Array.isArray(cfg.entities) ? (cfg.entities as Cfg[]) : []
|
|
142
|
-
const existing = new Set(entities.map((e) => (e as { table_name?: string }).table_name))
|
|
143
|
-
|
|
144
|
-
step('Add entities (blank table name to finish)')
|
|
145
|
-
const p = prompts()
|
|
146
|
-
let added = 0
|
|
147
|
-
try {
|
|
148
|
-
let e = await buildEntity(p)
|
|
149
|
-
while (e) {
|
|
150
|
-
const tn = (e as { table_name: string }).table_name
|
|
151
|
-
if (existing.has(tn)) warn(`Entity "${tn}" already exists — skipped`)
|
|
152
|
-
else {
|
|
153
|
-
entities.push(e)
|
|
154
|
-
existing.add(tn)
|
|
155
|
-
ok(`Queued entity "${tn}"`)
|
|
156
|
-
added++
|
|
157
|
-
}
|
|
158
|
-
e = await buildEntity(p)
|
|
159
|
-
}
|
|
160
|
-
} finally {
|
|
161
|
-
p.close()
|
|
162
|
-
}
|
|
163
|
-
if (added === 0) {
|
|
164
|
-
info('No entities added')
|
|
165
|
-
return 0
|
|
166
|
-
}
|
|
167
|
-
cfg.entities = entities
|
|
168
|
-
return persist(path, cfg, true)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function addFeature(name?: string): Promise<number> {
|
|
172
|
-
const key = (name || '').toLowerCase()
|
|
173
|
-
if (!key || !FEATURES[key]) {
|
|
174
|
-
fail(`Unknown feature "${name ?? ''}"`)
|
|
175
|
-
log(` Available features:`)
|
|
176
|
-
for (const [k, v] of Object.entries(FEATURES))
|
|
177
|
-
log(` ${c.cyan(k.padEnd(14))} ${c.dim(v.summary)}`)
|
|
178
|
-
return 1
|
|
179
|
-
}
|
|
180
|
-
const loaded = await readConfig()
|
|
181
|
-
if (!loaded) return 1
|
|
182
|
-
const { path, cfg } = loaded
|
|
183
|
-
if ((cfg[key] as Cfg | undefined)?.enabled) {
|
|
184
|
-
info(`Feature "${key}" is already enabled — leaving it as-is`)
|
|
185
|
-
return 0
|
|
186
|
-
}
|
|
187
|
-
step(`Enabling feature: ${c.cyan(key)} ${c.dim(`— ${FEATURES[key].summary}`)}`)
|
|
188
|
-
await FEATURES[key].apply(cfg)
|
|
189
|
-
const rc = await persist(path, cfg, false)
|
|
190
|
-
if (rc === 0) info('Secrets are referenced by ENV-VAR NAME — set them in .env before running.')
|
|
191
|
-
return rc
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function addOauth(provider?: string): Promise<number> {
|
|
195
|
-
const prov = (provider || '').toLowerCase()
|
|
196
|
-
if (!prov || !OAUTH_PROVIDERS.includes(prov)) {
|
|
197
|
-
fail(
|
|
198
|
-
`Unknown OAuth provider "${provider ?? ''}" — expected one of ${OAUTH_PROVIDERS.join(', ')}`
|
|
199
|
-
)
|
|
200
|
-
return 1
|
|
201
|
-
}
|
|
202
|
-
const loaded = await readConfig()
|
|
203
|
-
if (!loaded) return 1
|
|
204
|
-
const { path, cfg } = loaded
|
|
205
|
-
const auth = (cfg.authentication as Cfg | undefined) ?? {}
|
|
206
|
-
if (!auth.enabled) {
|
|
207
|
-
fail('authentication is not enabled — enable it first (an `api` or `saas` preset has it on)')
|
|
208
|
-
return 1
|
|
209
|
-
}
|
|
210
|
-
const oauth = (auth.oauth as Cfg | undefined) ?? {}
|
|
211
|
-
if (oauth[prov]) {
|
|
212
|
-
info(`OAuth provider "${prov}" is already configured`)
|
|
213
|
-
return 0
|
|
214
|
-
}
|
|
215
|
-
const up = prov.toUpperCase()
|
|
216
|
-
oauth[prov] = { clientId: `${up}_CLIENT_ID`, clientSecret: `${up}_CLIENT_SECRET` }
|
|
217
|
-
auth.oauth = oauth
|
|
218
|
-
cfg.authentication = auth
|
|
219
|
-
step(`Adding OAuth provider: ${c.cyan(prov)}`)
|
|
220
|
-
const rc = await persist(path, cfg, false)
|
|
221
|
-
if (rc === 0)
|
|
222
|
-
info(`Set ${up}_CLIENT_ID and ${up}_CLIENT_SECRET in .env, plus the provider redirect URI.`)
|
|
223
|
-
return rc
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function addCommand(args: string[]): Promise<number> {
|
|
227
|
-
const thing = args[0]
|
|
228
|
-
switch (thing) {
|
|
229
|
-
case 'entity':
|
|
230
|
-
case 'entities':
|
|
231
|
-
return addEntity()
|
|
232
|
-
case 'feature':
|
|
233
|
-
return addFeature(args[1])
|
|
234
|
-
case 'oauth':
|
|
235
|
-
return addOauth(args[1])
|
|
236
|
-
default:
|
|
237
|
-
fail(`Unknown: nucleus add ${thing ?? ''}`)
|
|
238
|
-
log(` Usage:`)
|
|
239
|
-
log(
|
|
240
|
-
` ${c.cyan('nucleus add entity')} ${c.dim('add one or more entities (interactive)')}`
|
|
241
|
-
)
|
|
242
|
-
log(` ${c.cyan('nucleus add feature <name>')} ${c.dim('enable a feature block')}`)
|
|
243
|
-
log(
|
|
244
|
-
` ${c.cyan('nucleus add oauth <provider>')} ${c.dim(`add OAuth (${OAUTH_PROVIDERS.join('|')})`)}`
|
|
245
|
-
)
|
|
246
|
-
return 1
|
|
247
|
-
}
|
|
248
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `nucleus add <thing>` — extend an existing project's config.json in place.
|
|
3
|
+
*
|
|
4
|
+
* nucleus add entity interactively add one or more entities
|
|
5
|
+
* nucleus add feature <name> enable a feature block (storage, chat,
|
|
6
|
+
* payment, notification, verification, audit,
|
|
7
|
+
* captcha, pubsub) with schema-valid defaults
|
|
8
|
+
* nucleus add oauth <provider> add an OAuth provider to authentication
|
|
9
|
+
*
|
|
10
|
+
* After editing, it re-validates and (for schema-affecting changes) regenerates
|
|
11
|
+
* the Drizzle schema so `nucleus dev` picks the change up.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { dirname, join, relative } from 'node:path'
|
|
16
|
+
import { buildEntity } from '../config-builder'
|
|
17
|
+
import { loadConfigSchema, validateAgainstSchema } from '../schema'
|
|
18
|
+
import { c, fail, info, log, ok, prompts, step, warn } from '../ui'
|
|
19
|
+
import { crossFieldChecks, findConfigPath } from './validate'
|
|
20
|
+
|
|
21
|
+
type Cfg = Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
/** Feature blocks keyed by name — each returns a schema-valid default config fragment. */
|
|
24
|
+
const FEATURES: Record<string, { summary: string; apply: (cfg: Cfg) => void | Promise<void> }> = {
|
|
25
|
+
storage: {
|
|
26
|
+
summary: 'file storage + CDN (upload/serve, ownership-scoped)',
|
|
27
|
+
apply: (cfg) => {
|
|
28
|
+
cfg.storage = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
provider: 'local',
|
|
31
|
+
local: { basePath: './.storage' },
|
|
32
|
+
cdn: { enabled: true },
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
chat: {
|
|
37
|
+
summary: 'realtime chat channels + messages',
|
|
38
|
+
apply: (cfg) => {
|
|
39
|
+
cfg.chat = { enabled: true, attachments: { enabled: false } }
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
notification: {
|
|
43
|
+
summary: 'notification channels (email/telegram/webhook)',
|
|
44
|
+
apply: (cfg) => {
|
|
45
|
+
cfg.notification = { enabled: true, channels: { email: false } }
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
verification: {
|
|
49
|
+
summary: 'signed verification / approval flows',
|
|
50
|
+
apply: (cfg) => {
|
|
51
|
+
cfg.verification = { enabled: true }
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
audit: {
|
|
55
|
+
summary: 'audit logging of mutations',
|
|
56
|
+
apply: (cfg) => {
|
|
57
|
+
cfg.audit = { enabled: true }
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
captcha: {
|
|
61
|
+
summary: 'CAPTCHA challenge on sensitive endpoints',
|
|
62
|
+
apply: (cfg) => {
|
|
63
|
+
cfg.captcha = { enabled: true, provider: 'turnstile', secretKey: 'CAPTCHA_SECRET_KEY' }
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
pubsub: {
|
|
67
|
+
summary: 'Dapr pub/sub event fan-out',
|
|
68
|
+
apply: (cfg) => {
|
|
69
|
+
cfg.pubsub = { enabled: true }
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
payment: {
|
|
73
|
+
summary: 'payment provider (iyzico | stripe)',
|
|
74
|
+
apply: async (cfg) => {
|
|
75
|
+
const p = prompts()
|
|
76
|
+
try {
|
|
77
|
+
const provider = await p.choose(' Provider', ['iyzico', 'stripe'], 0)
|
|
78
|
+
const block: Cfg = { enabled: true, provider }
|
|
79
|
+
if (provider === 'iyzico')
|
|
80
|
+
block.iyzico = {
|
|
81
|
+
apiKey: 'IYZICO_API_KEY',
|
|
82
|
+
secretKey: 'IYZICO_SECRET_KEY',
|
|
83
|
+
baseUrl: 'IYZICO_BASE_URL',
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
block.stripe = { secretKey: 'STRIPE_SECRET_KEY', webhookSecret: 'STRIPE_WEBHOOK_SECRET' }
|
|
87
|
+
cfg.payment = block
|
|
88
|
+
} finally {
|
|
89
|
+
p.close()
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const OAUTH_PROVIDERS = ['google', 'microsoft', 'github', 'apple']
|
|
96
|
+
|
|
97
|
+
async function readConfig(): Promise<{ path: string; cfg: Cfg } | null> {
|
|
98
|
+
const path = findConfigPath()
|
|
99
|
+
if (!path) {
|
|
100
|
+
fail('No config.json found here — run this from a Nucleus project (or `nucleus init` first)')
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return { path, cfg: JSON.parse(await Bun.file(path).text()) as Cfg }
|
|
105
|
+
} catch {
|
|
106
|
+
fail(`Config ${path} is not valid JSON`)
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Persist, validate, and (if schema-affecting) regenerate Drizzle. */
|
|
112
|
+
async function persist(path: string, cfg: Cfg, regen: boolean): Promise<number> {
|
|
113
|
+
const schema = await loadConfigSchema()
|
|
114
|
+
const res = validateAgainstSchema(cfg, schema)
|
|
115
|
+
const cross = crossFieldChecks(cfg)
|
|
116
|
+
if (res.errors.length + cross.length > 0) {
|
|
117
|
+
for (const e of [...res.errors, ...cross]) fail(`${c.bold(e.path || '(root)')} — ${e.message}`)
|
|
118
|
+
fail('Change would make the config invalid — nothing written')
|
|
119
|
+
return 1
|
|
120
|
+
}
|
|
121
|
+
await Bun.write(path, `${JSON.stringify(cfg, null, 2)}\n`)
|
|
122
|
+
ok(`Updated ${c.cyan(relative(process.cwd(), path))}`)
|
|
123
|
+
|
|
124
|
+
if (regen) {
|
|
125
|
+
const outDir = join(dirname(path), 'drizzle')
|
|
126
|
+
step(`Regenerating Drizzle schema → ${c.dim(relative(process.cwd(), outDir))}`)
|
|
127
|
+
const r = spawnSync('bunx', ['nucleus-core-ts', 'generate', path, outDir], { stdio: 'inherit' })
|
|
128
|
+
if (r.status !== 0) {
|
|
129
|
+
warn('Schema regeneration failed — run `bunx nucleus-core-ts generate` manually')
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
ok('Schema regenerated')
|
|
133
|
+
}
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function addEntity(): Promise<number> {
|
|
138
|
+
const loaded = await readConfig()
|
|
139
|
+
if (!loaded) return 1
|
|
140
|
+
const { path, cfg } = loaded
|
|
141
|
+
const entities = Array.isArray(cfg.entities) ? (cfg.entities as Cfg[]) : []
|
|
142
|
+
const existing = new Set(entities.map((e) => (e as { table_name?: string }).table_name))
|
|
143
|
+
|
|
144
|
+
step('Add entities (blank table name to finish)')
|
|
145
|
+
const p = prompts()
|
|
146
|
+
let added = 0
|
|
147
|
+
try {
|
|
148
|
+
let e = await buildEntity(p)
|
|
149
|
+
while (e) {
|
|
150
|
+
const tn = (e as { table_name: string }).table_name
|
|
151
|
+
if (existing.has(tn)) warn(`Entity "${tn}" already exists — skipped`)
|
|
152
|
+
else {
|
|
153
|
+
entities.push(e)
|
|
154
|
+
existing.add(tn)
|
|
155
|
+
ok(`Queued entity "${tn}"`)
|
|
156
|
+
added++
|
|
157
|
+
}
|
|
158
|
+
e = await buildEntity(p)
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
p.close()
|
|
162
|
+
}
|
|
163
|
+
if (added === 0) {
|
|
164
|
+
info('No entities added')
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
167
|
+
cfg.entities = entities
|
|
168
|
+
return persist(path, cfg, true)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function addFeature(name?: string): Promise<number> {
|
|
172
|
+
const key = (name || '').toLowerCase()
|
|
173
|
+
if (!key || !FEATURES[key]) {
|
|
174
|
+
fail(`Unknown feature "${name ?? ''}"`)
|
|
175
|
+
log(` Available features:`)
|
|
176
|
+
for (const [k, v] of Object.entries(FEATURES))
|
|
177
|
+
log(` ${c.cyan(k.padEnd(14))} ${c.dim(v.summary)}`)
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
const loaded = await readConfig()
|
|
181
|
+
if (!loaded) return 1
|
|
182
|
+
const { path, cfg } = loaded
|
|
183
|
+
if ((cfg[key] as Cfg | undefined)?.enabled) {
|
|
184
|
+
info(`Feature "${key}" is already enabled — leaving it as-is`)
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
step(`Enabling feature: ${c.cyan(key)} ${c.dim(`— ${FEATURES[key].summary}`)}`)
|
|
188
|
+
await FEATURES[key].apply(cfg)
|
|
189
|
+
const rc = await persist(path, cfg, false)
|
|
190
|
+
if (rc === 0) info('Secrets are referenced by ENV-VAR NAME — set them in .env before running.')
|
|
191
|
+
return rc
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function addOauth(provider?: string): Promise<number> {
|
|
195
|
+
const prov = (provider || '').toLowerCase()
|
|
196
|
+
if (!prov || !OAUTH_PROVIDERS.includes(prov)) {
|
|
197
|
+
fail(
|
|
198
|
+
`Unknown OAuth provider "${provider ?? ''}" — expected one of ${OAUTH_PROVIDERS.join(', ')}`
|
|
199
|
+
)
|
|
200
|
+
return 1
|
|
201
|
+
}
|
|
202
|
+
const loaded = await readConfig()
|
|
203
|
+
if (!loaded) return 1
|
|
204
|
+
const { path, cfg } = loaded
|
|
205
|
+
const auth = (cfg.authentication as Cfg | undefined) ?? {}
|
|
206
|
+
if (!auth.enabled) {
|
|
207
|
+
fail('authentication is not enabled — enable it first (an `api` or `saas` preset has it on)')
|
|
208
|
+
return 1
|
|
209
|
+
}
|
|
210
|
+
const oauth = (auth.oauth as Cfg | undefined) ?? {}
|
|
211
|
+
if (oauth[prov]) {
|
|
212
|
+
info(`OAuth provider "${prov}" is already configured`)
|
|
213
|
+
return 0
|
|
214
|
+
}
|
|
215
|
+
const up = prov.toUpperCase()
|
|
216
|
+
oauth[prov] = { clientId: `${up}_CLIENT_ID`, clientSecret: `${up}_CLIENT_SECRET` }
|
|
217
|
+
auth.oauth = oauth
|
|
218
|
+
cfg.authentication = auth
|
|
219
|
+
step(`Adding OAuth provider: ${c.cyan(prov)}`)
|
|
220
|
+
const rc = await persist(path, cfg, false)
|
|
221
|
+
if (rc === 0)
|
|
222
|
+
info(`Set ${up}_CLIENT_ID and ${up}_CLIENT_SECRET in .env, plus the provider redirect URI.`)
|
|
223
|
+
return rc
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function addCommand(args: string[]): Promise<number> {
|
|
227
|
+
const thing = args[0]
|
|
228
|
+
switch (thing) {
|
|
229
|
+
case 'entity':
|
|
230
|
+
case 'entities':
|
|
231
|
+
return addEntity()
|
|
232
|
+
case 'feature':
|
|
233
|
+
return addFeature(args[1])
|
|
234
|
+
case 'oauth':
|
|
235
|
+
return addOauth(args[1])
|
|
236
|
+
default:
|
|
237
|
+
fail(`Unknown: nucleus add ${thing ?? ''}`)
|
|
238
|
+
log(` Usage:`)
|
|
239
|
+
log(
|
|
240
|
+
` ${c.cyan('nucleus add entity')} ${c.dim('add one or more entities (interactive)')}`
|
|
241
|
+
)
|
|
242
|
+
log(` ${c.cyan('nucleus add feature <name>')} ${c.dim('enable a feature block')}`)
|
|
243
|
+
log(
|
|
244
|
+
` ${c.cyan('nucleus add oauth <provider>')} ${c.dim(`add OAuth (${OAUTH_PROVIDERS.join('|')})`)}`
|
|
245
|
+
)
|
|
246
|
+
return 1
|
|
247
|
+
}
|
|
248
|
+
}
|