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.
Files changed (80) hide show
  1. package/README.md +177 -177
  2. package/bin/cli.ts +136 -136
  3. package/bin/lib/commands/add.ts +248 -248
  4. package/bin/lib/commands/dev.ts +213 -213
  5. package/bin/lib/commands/doctor.ts +91 -91
  6. package/bin/lib/commands/init.ts +147 -147
  7. package/bin/lib/commands/validate.ts +137 -137
  8. package/bin/lib/config-builder.ts +84 -84
  9. package/bin/lib/env.ts +85 -85
  10. package/bin/lib/presets.ts +124 -124
  11. package/bin/lib/schema.ts +132 -132
  12. package/bin/lib/templates.ts +154 -154
  13. package/bin/lib/ui.ts +126 -126
  14. package/dist/.build-ok +1 -1
  15. package/dist/client.d.ts +1 -0
  16. package/dist/client.js +1 -1
  17. package/dist/fe/components/AuthGuard/store.js +11 -11
  18. package/dist/fe/components/VerificationFlowPage/components/PendingTab.js +107 -107
  19. package/dist/fe/components/VerificationFlowPage/components/PropertiesPanel.js +6 -6
  20. package/dist/fe/components/VerificationFlowPage/components/VerificationEntityList.js +58 -58
  21. package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +258 -258
  22. package/dist/fe/components/VerificationFlowPage/components/VerificationStatusCard.js +19 -19
  23. package/dist/fe/components/VerificationFlowPage/components/decisionPlan.js +108 -108
  24. package/dist/fe/components/VerificationFlowPage/components/recordLabels.js +27 -27
  25. package/dist/fe/components/VerificationFlowPage/components/supersededCard.js +9 -9
  26. package/dist/fe/components/VerificationFlowPage/components/useEntityLabels.js +8 -8
  27. package/dist/fe/components/VerificationFlowPage/index.js +17 -17
  28. package/dist/fe/components/VerificationFlowPage/labels.js +36 -36
  29. package/dist/fe/components/VerificationFlowPage/model2/ActionRows.js +212 -212
  30. package/dist/fe/components/VerificationFlowPage/model2/AssigneeRows.js +37 -37
  31. package/dist/fe/components/VerificationFlowPage/model2/FlowDiagram.js +175 -175
  32. package/dist/fe/components/VerificationFlowPage/model2/OutcomeRows.js +49 -49
  33. package/dist/fe/components/VerificationFlowPage/model2/ReadinessCard.js +65 -65
  34. package/dist/fe/components/VerificationFlowPage/model2/StepCard.js +15 -15
  35. package/dist/fe/components/VerificationFlowPage/model2/StepListEditor.js +75 -75
  36. package/dist/fe/components/VerificationFlowPage/model2/editorGraph.js +190 -190
  37. package/dist/fe/components/VerificationFlowPage/model2/readinessText.js +4 -4
  38. package/dist/fe/components/VerificationFlowPage/model2/theme.js +17 -17
  39. package/dist/fe/components/VerificationFlowPage/stepOrder.js +19 -19
  40. package/dist/fe/components/VerificationFlowPage/store/index.js +76 -76
  41. package/dist/fe/components/VerificationFlowPage/theme/index.js +7 -7
  42. package/dist/fe/components/VerificationFlowPage/types/index.js +7 -7
  43. package/dist/fe/components/VerificationPanel/components/PendingVerifications.js +30 -30
  44. package/dist/fe/components/VerificationPanel/components/VerificationPanel.js +25 -25
  45. package/dist/fe/utils/newId.js +20 -20
  46. package/dist/index.js +5 -5
  47. package/dist/server.js +4 -4
  48. package/dist/src/Client/Proxy/httpProxy.d.ts +37 -5
  49. package/dist/src/Client/Proxy/httpProxy.js +145 -77
  50. package/dist/src/Client/Proxy/index.d.ts +3 -0
  51. package/dist/src/Client/Proxy/index.js +2 -0
  52. package/dist/src/Client/Proxy/refreshOutcome.d.ts +40 -0
  53. package/dist/src/Client/Proxy/refreshOutcome.js +54 -0
  54. package/dist/src/Client/Proxy/synthesizedAuth.d.ts +18 -3
  55. package/dist/src/Client/Proxy/synthesizedAuth.js +23 -10
  56. package/dist/src/Client/Proxy/types.d.ts +13 -0
  57. package/dist/src/Client/Proxy/wsProxy.d.ts +1 -0
  58. package/dist/src/Client/Proxy/wsProxy.js +35 -7
  59. package/dist/src/Client/PubSub/usePubSub.js +101 -99
  60. package/dist/src/Client/PubSub/waitForReturn.js +29 -29
  61. package/dist/src/Client/Upload/progress.js +31 -31
  62. package/dist/src/Client/Upload/types.js +7 -7
  63. package/dist/src/Client/Upload/upload.js +21 -21
  64. package/dist/src/Nucleus/routes/auth/refresh/sessionBinding.d.ts +9 -0
  65. package/dist/src/Services/Auth/SessionStore/Read/index.d.ts +25 -0
  66. package/infra/templates/backend/package.json +22 -22
  67. package/infra/templates/backend/src/index.ts +33 -33
  68. package/package.json +1 -1
  69. package/public/components/docs/content/advanced.ts +506 -506
  70. package/public/components/docs/content/gettingStarted.ts +411 -411
  71. package/schemas/nucleus.tables.schema.json +19 -19
  72. package/schemas/table.schema.json +509 -509
  73. package/scripts/generate-schema.test.ts +399 -399
  74. package/scripts/generate-schema.ts +810 -810
  75. package/scripts/importCase.test.ts +174 -174
  76. package/scripts/scaffoldParity.test.ts +126 -126
  77. package/scripts/version.ts +127 -127
  78. package/src/Services/Logger/auditTaxonomy.ts +154 -154
  79. package/src/Services/SchemaTables/resolveEntities.ts +214 -214
  80. package/src/system.tables.json +6035 -6035
@@ -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
+ }