optimal-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/.env.example +17 -0
  4. package/CLAUDE.md +67 -0
  5. package/COMMANDS.md +264 -0
  6. package/PUBLISH.md +70 -0
  7. package/agents/content-ops.md +2 -2
  8. package/agents/financial-ops.md +2 -2
  9. package/agents/infra-ops.md +2 -2
  10. package/apps/.gitkeep +0 -0
  11. package/bin/optimal.ts +1418 -0
  12. package/docs/MIGRATION_NEEDED.md +37 -0
  13. package/docs/plans/.gitkeep +0 -0
  14. package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
  15. package/hooks/.gitkeep +0 -0
  16. package/lib/budget/projections.ts +561 -0
  17. package/lib/budget/scenarios.ts +312 -0
  18. package/lib/cms/publish-blog.ts +129 -0
  19. package/lib/cms/strapi-client.ts +302 -0
  20. package/lib/config/registry.ts +229 -0
  21. package/lib/config/schema.ts +58 -0
  22. package/lib/config.ts +247 -0
  23. package/lib/infra/.gitkeep +0 -0
  24. package/lib/infra/deploy.ts +70 -0
  25. package/lib/infra/migrate.ts +141 -0
  26. package/lib/kanban-obsidian.ts +232 -0
  27. package/lib/kanban-sync.ts +258 -0
  28. package/lib/kanban.ts +239 -0
  29. package/lib/newsletter/.gitkeep +0 -0
  30. package/lib/newsletter/distribute.ts +256 -0
  31. package/{dist/lib/newsletter/generate-insurance.d.ts → lib/newsletter/generate-insurance.ts} +24 -7
  32. package/lib/newsletter/generate.ts +735 -0
  33. package/lib/obsidian-tasks.ts +231 -0
  34. package/lib/returnpro/.gitkeep +0 -0
  35. package/lib/returnpro/anomalies.ts +258 -0
  36. package/lib/returnpro/audit.ts +194 -0
  37. package/lib/returnpro/diagnose.ts +400 -0
  38. package/lib/returnpro/kpis.ts +255 -0
  39. package/lib/returnpro/templates.ts +323 -0
  40. package/lib/returnpro/upload-income.ts +311 -0
  41. package/lib/returnpro/upload-netsuite.ts +696 -0
  42. package/lib/returnpro/upload-r1.ts +563 -0
  43. package/lib/social/post-generator.ts +468 -0
  44. package/lib/social/publish.ts +301 -0
  45. package/lib/social/scraper.ts +503 -0
  46. package/lib/supabase.ts +25 -0
  47. package/lib/transactions/delete-batch.ts +258 -0
  48. package/lib/transactions/ingest.ts +659 -0
  49. package/lib/transactions/stamp.ts +654 -0
  50. package/package.json +5 -18
  51. package/pnpm-workspace.yaml +3 -0
  52. package/scripts/check-table.ts +24 -0
  53. package/scripts/create-tables.ts +94 -0
  54. package/scripts/migrate-kanban.sh +28 -0
  55. package/scripts/migrate-v2.ts +78 -0
  56. package/scripts/migrate.ts +79 -0
  57. package/scripts/run-migration.ts +59 -0
  58. package/scripts/seed-board.ts +203 -0
  59. package/scripts/test-kanban.ts +21 -0
  60. package/skills/audit-financials/SKILL.md +33 -0
  61. package/skills/board-create/SKILL.md +28 -0
  62. package/skills/board-update/SKILL.md +27 -0
  63. package/skills/board-view/SKILL.md +27 -0
  64. package/skills/delete-batch/SKILL.md +77 -0
  65. package/skills/deploy/SKILL.md +40 -0
  66. package/skills/diagnose-months/SKILL.md +68 -0
  67. package/skills/distribute-newsletter/SKILL.md +58 -0
  68. package/skills/export-budget/SKILL.md +44 -0
  69. package/skills/export-kpis/SKILL.md +52 -0
  70. package/skills/generate-netsuite-template/SKILL.md +51 -0
  71. package/skills/generate-newsletter/SKILL.md +53 -0
  72. package/skills/generate-newsletter-insurance/SKILL.md +59 -0
  73. package/skills/generate-social-posts/SKILL.md +67 -0
  74. package/skills/health-check/SKILL.md +42 -0
  75. package/skills/ingest-transactions/SKILL.md +51 -0
  76. package/skills/manage-cms/SKILL.md +50 -0
  77. package/skills/manage-scenarios/SKILL.md +83 -0
  78. package/skills/migrate-db/SKILL.md +79 -0
  79. package/skills/preview-newsletter/SKILL.md +50 -0
  80. package/skills/project-budget/SKILL.md +60 -0
  81. package/skills/publish-blog/SKILL.md +70 -0
  82. package/skills/publish-social-posts/SKILL.md +70 -0
  83. package/skills/rate-anomalies/SKILL.md +62 -0
  84. package/skills/scrape-ads/SKILL.md +49 -0
  85. package/skills/stamp-transactions/SKILL.md +62 -0
  86. package/skills/upload-income-statements/SKILL.md +54 -0
  87. package/skills/upload-netsuite/SKILL.md +56 -0
  88. package/skills/upload-r1/SKILL.md +45 -0
  89. package/supabase/.temp/cli-latest +1 -0
  90. package/supabase/migrations/.gitkeep +0 -0
  91. package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
  92. package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
  93. package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
  94. package/tests/config-command-smoke.test.ts +395 -0
  95. package/tests/config-registry.test.ts +173 -0
  96. package/tsconfig.json +19 -0
  97. package/agents/profiles.json +0 -5
  98. package/dist/bin/optimal.d.ts +0 -2
  99. package/dist/bin/optimal.js +0 -1590
  100. package/dist/lib/assets/index.d.ts +0 -79
  101. package/dist/lib/assets/index.js +0 -153
  102. package/dist/lib/assets.d.ts +0 -20
  103. package/dist/lib/assets.js +0 -112
  104. package/dist/lib/auth/index.d.ts +0 -83
  105. package/dist/lib/auth/index.js +0 -146
  106. package/dist/lib/board/index.d.ts +0 -39
  107. package/dist/lib/board/index.js +0 -285
  108. package/dist/lib/board/types.d.ts +0 -111
  109. package/dist/lib/board/types.js +0 -1
  110. package/dist/lib/bot/claim.d.ts +0 -3
  111. package/dist/lib/bot/claim.js +0 -20
  112. package/dist/lib/bot/coordinator.d.ts +0 -27
  113. package/dist/lib/bot/coordinator.js +0 -178
  114. package/dist/lib/bot/heartbeat.d.ts +0 -6
  115. package/dist/lib/bot/heartbeat.js +0 -30
  116. package/dist/lib/bot/index.d.ts +0 -9
  117. package/dist/lib/bot/index.js +0 -6
  118. package/dist/lib/bot/protocol.d.ts +0 -12
  119. package/dist/lib/bot/protocol.js +0 -74
  120. package/dist/lib/bot/reporter.d.ts +0 -3
  121. package/dist/lib/bot/reporter.js +0 -27
  122. package/dist/lib/bot/skills.d.ts +0 -26
  123. package/dist/lib/bot/skills.js +0 -69
  124. package/dist/lib/budget/projections.d.ts +0 -115
  125. package/dist/lib/budget/projections.js +0 -384
  126. package/dist/lib/budget/scenarios.d.ts +0 -93
  127. package/dist/lib/budget/scenarios.js +0 -214
  128. package/dist/lib/cms/publish-blog.d.ts +0 -62
  129. package/dist/lib/cms/publish-blog.js +0 -74
  130. package/dist/lib/cms/strapi-client.d.ts +0 -123
  131. package/dist/lib/cms/strapi-client.js +0 -213
  132. package/dist/lib/config/registry.d.ts +0 -17
  133. package/dist/lib/config/registry.js +0 -182
  134. package/dist/lib/config/schema.d.ts +0 -31
  135. package/dist/lib/config/schema.js +0 -25
  136. package/dist/lib/config.d.ts +0 -55
  137. package/dist/lib/config.js +0 -206
  138. package/dist/lib/errors.d.ts +0 -25
  139. package/dist/lib/errors.js +0 -91
  140. package/dist/lib/format.d.ts +0 -28
  141. package/dist/lib/format.js +0 -98
  142. package/dist/lib/infra/deploy.d.ts +0 -29
  143. package/dist/lib/infra/deploy.js +0 -58
  144. package/dist/lib/infra/migrate.d.ts +0 -34
  145. package/dist/lib/infra/migrate.js +0 -103
  146. package/dist/lib/newsletter/distribute.d.ts +0 -52
  147. package/dist/lib/newsletter/distribute.js +0 -193
  148. package/dist/lib/newsletter/generate-insurance.js +0 -36
  149. package/dist/lib/newsletter/generate.d.ts +0 -104
  150. package/dist/lib/newsletter/generate.js +0 -571
  151. package/dist/lib/returnpro/anomalies.d.ts +0 -64
  152. package/dist/lib/returnpro/anomalies.js +0 -166
  153. package/dist/lib/returnpro/audit.d.ts +0 -32
  154. package/dist/lib/returnpro/audit.js +0 -147
  155. package/dist/lib/returnpro/diagnose.d.ts +0 -52
  156. package/dist/lib/returnpro/diagnose.js +0 -281
  157. package/dist/lib/returnpro/kpis.d.ts +0 -32
  158. package/dist/lib/returnpro/kpis.js +0 -192
  159. package/dist/lib/returnpro/templates.d.ts +0 -48
  160. package/dist/lib/returnpro/templates.js +0 -229
  161. package/dist/lib/returnpro/upload-income.d.ts +0 -25
  162. package/dist/lib/returnpro/upload-income.js +0 -235
  163. package/dist/lib/returnpro/upload-netsuite.d.ts +0 -37
  164. package/dist/lib/returnpro/upload-netsuite.js +0 -566
  165. package/dist/lib/returnpro/upload-r1.d.ts +0 -48
  166. package/dist/lib/returnpro/upload-r1.js +0 -398
  167. package/dist/lib/returnpro/validate.d.ts +0 -37
  168. package/dist/lib/returnpro/validate.js +0 -124
  169. package/dist/lib/social/meta.d.ts +0 -90
  170. package/dist/lib/social/meta.js +0 -160
  171. package/dist/lib/social/post-generator.d.ts +0 -83
  172. package/dist/lib/social/post-generator.js +0 -333
  173. package/dist/lib/social/publish.d.ts +0 -66
  174. package/dist/lib/social/publish.js +0 -226
  175. package/dist/lib/social/scraper.d.ts +0 -67
  176. package/dist/lib/social/scraper.js +0 -361
  177. package/dist/lib/supabase.d.ts +0 -4
  178. package/dist/lib/supabase.js +0 -20
  179. package/dist/lib/transactions/delete-batch.d.ts +0 -60
  180. package/dist/lib/transactions/delete-batch.js +0 -203
  181. package/dist/lib/transactions/ingest.d.ts +0 -43
  182. package/dist/lib/transactions/ingest.js +0 -555
  183. package/dist/lib/transactions/stamp.d.ts +0 -51
  184. package/dist/lib/transactions/stamp.js +0 -524
  185. package/docs/CLI-REFERENCE.md +0 -361
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Command-level smoke tests for optimal config commands.
3
+ * These test the CLI surface directly via subprocess invocation.
4
+ */
5
+ import test from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { mkdtemp, rm, writeFile, readFile } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { spawn } from 'node:child_process'
11
+ import { env } from 'node:process'
12
+
13
+ // Run from the actual project directory, but with a custom HOME for config isolation
14
+ const PROJECT_DIR = '/home/oracle/.openclaw/workspace/optimal-cli'
15
+
16
+ function runOptimalCli(args: string[], extraEnv: Record<string, string> = {}): Promise<{ stdout: string; stderr: string; code: number }> {
17
+ return new Promise((resolve) => {
18
+ const child = spawn('npx', ['tsx', 'bin/optimal.ts', ...args], {
19
+ cwd: PROJECT_DIR,
20
+ env: { ...env, ...extraEnv },
21
+ stdio: ['pipe', 'pipe', 'pipe'],
22
+ })
23
+
24
+ let stdout = ''
25
+ let stderr = ''
26
+
27
+ child.stdout?.on('data', (data) => { stdout += data.toString() })
28
+ child.stderr?.on('data', (data) => { stderr += data.toString() })
29
+ child.on('close', (code) => {
30
+ resolve({ stdout, stderr, code: code ?? 0 })
31
+ })
32
+ })
33
+ }
34
+
35
+ test('config init creates local config file with defaults', async () => {
36
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
37
+ try {
38
+ const result = await runOptimalCli(
39
+ ['config', 'init', '--owner', 'test-smoke', '--profile', 'testprof', '--brand', 'CRE-11TRUST', '--timezone', 'America/New_York'],
40
+ { OPTIMAL_CONFIG_DIR: configDir, OPTIMAL_CONFIG_OWNER: 'test-smoke' }
41
+ )
42
+
43
+ assert.equal(result.code, 0, `config init failed: ${result.stderr}`)
44
+ assert.match(result.stdout, /Initialized config/)
45
+
46
+ // Verify config file was created
47
+ const configPath = join(configDir, 'optimal.config.json')
48
+ const content = await readFile(configPath, 'utf-8')
49
+ const config = JSON.parse(content)
50
+
51
+ assert.equal(config.version, '1.0.0')
52
+ assert.equal(config.profile.name, 'testprof')
53
+ assert.equal(config.profile.owner, 'test-smoke')
54
+ assert.equal(config.defaults.brand, 'CRE-11TRUST')
55
+ assert.equal(config.defaults.timezone, 'America/New_York')
56
+
57
+ // Verify history was created
58
+ const historyPath = join(configDir, 'config-history.log')
59
+ const history = await readFile(historyPath, 'utf-8')
60
+ assert.match(history, /init profile=testprof/)
61
+ } finally {
62
+ await rm(configDir, { recursive: true, force: true })
63
+ }
64
+ })
65
+
66
+ test('config doctor reports health for existing config', async () => {
67
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
68
+ try {
69
+ // First create a config
70
+ await runOptimalCli(
71
+ ['config', 'init', '--owner', 'test-doctor', '--profile', 'default', '--brand', 'LIFEINSUR', '--timezone', 'America/Los_Angeles'],
72
+ { OPTIMAL_CONFIG_DIR: configDir }
73
+ )
74
+
75
+ // Now run doctor
76
+ const result = await runOptimalCli(
77
+ ['config', 'doctor'],
78
+ { OPTIMAL_CONFIG_DIR: configDir }
79
+ )
80
+
81
+ assert.equal(result.code, 0, `config doctor failed: ${result.stderr}`)
82
+ assert.match(result.stdout, /config: ok/)
83
+ assert.match(result.stdout, /profile: default/)
84
+ assert.match(result.stdout, /owner: test-doctor/)
85
+ assert.match(result.stdout, /version: 1.0.0/)
86
+ assert.match(result.stdout, /hash:/)
87
+ } finally {
88
+ await rm(configDir, { recursive: true, force: true })
89
+ }
90
+ })
91
+
92
+ test('config export writes JSON to specified path', async () => {
93
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
94
+ try {
95
+ // First create a config
96
+ await runOptimalCli(
97
+ ['config', 'init', '--owner', 'test-export', '--profile', 'default'],
98
+ { OPTIMAL_CONFIG_DIR: configDir }
99
+ )
100
+
101
+ // Export to a temp path
102
+ const exportPath = join(configDir, 'exported-config.json')
103
+ const result = await runOptimalCli(
104
+ ['config', 'export', '--out', exportPath],
105
+ { OPTIMAL_CONFIG_DIR: configDir }
106
+ )
107
+
108
+ assert.equal(result.code, 0, `config export failed: ${result.stderr}`)
109
+ assert.match(result.stdout, /Exported config/)
110
+
111
+ // Verify exported file
112
+ const content = await readFile(exportPath, 'utf-8')
113
+ const config = JSON.parse(content)
114
+ assert.equal(config.profile.owner, 'test-export')
115
+ } finally {
116
+ await rm(configDir, { recursive: true, force: true })
117
+ }
118
+ })
119
+
120
+ test('config import reads JSON from specified path', async () => {
121
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
122
+ try {
123
+ // Create a config file to import
124
+ const importPath = join(configDir, 'import-config.json')
125
+ const configToImport = {
126
+ version: '1.0.0',
127
+ profile: { name: 'imported', owner: 'test-import', updated_at: '2026-03-06T15:00:00.000Z' },
128
+ providers: { supabase: { project_ref: 'test', url: 'https://test.supabase.co', anon_key_present: true }, strapi: { base_url: 'https://test.strapi.com', token_present: true } },
129
+ defaults: { brand: 'CRE-11TRUST', timezone: 'America/Chicago' },
130
+ features: { cms: true, tasks: false, deploy: false },
131
+ }
132
+ await writeFile(importPath, JSON.stringify(configToImport, null, 2))
133
+
134
+ // Import it
135
+ const result = await runOptimalCli(
136
+ ['config', 'import', '--in', importPath],
137
+ { OPTIMAL_CONFIG_DIR: configDir }
138
+ )
139
+
140
+ assert.equal(result.code, 0, `config import failed: ${result.stderr}`)
141
+ assert.match(result.stdout, /Imported config/)
142
+
143
+ // Verify it was imported
144
+ const configPath = join(configDir, 'optimal.config.json')
145
+ const content = await readFile(configPath, 'utf-8')
146
+ const config = JSON.parse(content)
147
+ assert.equal(config.profile.name, 'imported')
148
+ assert.equal(config.profile.owner, 'test-import')
149
+ } finally {
150
+ await rm(configDir, { recursive: true, force: true })
151
+ }
152
+ })
153
+
154
+ test('config sync pull --dry-run shows what would be pulled', async () => {
155
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
156
+ try {
157
+ // Create a config first
158
+ await runOptimalCli(
159
+ ['config', 'init', '--owner', 'test-pull', '--profile', 'default'],
160
+ { OPTIMAL_CONFIG_DIR: configDir }
161
+ )
162
+
163
+ // Run dry-run pull
164
+ const result = await runOptimalCli(
165
+ ['config', 'sync', 'pull', '--profile', 'default', '--dry-run'],
166
+ { OPTIMAL_CONFIG_DIR: configDir }
167
+ )
168
+
169
+ assert.equal(result.code, 0, `config sync pull --dry-run failed: ${result.stderr}`)
170
+ assert.match(result.stdout, /\[dry-run\]/)
171
+ assert.match(result.stdout, /Would pull profile/)
172
+ assert.match(result.stdout, /test-pull/)
173
+ } finally {
174
+ await rm(configDir, { recursive: true, force: true })
175
+ }
176
+ })
177
+
178
+ test('config sync push --dry-run shows what would be pushed', async () => {
179
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
180
+ try {
181
+ // Create a config first
182
+ await runOptimalCli(
183
+ ['config', 'init', '--owner', 'test-push', '--profile', 'default'],
184
+ { OPTIMAL_CONFIG_DIR: configDir }
185
+ )
186
+
187
+ // Run dry-run push
188
+ const result = await runOptimalCli(
189
+ ['config', 'sync', 'push', '--agent', 'test-push-agent', '--profile', 'default', '--dry-run'],
190
+ { OPTIMAL_CONFIG_DIR: configDir }
191
+ )
192
+
193
+ assert.equal(result.code, 0, `config sync push --dry-run failed: ${result.stderr}`)
194
+ assert.match(result.stdout, /\[dry-run\]/)
195
+ assert.match(result.stdout, /Would push/)
196
+ assert.match(result.stdout, /test-push-agent/)
197
+ assert.match(result.stdout, /hash:/)
198
+ } finally {
199
+ await rm(configDir, { recursive: true, force: true })
200
+ }
201
+ })
202
+
203
+ test('config sync push --force --dry-run shows force flag', async () => {
204
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
205
+ try {
206
+ // Create a config first
207
+ await runOptimalCli(
208
+ ['config', 'init', '--owner', 'test-force', '--profile', 'default'],
209
+ { OPTIMAL_CONFIG_DIR: configDir }
210
+ )
211
+
212
+ // Run dry-run push with force
213
+ const result = await runOptimalCli(
214
+ ['config', 'sync', 'push', '--agent', 'test-force-agent', '--profile', 'default', '--force', '--dry-run'],
215
+ { OPTIMAL_CONFIG_DIR: configDir }
216
+ )
217
+
218
+ assert.equal(result.code, 0, `config sync push --force --dry-run failed: ${result.stderr}`)
219
+ assert.match(result.stdout, /Force: true/)
220
+ } finally {
221
+ await rm(configDir, { recursive: true, force: true })
222
+ }
223
+ })
224
+
225
+ // Error case tests
226
+
227
+ test('config doctor fails gracefully when no config exists', async () => {
228
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
229
+ try {
230
+ const result = await runOptimalCli(
231
+ ['config', 'doctor'],
232
+ { OPTIMAL_CONFIG_DIR: configDir }
233
+ )
234
+
235
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
236
+ // Error message goes to stdout (console.log), not stderr
237
+ assert.match(result.stdout, /No local config found/)
238
+ } finally {
239
+ await rm(configDir, { recursive: true, force: true })
240
+ }
241
+ })
242
+
243
+ test('config export fails when no config exists', async () => {
244
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
245
+ try {
246
+ const result = await runOptimalCli(
247
+ ['config', 'export', '--out', join(configDir, 'out.json')],
248
+ { OPTIMAL_CONFIG_DIR: configDir }
249
+ )
250
+
251
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
252
+ assert.match(result.stderr, /No local config found/)
253
+ } finally {
254
+ await rm(configDir, { recursive: true, force: true })
255
+ }
256
+ })
257
+
258
+ test('config sync push --dry-run fails when no config exists', async () => {
259
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
260
+ try {
261
+ const result = await runOptimalCli(
262
+ ['config', 'sync', 'push', '--agent', 'test', '--dry-run'],
263
+ { OPTIMAL_CONFIG_DIR: configDir }
264
+ )
265
+
266
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
267
+ assert.match(result.stderr, /No local config/)
268
+ } finally {
269
+ await rm(configDir, { recursive: true, force: true })
270
+ }
271
+ })
272
+
273
+ test('config import fails when input file does not exist', async () => {
274
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
275
+ try {
276
+ const result = await runOptimalCli(
277
+ ['config', 'import', '--in', '/nonexistent/path/config.json'],
278
+ { OPTIMAL_CONFIG_DIR: configDir }
279
+ )
280
+
281
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
282
+ assert.match(result.stderr, /Input file not found/)
283
+ } finally {
284
+ await rm(configDir, { recursive: true, force: true })
285
+ }
286
+ })
287
+
288
+ test('config sync pull fails gracefully without supabase config', async () => {
289
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
290
+ try {
291
+ // Create a config first
292
+ await runOptimalCli(
293
+ ['config', 'init', '--owner', 'test-nosupabase', '--profile', 'default'],
294
+ { OPTIMAL_CONFIG_DIR: configDir }
295
+ )
296
+
297
+ // Try sync pull without supabase env vars - should fail gracefully
298
+ const result = await runOptimalCli(
299
+ ['config', 'sync', 'pull', '--profile', 'default'],
300
+ {
301
+ OPTIMAL_CONFIG_DIR: configDir,
302
+ OPTIMAL_SUPABASE_URL: '', // empty = not configured
303
+ OPTIMAL_SUPABASE_ANON_KEY: ''
304
+ }
305
+ )
306
+
307
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
308
+ // Should mention missing env vars
309
+ assert.ok(result.stdout.includes('Missing env vars') || result.stderr.includes('Missing env vars'),
310
+ `Expected missing env vars error, got: ${result.stdout} ${result.stderr}`)
311
+ } finally {
312
+ await rm(configDir, { recursive: true, force: true })
313
+ }
314
+ })
315
+
316
+ test('config init --force overwrites existing config', async () => {
317
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
318
+ try {
319
+ // Create initial config
320
+ await runOptimalCli(
321
+ ['config', 'init', '--owner', 'initial-owner', '--profile', 'default', '--brand', 'CRE-11TRUST'],
322
+ { OPTIMAL_CONFIG_DIR: configDir }
323
+ )
324
+
325
+ // Verify initial config
326
+ let configPath = join(configDir, 'optimal.config.json')
327
+ let content = await readFile(configPath, 'utf-8')
328
+ let config = JSON.parse(content)
329
+ assert.equal(config.profile.owner, 'initial-owner')
330
+
331
+ // Now init again with --force
332
+ const result = await runOptimalCli(
333
+ ['config', 'init', '--owner', 'new-owner', '--profile', 'default', '--brand', 'LIFEINSUR', '--force'],
334
+ { OPTIMAL_CONFIG_DIR: configDir }
335
+ )
336
+
337
+ assert.equal(result.code, 0, `config init --force failed: ${result.stderr}`)
338
+ assert.match(result.stdout, /Initialized config/)
339
+
340
+ // Verify it was overwritten
341
+ content = await readFile(configPath, 'utf-8')
342
+ config = JSON.parse(content)
343
+ assert.equal(config.profile.owner, 'new-owner')
344
+ assert.equal(config.defaults.brand, 'LIFEINSUR')
345
+ } finally {
346
+ await rm(configDir, { recursive: true, force: true })
347
+ }
348
+ })
349
+
350
+ test('config init without --force fails when config exists', async () => {
351
+ const configDir = await mkdtemp(join(tmpdir(), 'optimal-cli-smoke-'))
352
+ try {
353
+ // Create initial config
354
+ await runOptimalCli(
355
+ ['config', 'init', '--owner', 'first-owner', '--profile', 'default'],
356
+ { OPTIMAL_CONFIG_DIR: configDir }
357
+ )
358
+
359
+ // Try to init again without --force - should fail
360
+ const result = await runOptimalCli(
361
+ ['config', 'init', '--owner', 'second-owner', '--profile', 'default'],
362
+ { OPTIMAL_CONFIG_DIR: configDir }
363
+ )
364
+
365
+ assert.equal(result.code, 1, `Expected failure code, got: ${result.code}`)
366
+ assert.match(result.stderr, /Config already exists/)
367
+ } finally {
368
+ await rm(configDir, { recursive: true, force: true })
369
+ }
370
+ })
371
+
372
+ // Basic CLI smoke tests
373
+
374
+ test('--version shows version number', async () => {
375
+ const result = await runOptimalCli(['--version'], {})
376
+ assert.equal(result.code, 0, `--version should succeed`)
377
+ assert.match(result.stdout, /0\./) // version like 0.1.0
378
+ })
379
+
380
+ test('--help shows help', async () => {
381
+ const result = await runOptimalCli(['--help'], {})
382
+ assert.equal(result.code, 0, `--help should succeed`)
383
+ assert.match(result.stdout, /Usage:/)
384
+ assert.match(result.stdout, /Commands:/)
385
+ })
386
+
387
+ test('config --help shows config subcommands', async () => {
388
+ const result = await runOptimalCli(['config', '--help'], {})
389
+ assert.equal(result.code, 0, `config --help should succeed`)
390
+ assert.match(result.stdout, /init/)
391
+ assert.match(result.stdout, /doctor/)
392
+ assert.match(result.stdout, /export/)
393
+ assert.match(result.stdout, /import/)
394
+ assert.match(result.stdout, /sync/)
395
+ })
@@ -0,0 +1,173 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtemp, rm } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ type RegistryModule = typeof import('../lib/config/registry.ts')
8
+
9
+ function makeConfig(owner: string, updatedAt: string) {
10
+ return {
11
+ version: '1.0.0' as const,
12
+ profile: { name: 'default', owner, updated_at: updatedAt },
13
+ providers: {
14
+ supabase: { project_ref: 'proj', url: 'https://example.supabase.co', anon_key_present: true },
15
+ strapi: { base_url: 'https://strapi.example.com', token_present: true },
16
+ },
17
+ defaults: { brand: 'CRE-11TRUST', timezone: 'America/New_York' },
18
+ features: { cms: true, tasks: true, deploy: true },
19
+ }
20
+ }
21
+
22
+ async function loadRegistryWithHome(home: string): Promise<RegistryModule> {
23
+ process.env.HOME = home
24
+ return import(`../lib/config/registry.ts?home=${encodeURIComponent(home)}&ts=${Date.now()}`)
25
+ }
26
+
27
+ test('sync pull uses OPTIMAL_CONFIG_OWNER fallback when no local config exists', async () => {
28
+ const home = await mkdtemp(join(tmpdir(), 'optimal-cli-test-'))
29
+ try {
30
+ process.env.OPTIMAL_CONFIG_OWNER = 'fallback-owner'
31
+ const registry = await loadRegistryWithHome(home)
32
+
33
+ const remoteConfig = makeConfig('fallback-owner', '2026-03-05T11:00:00.000Z')
34
+
35
+ const supabaseMock = {
36
+ from() {
37
+ return {
38
+ select() { return this },
39
+ eq() { return this },
40
+ async maybeSingle() {
41
+ return {
42
+ data: {
43
+ owner: 'fallback-owner',
44
+ profile: 'default',
45
+ config_version: '1.0.0',
46
+ payload: remoteConfig,
47
+ payload_hash: registry.hashConfig(remoteConfig),
48
+ updated_at: '2026-03-05T11:00:00.000Z',
49
+ },
50
+ error: null,
51
+ }
52
+ },
53
+ }
54
+ },
55
+ }
56
+
57
+ registry.setRegistrySupabaseProviderForTests(() => supabaseMock as any)
58
+ const result = await registry.pullRegistryProfile('default')
59
+
60
+ assert.equal(result.ok, true)
61
+ assert.match(result.message, /registry pull ok/)
62
+
63
+ const local = await registry.readLocalConfig()
64
+ assert.ok(local)
65
+ assert.equal(local?.profile.owner, 'fallback-owner')
66
+
67
+ registry.resetRegistrySupabaseProviderForTests()
68
+ } finally {
69
+ delete process.env.OPTIMAL_CONFIG_OWNER
70
+ await rm(home, { recursive: true, force: true })
71
+ }
72
+ })
73
+
74
+ test('sync push returns conflict when remote is newer and different', async () => {
75
+ const home = await mkdtemp(join(tmpdir(), 'optimal-cli-test-'))
76
+ try {
77
+ const registry = await loadRegistryWithHome(home)
78
+
79
+ const localConfig = makeConfig('oracle', '2026-03-05T10:00:00.000Z')
80
+ await registry.writeLocalConfig(localConfig)
81
+
82
+ const remoteConfig = makeConfig('oracle', '2026-03-05T12:00:00.000Z')
83
+
84
+ const supabaseMock = {
85
+ from() {
86
+ return {
87
+ select() { return this },
88
+ eq() { return this },
89
+ async maybeSingle() {
90
+ return {
91
+ data: {
92
+ owner: 'oracle',
93
+ profile: 'default',
94
+ config_version: '1.0.0',
95
+ payload: remoteConfig,
96
+ payload_hash: registry.hashConfig(remoteConfig),
97
+ updated_at: '2026-03-05T12:00:00.000Z',
98
+ },
99
+ error: null,
100
+ }
101
+ },
102
+ async upsert() {
103
+ throw new Error('upsert should not be called on conflict')
104
+ },
105
+ }
106
+ },
107
+ }
108
+
109
+ registry.setRegistrySupabaseProviderForTests(() => supabaseMock as any)
110
+ const result = await registry.pushRegistryProfile('default', false)
111
+
112
+ assert.equal(result.ok, false)
113
+ assert.match(result.message, /registry push conflict/)
114
+
115
+ registry.resetRegistrySupabaseProviderForTests()
116
+ } finally {
117
+ await rm(home, { recursive: true, force: true })
118
+ }
119
+ })
120
+
121
+ test('sync push with --force upserts newer local config', async () => {
122
+ const home = await mkdtemp(join(tmpdir(), 'optimal-cli-test-'))
123
+ try {
124
+ const registry = await loadRegistryWithHome(home)
125
+
126
+ const localConfig = makeConfig('oracle', '2026-03-05T13:00:00.000Z')
127
+ await registry.writeLocalConfig(localConfig)
128
+
129
+ const remoteConfig = makeConfig('oracle', '2026-03-05T12:00:00.000Z')
130
+
131
+ let upsertPayload: any = null
132
+
133
+ const supabaseMock = {
134
+ from() {
135
+ return {
136
+ select() { return this },
137
+ eq() { return this },
138
+ async maybeSingle() {
139
+ return {
140
+ data: {
141
+ owner: 'oracle',
142
+ profile: 'default',
143
+ config_version: '1.0.0',
144
+ payload: remoteConfig,
145
+ payload_hash: registry.hashConfig(remoteConfig),
146
+ updated_at: '2026-03-05T12:00:00.000Z',
147
+ },
148
+ error: null,
149
+ }
150
+ },
151
+ async upsert(payload: any) {
152
+ upsertPayload = payload
153
+ return { error: null }
154
+ },
155
+ }
156
+ },
157
+ }
158
+
159
+ registry.setRegistrySupabaseProviderForTests(() => supabaseMock as any)
160
+ const result = await registry.pushRegistryProfile('default', true)
161
+
162
+ assert.equal(result.ok, true)
163
+ assert.match(result.message, /registry push ok/)
164
+ assert.ok(upsertPayload)
165
+ assert.equal(upsertPayload.owner, 'oracle')
166
+ assert.equal(upsertPayload.profile, 'default')
167
+ assert.equal(upsertPayload.source, 'optimal-cli')
168
+
169
+ registry.resetRegistrySupabaseProviderForTests()
170
+ } finally {
171
+ await rm(home, { recursive: true, force: true })
172
+ }
173
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "paths": {
14
+ "@lib/*": ["./lib/*"]
15
+ }
16
+ },
17
+ "include": ["lib/**/*.ts", "bin/**/*.ts"],
18
+ "exclude": ["node_modules", "dist", "apps"]
19
+ }
@@ -1,5 +0,0 @@
1
- [
2
- { "id": "claude-alpha", "skills": ["*"], "maxConcurrent": 3, "status": "idle" },
3
- { "id": "claude-beta", "skills": ["generate-social-posts", "generate-newsletter", "publish-social-posts", "publish-blog", "scrape-ads"], "maxConcurrent": 2, "status": "idle" },
4
- { "id": "claude-gamma", "skills": ["ingest-transactions", "stamp-transactions", "project-budget", "audit-financials", "manage-scenarios"], "maxConcurrent": 2, "status": "idle" }
5
- ]
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import 'dotenv/config';