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.
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.env.example +17 -0
- package/CLAUDE.md +67 -0
- package/COMMANDS.md +264 -0
- package/PUBLISH.md +70 -0
- package/agents/content-ops.md +2 -2
- package/agents/financial-ops.md +2 -2
- package/agents/infra-ops.md +2 -2
- package/apps/.gitkeep +0 -0
- package/bin/optimal.ts +1418 -0
- package/docs/MIGRATION_NEEDED.md +37 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
- package/hooks/.gitkeep +0 -0
- package/lib/budget/projections.ts +561 -0
- package/lib/budget/scenarios.ts +312 -0
- package/lib/cms/publish-blog.ts +129 -0
- package/lib/cms/strapi-client.ts +302 -0
- package/lib/config/registry.ts +229 -0
- package/lib/config/schema.ts +58 -0
- package/lib/config.ts +247 -0
- package/lib/infra/.gitkeep +0 -0
- package/lib/infra/deploy.ts +70 -0
- package/lib/infra/migrate.ts +141 -0
- package/lib/kanban-obsidian.ts +232 -0
- package/lib/kanban-sync.ts +258 -0
- package/lib/kanban.ts +239 -0
- package/lib/newsletter/.gitkeep +0 -0
- package/lib/newsletter/distribute.ts +256 -0
- package/{dist/lib/newsletter/generate-insurance.d.ts → lib/newsletter/generate-insurance.ts} +24 -7
- package/lib/newsletter/generate.ts +735 -0
- package/lib/obsidian-tasks.ts +231 -0
- package/lib/returnpro/.gitkeep +0 -0
- package/lib/returnpro/anomalies.ts +258 -0
- package/lib/returnpro/audit.ts +194 -0
- package/lib/returnpro/diagnose.ts +400 -0
- package/lib/returnpro/kpis.ts +255 -0
- package/lib/returnpro/templates.ts +323 -0
- package/lib/returnpro/upload-income.ts +311 -0
- package/lib/returnpro/upload-netsuite.ts +696 -0
- package/lib/returnpro/upload-r1.ts +563 -0
- package/lib/social/post-generator.ts +468 -0
- package/lib/social/publish.ts +301 -0
- package/lib/social/scraper.ts +503 -0
- package/lib/supabase.ts +25 -0
- package/lib/transactions/delete-batch.ts +258 -0
- package/lib/transactions/ingest.ts +659 -0
- package/lib/transactions/stamp.ts +654 -0
- package/package.json +5 -18
- package/pnpm-workspace.yaml +3 -0
- package/scripts/check-table.ts +24 -0
- package/scripts/create-tables.ts +94 -0
- package/scripts/migrate-kanban.sh +28 -0
- package/scripts/migrate-v2.ts +78 -0
- package/scripts/migrate.ts +79 -0
- package/scripts/run-migration.ts +59 -0
- package/scripts/seed-board.ts +203 -0
- package/scripts/test-kanban.ts +21 -0
- package/skills/audit-financials/SKILL.md +33 -0
- package/skills/board-create/SKILL.md +28 -0
- package/skills/board-update/SKILL.md +27 -0
- package/skills/board-view/SKILL.md +27 -0
- package/skills/delete-batch/SKILL.md +77 -0
- package/skills/deploy/SKILL.md +40 -0
- package/skills/diagnose-months/SKILL.md +68 -0
- package/skills/distribute-newsletter/SKILL.md +58 -0
- package/skills/export-budget/SKILL.md +44 -0
- package/skills/export-kpis/SKILL.md +52 -0
- package/skills/generate-netsuite-template/SKILL.md +51 -0
- package/skills/generate-newsletter/SKILL.md +53 -0
- package/skills/generate-newsletter-insurance/SKILL.md +59 -0
- package/skills/generate-social-posts/SKILL.md +67 -0
- package/skills/health-check/SKILL.md +42 -0
- package/skills/ingest-transactions/SKILL.md +51 -0
- package/skills/manage-cms/SKILL.md +50 -0
- package/skills/manage-scenarios/SKILL.md +83 -0
- package/skills/migrate-db/SKILL.md +79 -0
- package/skills/preview-newsletter/SKILL.md +50 -0
- package/skills/project-budget/SKILL.md +60 -0
- package/skills/publish-blog/SKILL.md +70 -0
- package/skills/publish-social-posts/SKILL.md +70 -0
- package/skills/rate-anomalies/SKILL.md +62 -0
- package/skills/scrape-ads/SKILL.md +49 -0
- package/skills/stamp-transactions/SKILL.md +62 -0
- package/skills/upload-income-statements/SKILL.md +54 -0
- package/skills/upload-netsuite/SKILL.md +56 -0
- package/skills/upload-r1/SKILL.md +45 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/.gitkeep +0 -0
- package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
- package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
- package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
- package/tests/config-command-smoke.test.ts +395 -0
- package/tests/config-registry.test.ts +173 -0
- package/tsconfig.json +19 -0
- package/agents/profiles.json +0 -5
- package/dist/bin/optimal.d.ts +0 -2
- package/dist/bin/optimal.js +0 -1590
- package/dist/lib/assets/index.d.ts +0 -79
- package/dist/lib/assets/index.js +0 -153
- package/dist/lib/assets.d.ts +0 -20
- package/dist/lib/assets.js +0 -112
- package/dist/lib/auth/index.d.ts +0 -83
- package/dist/lib/auth/index.js +0 -146
- package/dist/lib/board/index.d.ts +0 -39
- package/dist/lib/board/index.js +0 -285
- package/dist/lib/board/types.d.ts +0 -111
- package/dist/lib/board/types.js +0 -1
- package/dist/lib/bot/claim.d.ts +0 -3
- package/dist/lib/bot/claim.js +0 -20
- package/dist/lib/bot/coordinator.d.ts +0 -27
- package/dist/lib/bot/coordinator.js +0 -178
- package/dist/lib/bot/heartbeat.d.ts +0 -6
- package/dist/lib/bot/heartbeat.js +0 -30
- package/dist/lib/bot/index.d.ts +0 -9
- package/dist/lib/bot/index.js +0 -6
- package/dist/lib/bot/protocol.d.ts +0 -12
- package/dist/lib/bot/protocol.js +0 -74
- package/dist/lib/bot/reporter.d.ts +0 -3
- package/dist/lib/bot/reporter.js +0 -27
- package/dist/lib/bot/skills.d.ts +0 -26
- package/dist/lib/bot/skills.js +0 -69
- package/dist/lib/budget/projections.d.ts +0 -115
- package/dist/lib/budget/projections.js +0 -384
- package/dist/lib/budget/scenarios.d.ts +0 -93
- package/dist/lib/budget/scenarios.js +0 -214
- package/dist/lib/cms/publish-blog.d.ts +0 -62
- package/dist/lib/cms/publish-blog.js +0 -74
- package/dist/lib/cms/strapi-client.d.ts +0 -123
- package/dist/lib/cms/strapi-client.js +0 -213
- package/dist/lib/config/registry.d.ts +0 -17
- package/dist/lib/config/registry.js +0 -182
- package/dist/lib/config/schema.d.ts +0 -31
- package/dist/lib/config/schema.js +0 -25
- package/dist/lib/config.d.ts +0 -55
- package/dist/lib/config.js +0 -206
- package/dist/lib/errors.d.ts +0 -25
- package/dist/lib/errors.js +0 -91
- package/dist/lib/format.d.ts +0 -28
- package/dist/lib/format.js +0 -98
- package/dist/lib/infra/deploy.d.ts +0 -29
- package/dist/lib/infra/deploy.js +0 -58
- package/dist/lib/infra/migrate.d.ts +0 -34
- package/dist/lib/infra/migrate.js +0 -103
- package/dist/lib/newsletter/distribute.d.ts +0 -52
- package/dist/lib/newsletter/distribute.js +0 -193
- package/dist/lib/newsletter/generate-insurance.js +0 -36
- package/dist/lib/newsletter/generate.d.ts +0 -104
- package/dist/lib/newsletter/generate.js +0 -571
- package/dist/lib/returnpro/anomalies.d.ts +0 -64
- package/dist/lib/returnpro/anomalies.js +0 -166
- package/dist/lib/returnpro/audit.d.ts +0 -32
- package/dist/lib/returnpro/audit.js +0 -147
- package/dist/lib/returnpro/diagnose.d.ts +0 -52
- package/dist/lib/returnpro/diagnose.js +0 -281
- package/dist/lib/returnpro/kpis.d.ts +0 -32
- package/dist/lib/returnpro/kpis.js +0 -192
- package/dist/lib/returnpro/templates.d.ts +0 -48
- package/dist/lib/returnpro/templates.js +0 -229
- package/dist/lib/returnpro/upload-income.d.ts +0 -25
- package/dist/lib/returnpro/upload-income.js +0 -235
- package/dist/lib/returnpro/upload-netsuite.d.ts +0 -37
- package/dist/lib/returnpro/upload-netsuite.js +0 -566
- package/dist/lib/returnpro/upload-r1.d.ts +0 -48
- package/dist/lib/returnpro/upload-r1.js +0 -398
- package/dist/lib/returnpro/validate.d.ts +0 -37
- package/dist/lib/returnpro/validate.js +0 -124
- package/dist/lib/social/meta.d.ts +0 -90
- package/dist/lib/social/meta.js +0 -160
- package/dist/lib/social/post-generator.d.ts +0 -83
- package/dist/lib/social/post-generator.js +0 -333
- package/dist/lib/social/publish.d.ts +0 -66
- package/dist/lib/social/publish.js +0 -226
- package/dist/lib/social/scraper.d.ts +0 -67
- package/dist/lib/social/scraper.js +0 -361
- package/dist/lib/supabase.d.ts +0 -4
- package/dist/lib/supabase.js +0 -20
- package/dist/lib/transactions/delete-batch.d.ts +0 -60
- package/dist/lib/transactions/delete-batch.js +0 -203
- package/dist/lib/transactions/ingest.d.ts +0 -43
- package/dist/lib/transactions/ingest.js +0 -555
- package/dist/lib/transactions/stamp.d.ts +0 -51
- package/dist/lib/transactions/stamp.js +0 -524
- 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
|
+
}
|
package/agents/profiles.json
DELETED
|
@@ -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
|
-
]
|
package/dist/bin/optimal.d.ts
DELETED