optimal-cli 1.0.0 → 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 (85) 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 +278 -591
  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/config/registry.ts +5 -4
  17. package/lib/kanban-obsidian.ts +232 -0
  18. package/lib/kanban-sync.ts +258 -0
  19. package/lib/kanban.ts +239 -0
  20. package/lib/obsidian-tasks.ts +231 -0
  21. package/package.json +5 -19
  22. package/pnpm-workspace.yaml +3 -0
  23. package/scripts/check-table.ts +24 -0
  24. package/scripts/create-tables.ts +94 -0
  25. package/scripts/migrate-kanban.sh +28 -0
  26. package/scripts/migrate-v2.ts +78 -0
  27. package/scripts/migrate.ts +79 -0
  28. package/scripts/run-migration.ts +59 -0
  29. package/scripts/seed-board.ts +203 -0
  30. package/scripts/test-kanban.ts +21 -0
  31. package/skills/audit-financials/SKILL.md +33 -0
  32. package/skills/board-create/SKILL.md +28 -0
  33. package/skills/board-update/SKILL.md +27 -0
  34. package/skills/board-view/SKILL.md +27 -0
  35. package/skills/delete-batch/SKILL.md +77 -0
  36. package/skills/deploy/SKILL.md +40 -0
  37. package/skills/diagnose-months/SKILL.md +68 -0
  38. package/skills/distribute-newsletter/SKILL.md +58 -0
  39. package/skills/export-budget/SKILL.md +44 -0
  40. package/skills/export-kpis/SKILL.md +52 -0
  41. package/skills/generate-netsuite-template/SKILL.md +51 -0
  42. package/skills/generate-newsletter/SKILL.md +53 -0
  43. package/skills/generate-newsletter-insurance/SKILL.md +59 -0
  44. package/skills/generate-social-posts/SKILL.md +67 -0
  45. package/skills/health-check/SKILL.md +42 -0
  46. package/skills/ingest-transactions/SKILL.md +51 -0
  47. package/skills/manage-cms/SKILL.md +50 -0
  48. package/skills/manage-scenarios/SKILL.md +83 -0
  49. package/skills/migrate-db/SKILL.md +79 -0
  50. package/skills/preview-newsletter/SKILL.md +50 -0
  51. package/skills/project-budget/SKILL.md +60 -0
  52. package/skills/publish-blog/SKILL.md +70 -0
  53. package/skills/publish-social-posts/SKILL.md +70 -0
  54. package/skills/rate-anomalies/SKILL.md +62 -0
  55. package/skills/scrape-ads/SKILL.md +49 -0
  56. package/skills/stamp-transactions/SKILL.md +62 -0
  57. package/skills/upload-income-statements/SKILL.md +54 -0
  58. package/skills/upload-netsuite/SKILL.md +56 -0
  59. package/skills/upload-r1/SKILL.md +45 -0
  60. package/supabase/.temp/cli-latest +1 -0
  61. package/supabase/migrations/.gitkeep +0 -0
  62. package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
  63. package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
  64. package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
  65. package/tests/config-command-smoke.test.ts +395 -0
  66. package/tests/config-registry.test.ts +173 -0
  67. package/tsconfig.json +19 -0
  68. package/agents/profiles.json +0 -5
  69. package/docs/CLI-REFERENCE.md +0 -361
  70. package/lib/assets/index.ts +0 -225
  71. package/lib/assets.ts +0 -124
  72. package/lib/auth/index.ts +0 -189
  73. package/lib/board/index.ts +0 -309
  74. package/lib/board/types.ts +0 -124
  75. package/lib/bot/claim.ts +0 -43
  76. package/lib/bot/coordinator.ts +0 -254
  77. package/lib/bot/heartbeat.ts +0 -37
  78. package/lib/bot/index.ts +0 -9
  79. package/lib/bot/protocol.ts +0 -99
  80. package/lib/bot/reporter.ts +0 -42
  81. package/lib/bot/skills.ts +0 -81
  82. package/lib/errors.ts +0 -129
  83. package/lib/format.ts +0 -120
  84. package/lib/returnpro/validate.ts +0 -154
  85. package/lib/social/meta.ts +0 -228
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: upload-netsuite
3
+ description: Upload NetSuite XLSM or CSV financial data into ReturnPro staging tables
4
+ ---
5
+
6
+ ## Purpose
7
+ Uploads NetSuite financial exports (XLSM macro-enabled workbooks or CSV files) into `stg_financials_raw`. This is the primary data pipeline for ReturnPro FP&A staging. Handles both single-sheet CSV files and multi-sheet XLSM workbooks (auto-detects format). Supports Wes-style multi-sheet workbooks where each month tab contains that month's data.
8
+
9
+ ## Inputs
10
+ - **file** (required): Absolute path to the NetSuite XLSM or CSV file on disk
11
+ - **month** (optional): Target month as YYYY-MM. Required for CSV files. For XLSM with monthly tabs, auto-detected from sheet names.
12
+ - **dry-run** (optional): Parse and validate without writing to Supabase.
13
+
14
+ ## Steps
15
+ 1. Call `lib/returnpro/upload-netsuite.ts::uploadNetsuite(file, month?, options?)` to orchestrate the upload
16
+ 2. Detect file format by extension (.xlsm, .xlsx, .csv)
17
+ 3. For XLSM/XLSX: check for multi-sheet layout using `hasMonthlySheets()` — if 3+ month-named sheets exist, read per-month tabs (NOT Summary)
18
+ 4. For CSV: read single file, require `--month` parameter
19
+ 5. Parse rows into staging format: `account_code`, `account_name`, `amount` (as TEXT), `period` (YYYY-MM), `source_file`
20
+ 6. Resolve account codes against `dim_account` for validation
21
+ 7. Batch-upsert into `stg_financials_raw` (keyed on account_code + period + master_program_id)
22
+ 8. Log execution via `lib/kanban.ts::logSkillExecution()`
23
+
24
+ ## Output
25
+ ```
26
+ Format: NetSuite XLSM (multi-sheet: Jan, Feb, Mar)
27
+ Parsed 3 months: 2026-01 (91 rows), 2026-02 (89 rows), 2026-03 (93 rows)
28
+ Total inserted: 273 | Updated: 0 | Skipped: 0
29
+ ```
30
+
31
+ ## CLI Usage
32
+ ```bash
33
+ # XLSM with auto-detected monthly tabs
34
+ optimal upload-netsuite --file ~/Downloads/returnpro-data/Solution7-Q1-2026.xlsm
35
+
36
+ # CSV with explicit month
37
+ optimal upload-netsuite --file ~/Downloads/returnpro-data/netsuite-jan-2026.csv --month 2026-01
38
+
39
+ # Dry run to preview
40
+ optimal upload-netsuite --file ~/Downloads/returnpro-data/Solution7-Q1-2026.xlsm --dry-run
41
+ ```
42
+
43
+ ## Environment
44
+ Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
45
+
46
+ ## Tables Touched
47
+ - `stg_financials_raw` — upsert parsed rows (amount stored as TEXT)
48
+ - `dim_account` — validate account codes
49
+
50
+ ## Gotchas
51
+ - **amount is TEXT**: The `stg_financials_raw.amount` column is TEXT, not NUMERIC. Always CAST before numeric comparisons.
52
+ - **Multi-sheet detection**: `hasMonthlySheets()` triggers when 3+ sheets have month-like names. If present, reads individual month tabs and ignores Summary sheet.
53
+ - **Never run SQL manually**: Use migration files + `supabase db push --linked` for schema changes.
54
+
55
+ ## Status
56
+ Implementation status: Not yet implemented. Spec only. Lib function `lib/returnpro/upload-netsuite.ts` to be extracted from dashboard-returnpro's `/api/staging/upload` route.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: upload-r1
3
+ description: Upload R1 XLSX files parsed with WASM/calamine, aggregate by program, and load into ReturnPro staging
4
+ ---
5
+
6
+ ## Purpose
7
+ Uploads R1 reverse-logistics XLSX files into ReturnPro's financial staging tables. The R1 export is parsed using WASM-based calamine (fast XLSX parser without ExcelJS overhead), rows are aggregated by master program, and results are upserted into `stg_financials_raw`. This is one of the primary data ingestion paths for ReturnPro FP&A.
8
+
9
+ ## Inputs
10
+ - **file** (required): Absolute path to the R1 XLSX file on disk
11
+ - **month** (required): Target month as YYYY-MM (e.g., `2026-01`). Used for the staging period column.
12
+ - **dry-run** (optional): Parse and aggregate without writing to Supabase. Useful for previewing row counts.
13
+
14
+ ## Steps
15
+ 1. Call `lib/returnpro/upload-r1.ts::uploadR1(file, month, options?)` to orchestrate the upload
16
+ 2. Read the XLSX file with WASM/calamine parser (faster than ExcelJS for large files)
17
+ 3. Normalize column headers — map R1-specific column names to standard staging fields (account_code, amount, description)
18
+ 4. Aggregate rows by `master_program_id` + `account_code` within the target month
19
+ 5. Resolve program names to `dim_master_program.id` via fuzzy match
20
+ 6. Upsert aggregated rows into `stg_financials_raw` (keyed on account_code + month + master_program_id)
21
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
22
+
23
+ ## Output
24
+ ```
25
+ Parsed R1 XLSX: 1,842 rows across 47 programs
26
+ Aggregated to 312 staging rows for 2026-01
27
+ Inserted: 298 | Updated: 14 | Skipped: 0
28
+ ```
29
+
30
+ ## CLI Usage
31
+ ```bash
32
+ optimal upload-r1 --file ~/Downloads/returnpro-data/R1-January-2026.xlsx --month 2026-01
33
+ optimal upload-r1 --file ~/Downloads/returnpro-data/R1-January-2026.xlsx --month 2026-01 --dry-run
34
+ ```
35
+
36
+ ## Environment
37
+ Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
38
+
39
+ ## Tables Touched
40
+ - `stg_financials_raw` — upsert aggregated rows (note: `amount` column is TEXT, not NUMERIC)
41
+ - `dim_master_program` — lookup for program name resolution
42
+ - `dim_account` — lookup for account code validation
43
+
44
+ ## Status
45
+ Implementation status: Not yet implemented. Spec only. Lib function `lib/returnpro/upload-r1.ts` to be extracted from dashboard-returnpro's `/api/r1/` routes.
@@ -0,0 +1 @@
1
+ v2.75.0
File without changes
@@ -0,0 +1,36 @@
1
+ -- Create table for storing agent OpenClaw configs
2
+ CREATE TABLE IF NOT EXISTS agent_configs (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ agent_name TEXT NOT NULL UNIQUE,
5
+ config_json JSONB NOT NULL,
6
+ version TEXT NOT NULL,
7
+ created_at TIMESTAMPTZ DEFAULT NOW(),
8
+ updated_at TIMESTAMPTZ DEFAULT NOW()
9
+ );
10
+
11
+ -- Index for faster lookups
12
+ CREATE INDEX IF NOT EXISTS idx_agent_configs_name ON agent_configs(agent_name);
13
+
14
+ -- Enable RLS
15
+ ALTER TABLE agent_configs ENABLE ROW LEVEL SECURITY;
16
+
17
+ -- Allow service role full access
18
+ CREATE POLICY "Service role full access" ON agent_configs
19
+ FOR ALL
20
+ TO service_role
21
+ USING (true)
22
+ WITH CHECK (true);
23
+
24
+ -- Add updated_at trigger
25
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
26
+ RETURNS TRIGGER AS $$
27
+ BEGIN
28
+ NEW.updated_at = NOW();
29
+ RETURN NEW;
30
+ END;
31
+ $$ LANGUAGE plpgsql;
32
+
33
+ CREATE TRIGGER update_agent_configs_updated_at
34
+ BEFORE UPDATE ON agent_configs
35
+ FOR EACH ROW
36
+ EXECUTE FUNCTION update_updated_at_column();
@@ -0,0 +1,22 @@
1
+ -- create shared config registry for optimal-cli profile sync
2
+ create table if not exists public.cli_config_registry (
3
+ id uuid primary key default gen_random_uuid(),
4
+ owner text not null,
5
+ profile text not null default 'default',
6
+ config_version text not null,
7
+ payload jsonb not null,
8
+ payload_hash text not null,
9
+ source text not null default 'optimal-cli',
10
+ updated_by text,
11
+ updated_at timestamptz not null default now(),
12
+ created_at timestamptz not null default now(),
13
+ unique (owner, profile)
14
+ );
15
+
16
+ create index if not exists idx_cli_config_registry_owner_profile
17
+ on public.cli_config_registry (owner, profile);
18
+
19
+ create index if not exists idx_cli_config_registry_updated_at
20
+ on public.cli_config_registry (updated_at desc);
21
+
22
+ -- note: rls policies intentionally deferred until auth model is finalized.
@@ -0,0 +1,97 @@
1
+ -- Kanban tables for 3-way sync: supabase + obsidian + cli
2
+
3
+ -- Projects table
4
+ CREATE TABLE IF NOT EXISTS cli_projects (
5
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ slug TEXT UNIQUE NOT NULL,
7
+ name TEXT NOT NULL,
8
+ description TEXT,
9
+ status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'on_hold')),
10
+ created_at TIMESTAMPTZ DEFAULT NOW(),
11
+ updated_at TIMESTAMPTZ DEFAULT NOW()
12
+ );
13
+
14
+ -- Tasks table
15
+ CREATE TABLE IF NOT EXISTS cli_tasks (
16
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
+ project_id UUID REFERENCES cli_projects(id) ON DELETE CASCADE,
18
+ task_id TEXT NOT NULL, -- from obsidian filename, e.g., "task__optimal-cli-shared-config-registry__b7c1d2e3"
19
+ title TEXT NOT NULL,
20
+ description TEXT,
21
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'done', 'cancelled')),
22
+ priority INTEGER DEFAULT 3 CHECK (priority BETWEEN 1 AND 4),
23
+ owner TEXT, -- agent or person responsible
24
+ assignee TEXT,
25
+ tags JSONB DEFAULT '[]',
26
+ source TEXT, -- e.g., "chat:carlos:2026-03-04"
27
+ created_at TIMESTAMPTZ DEFAULT NOW(),
28
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
29
+ completed_at TIMESTAMPTZ,
30
+ completed_by TEXT,
31
+ metadata JSONB DEFAULT '{}',
32
+ UNIQUE(project_id, task_id)
33
+ );
34
+
35
+ -- Sync log for tracking changes
36
+ CREATE TABLE IF NOT EXISTS cli_sync_log (
37
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
38
+ entity_type TEXT NOT NULL, -- 'project' or 'task'
39
+ entity_id UUID NOT NULL,
40
+ action TEXT NOT NULL, -- 'create', 'update', 'delete'
41
+ source TEXT NOT NULL, -- 'cli', 'obsidian', 'manual'
42
+ synced_at TIMESTAMPTZ DEFAULT NOW(),
43
+ payload JSONB
44
+ );
45
+
46
+ -- Indexes
47
+ CREATE INDEX IF NOT EXISTS idx_tasks_project ON cli_tasks(project_id);
48
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON cli_tasks(status);
49
+ CREATE INDEX IF NOT EXISTS idx_tasks_owner ON cli_tasks(owner);
50
+ CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON cli_tasks(assignee);
51
+ CREATE INDEX IF NOT EXISTS idx_sync_log_entity ON cli_sync_log(entity_type, entity_id);
52
+
53
+ -- Enable RLS
54
+ ALTER TABLE cli_projects ENABLE ROW LEVEL SECURITY;
55
+ ALTER TABLE cli_tasks ENABLE ROW LEVEL SECURITY;
56
+ ALTER TABLE cli_sync_log ENABLE ROW LEVEL SECURITY;
57
+
58
+ -- RLS policies (service role can do everything)
59
+ CREATE POLICY "service_role_full_access_projects" ON cli_projects FOR ALL USING (true) WITH CHECK (true);
60
+ CREATE POLICY "service_role_full_access_tasks" ON cli_tasks FOR ALL USING (true) WITH CHECK (true);
61
+ CREATE POLICY "service_role_full_access_sync_log" ON cli_sync_log FOR ALL USING (true) WITH CHECK (true);
62
+
63
+ -- Function to update updated_at
64
+ CREATE OR REPLACE FUNCTION update_updated_at()
65
+ RETURNS TRIGGER AS $$
66
+ BEGIN
67
+ NEW.updated_at = NOW();
68
+ RETURN NEW;
69
+ END;
70
+ $$ LANGUAGE plpgsql;
71
+
72
+ -- Triggers for updated_at
73
+ CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON cli_projects
74
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
75
+
76
+ CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON cli_tasks
77
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
78
+
79
+ -- Insert default project for optimal tasks
80
+ INSERT INTO cli_projects (slug, name, description)
81
+ VALUES ('optimal-tasks', 'Optimal Tasks', 'Main task tracking for optimal agents')
82
+ ON CONFLICT (slug) DO NOTHING;
83
+
84
+ -- Function to log sync
85
+ CREATE OR REPLACE FUNCTION log_sync(
86
+ p_entity_type TEXT,
87
+ p_entity_id UUID,
88
+ p_action TEXT,
89
+ p_source TEXT,
90
+ p_payload JSONB DEFAULT '{}'::JSONB
91
+ )
92
+ RETURNS VOID AS $$
93
+ BEGIN
94
+ INSERT INTO cli_sync_log (entity_type, entity_id, action, source, payload)
95
+ VALUES (p_entity_type, p_entity_id, p_action, p_source, p_payload);
96
+ END;
97
+ $$ LANGUAGE plpgsql;
@@ -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
+ })