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
package/lib/kanban.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Supabase-backed Kanban operations
3
+ * Uses existing projects and tasks tables in supabase
4
+ */
5
+ import { getSupabase } from './supabase.js'
6
+
7
+ const sb = () => getSupabase('optimal')
8
+
9
+ // --- Types ---
10
+
11
+ export interface Task {
12
+ id: string
13
+ project_id: string
14
+ milestone_id: string | null
15
+ title: string
16
+ description: string | null
17
+ status: 'backlog' | 'ready' | 'in_progress' | 'blocked' | 'review' | 'done' | 'cancelled'
18
+ priority: number
19
+ assigned_to: string | null
20
+ claimed_by: string | null
21
+ claimed_at: string | null
22
+ skill_required: string | null
23
+ source_repo: string | null
24
+ target_module: string | null
25
+ estimated_effort: string | null
26
+ blocked_by: string[]
27
+ sort_order: number
28
+ created_at: string
29
+ updated_at: string
30
+ completed_at: string | null
31
+ }
32
+
33
+ export interface Project {
34
+ id: string
35
+ slug: string
36
+ name: string
37
+ description: string | null
38
+ status: 'active' | 'archived' | 'on_hold'
39
+ priority: number
40
+ owner: string | null
41
+ created_at: string
42
+ updated_at: string
43
+ }
44
+
45
+ export interface CreateTaskInput {
46
+ project_slug: string
47
+ title: string
48
+ description?: string
49
+ priority?: number
50
+ skill_required?: string
51
+ source_repo?: string
52
+ }
53
+
54
+ // --- Projects ---
55
+
56
+ export async function getProjectBySlug(slug: string): Promise<Project> {
57
+ const { data, error } = await sb()
58
+ .from('projects')
59
+ .select('*')
60
+ .eq('slug', slug)
61
+ .single()
62
+
63
+ if (error) throw new Error(`Project not found: ${slug} — ${error.message}`)
64
+ return data as Project
65
+ }
66
+
67
+ export async function listProjects(): Promise<Project[]> {
68
+ const { data, error } = await sb()
69
+ .from('projects')
70
+ .select('*')
71
+ .eq('status', 'active')
72
+ .order('name')
73
+
74
+ if (error) throw new Error(`Failed to list projects: ${error.message}`)
75
+ return (data ?? []) as Project[]
76
+ }
77
+
78
+ // --- Tasks ---
79
+
80
+ export async function createTask(input: CreateTaskInput): Promise<Task> {
81
+ const project = await getProjectBySlug(input.project_slug)
82
+
83
+ const { data, error } = await sb()
84
+ .from('tasks')
85
+ .insert({
86
+ project_id: project.id,
87
+ title: input.title,
88
+ description: input.description ?? null,
89
+ priority: input.priority ?? 3,
90
+ skill_required: input.skill_required ?? null,
91
+ source_repo: input.source_repo ?? null,
92
+ status: 'backlog',
93
+ blocked_by: [],
94
+ sort_order: 0,
95
+ })
96
+ .select()
97
+ .single()
98
+
99
+ if (error) throw new Error(`Failed to create task: ${error.message}`)
100
+ return data as Task
101
+ }
102
+
103
+ export async function updateTask(
104
+ taskId: string,
105
+ updates: Partial<Pick<Task, 'status' | 'assigned_to' | 'claimed_by' | 'claimed_at' | 'priority' | 'title' | 'description'>>
106
+ ): Promise<Task> {
107
+ const updateData: Record<string, unknown> = { ...updates, updated_at: new Date().toISOString() }
108
+
109
+ // Handle completed_at
110
+ if (updates.status === 'done') {
111
+ updateData.completed_at = new Date().toISOString()
112
+ }
113
+
114
+ const { data, error } = await sb()
115
+ .from('tasks')
116
+ .update(updateData)
117
+ .eq('id', taskId)
118
+ .select()
119
+ .single()
120
+
121
+ if (error) throw new Error(`Failed to update task ${taskId}: ${error.message}`)
122
+ return data as Task
123
+ }
124
+
125
+ export async function getTask(taskId: string): Promise<Task> {
126
+ const { data, error } = await sb()
127
+ .from('tasks')
128
+ .select('*')
129
+ .eq('id', taskId)
130
+ .single()
131
+
132
+ if (error) throw new Error(`Task not found: ${taskId} — ${error.message}`)
133
+ return data as Task
134
+ }
135
+
136
+ export async function claimTask(taskId: string, agentName: string): Promise<Task> {
137
+ const { data, error } = await sb()
138
+ .from('tasks')
139
+ .update({
140
+ claimed_by: agentName,
141
+ claimed_at: new Date().toISOString(),
142
+ status: 'in_progress',
143
+ updated_at: new Date().toISOString(),
144
+ })
145
+ .eq('id', taskId)
146
+ .select()
147
+ .single()
148
+
149
+ if (error) throw new Error(`Failed to claim task ${taskId}: ${error.message}`)
150
+ return data as Task
151
+ }
152
+
153
+ export async function getNextTask(projectSlug: string, agentName: string): Promise<Task | null> {
154
+ const project = await getProjectBySlug(projectSlug)
155
+
156
+ const { data, error } = await sb()
157
+ .from('tasks')
158
+ .select('*')
159
+ .eq('project_id', project.id)
160
+ .in('status', ['backlog', 'ready'])
161
+ .is('claimed_by', null)
162
+ .order('priority', { ascending: true })
163
+ .order('created_at', { ascending: true })
164
+ .limit(10)
165
+
166
+ if (error) throw new Error(`Failed to fetch tasks: ${error.message}`)
167
+ if (!data || data.length === 0) return null
168
+
169
+ for (const task of data as Task[]) {
170
+ if (!task.blocked_by || task.blocked_by.length === 0) return task
171
+ const { data: blockers } = await sb()
172
+ .from('tasks')
173
+ .select('id, status')
174
+ .in('id', task.blocked_by)
175
+ const allDone = blockers?.every(b => b.status === 'done' || b.status === 'cancelled')
176
+ if (allDone) return task
177
+ }
178
+ return null
179
+ }
180
+
181
+ export async function getBoard(projectSlug: string): Promise<Task[]> {
182
+ const project = await getProjectBySlug(projectSlug)
183
+
184
+ const { data, error } = await sb()
185
+ .from('tasks')
186
+ .select('*')
187
+ .eq('project_id', project.id)
188
+ .not('status', 'eq', 'cancelled')
189
+ .order('sort_order')
190
+ .order('priority', { ascending: true })
191
+ .order('created_at', { ascending: true })
192
+
193
+ if (error) throw new Error(`Failed to fetch board: ${error.message}`)
194
+ return (data ?? []) as Task[]
195
+ }
196
+
197
+ export async function getBoardByStatus(projectSlug: string): Promise<Record<string, Task[]>> {
198
+ const tasks = await getBoard(projectSlug)
199
+
200
+ const byStatus: Record<string, Task[]> = {
201
+ backlog: [],
202
+ ready: [],
203
+ in_progress: [],
204
+ blocked: [],
205
+ review: [],
206
+ done: [],
207
+ }
208
+
209
+ for (const task of tasks) {
210
+ const status = task.status as string
211
+ if (byStatus[status]) {
212
+ byStatus[status].push(task)
213
+ } else {
214
+ byStatus.backlog.push(task)
215
+ }
216
+ }
217
+
218
+ return byStatus
219
+ }
220
+
221
+ // --- Activity/Logs (if activity_log table exists) ---
222
+
223
+ export async function logActivity(
224
+ taskId: string,
225
+ entry: { agent: string; action: string; details?: string }
226
+ ) {
227
+ const { error } = await sb()
228
+ .from('activity_log')
229
+ .insert({
230
+ task_id: taskId,
231
+ agent: entry.agent,
232
+ action: entry.action,
233
+ details: entry.details ?? null,
234
+ })
235
+
236
+ if (error) {
237
+ console.warn(`Failed to log activity: ${error.message}`)
238
+ }
239
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Obsidian Tasks Interface
3
+ *
4
+ * Reads/writes markdown task files in /home/oracle/Documents/optimal/tasks/
5
+ * Each task is a separate .md file with YAML frontmatter.
6
+ */
7
+ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'
8
+ import { existsSync } from 'node:fs'
9
+ import { join, basename } from 'node:path'
10
+ import { parse, stringify } from 'yaml'
11
+
12
+ const TASKS_DIR = process.env.OPTIMAL_TASKS_DIR || '/home/oracle/Documents/optimal/tasks'
13
+
14
+ export interface TaskFrontmatter {
15
+ id: string
16
+ type: 'task'
17
+ status: 'pending' | 'in-progress' | 'done' | 'blocked'
18
+ owner?: string
19
+ assignee?: string
20
+ project?: string
21
+ priority?: '1' | '2' | '3' | '4'
22
+ source?: string
23
+ created_at?: string
24
+ updated_at?: string
25
+ updated_by?: string
26
+ tags?: string[]
27
+ // Custom fields
28
+ title?: string
29
+ description?: string
30
+ }
31
+
32
+ export interface Task {
33
+ id: string
34
+ frontmatter: TaskFrontmatter
35
+ body: string
36
+ filePath: string
37
+ }
38
+
39
+ function ensureTasksDir(): void {
40
+ if (!existsSync(TASKS_DIR)) {
41
+ throw new Error(`Tasks directory not found: ${TASKS_DIR}`)
42
+ }
43
+ }
44
+
45
+ export async function listTasks(filters?: {
46
+ project?: string
47
+ status?: string
48
+ owner?: string
49
+ assignee?: string
50
+ }): Promise<Task[]> {
51
+ ensureTasksDir()
52
+
53
+ const files = await readdir(TASKS_DIR)
54
+ const tasks: Task[] = []
55
+
56
+ for (const file of files) {
57
+ if (!file.endsWith('.md') || file === 'index__tasks__task_hub.md') continue
58
+
59
+ const filePath = join(TASKS_DIR, file)
60
+ const content = await readFile(filePath, 'utf-8')
61
+
62
+ const task = parseTaskFile(content, filePath)
63
+ if (!task) continue
64
+
65
+ // Apply filters
66
+ if (filters?.project && task.frontmatter.project !== filters.project) continue
67
+ if (filters?.status && task.frontmatter.status !== filters.status) continue
68
+ if (filters?.owner && task.frontmatter.owner !== filters.owner) continue
69
+ if (filters?.assignee && task.frontmatter.assignee !== filters.assignee) continue
70
+
71
+ tasks.push(task)
72
+ }
73
+
74
+ return tasks.sort((a, b) => {
75
+ // Sort by priority (1 = highest), then by updated_at
76
+ const priA = parseInt(a.frontmatter.priority || '3')
77
+ const priB = parseInt(b.frontmatter.priority || '3')
78
+ if (priA !== priB) return priA - priB
79
+ return (b.frontmatter.updated_at || '').localeCompare(a.frontmatter.updated_at || '')
80
+ })
81
+ }
82
+
83
+ export function parseTaskFile(content: string, filePath: string): Task | null {
84
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
85
+ if (!match) return null
86
+
87
+ try {
88
+ const frontmatter = parse(match[1]) as TaskFrontmatter
89
+ const body = match[2] || ''
90
+
91
+ return {
92
+ id: frontmatter.id || basename(filePath, '.md'),
93
+ frontmatter,
94
+ body,
95
+ filePath
96
+ }
97
+ } catch {
98
+ return null
99
+ }
100
+ }
101
+
102
+ export async function getTaskById(id: string): Promise<Task | null> {
103
+ ensureTasksDir()
104
+
105
+ // Try to find by ID in filename
106
+ const files = await readdir(TASKS_DIR)
107
+ const matchingFile = files.find(f => f.includes(id))
108
+
109
+ if (!matchingFile) return null
110
+
111
+ const filePath = join(TASKS_DIR, matchingFile)
112
+ const content = await readFile(filePath, 'utf-8')
113
+
114
+ return parseTaskFile(content, filePath)
115
+ }
116
+
117
+ export async function createTask(opts: {
118
+ title: string
119
+ project?: string
120
+ description?: string
121
+ priority?: string
122
+ owner?: string
123
+ assignee?: string
124
+ source?: string
125
+ tags?: string[]
126
+ }): Promise<Task> {
127
+ ensureTasksDir()
128
+
129
+ const id = `task__${opts.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40)}_${Date.now().toString(36).slice(-6)}`
130
+ const now = new Date().toISOString()
131
+
132
+ const frontmatter: TaskFrontmatter = {
133
+ id,
134
+ type: 'task',
135
+ status: 'pending',
136
+ project: opts.project || 'default',
137
+ priority: (opts.priority as TaskFrontmatter['priority']) || '3',
138
+ owner: opts.owner,
139
+ assignee: opts.assignee,
140
+ source: opts.source,
141
+ created_at: now,
142
+ updated_at: now,
143
+ tags: opts.tags
144
+ }
145
+
146
+ const content = `---
147
+ ${stringify(frontmatter).trim()}
148
+ ---
149
+
150
+ ${opts.description || ''}
151
+ `
152
+
153
+ const filePath = join(TASKS_DIR, `${id}.md`)
154
+ await writeFile(filePath, content, 'utf-8')
155
+
156
+ return {
157
+ id,
158
+ frontmatter,
159
+ body: opts.description || '',
160
+ filePath
161
+ }
162
+ }
163
+
164
+ export async function updateTask(id: string, updates: Partial<{
165
+ status: TaskFrontmatter['status']
166
+ owner: string
167
+ assignee: string
168
+ priority: string
169
+ message: string
170
+ }>): Promise<Task | null> {
171
+ const task = await getTaskById(id)
172
+ if (!task) return null
173
+
174
+ const now = new Date().toISOString()
175
+
176
+ // Update frontmatter
177
+ if (updates.status) task.frontmatter.status = updates.status
178
+ if (updates.owner) task.frontmatter.owner = updates.owner
179
+ if (updates.assignee) task.frontmatter.assignee = updates.assignee
180
+ if (updates.priority) task.frontmatter.priority = updates.priority as TaskFrontmatter['priority']
181
+ task.frontmatter.updated_at = now
182
+
183
+ // Append message to body if provided
184
+ if (updates.message) {
185
+ const timestamp = new Date().toISOString()
186
+ task.body += `\n\n---\n*${timestamp}:* ${updates.message}`
187
+ }
188
+
189
+ const content = `---
190
+ ${stringify(task.frontmatter).trim()}
191
+ ---
192
+
193
+ ${task.body}
194
+ `
195
+
196
+ await writeFile(task.filePath, content, 'utf-8')
197
+
198
+ return task
199
+ }
200
+
201
+ export async function deleteTask(id: string): Promise<boolean> {
202
+ const task = await getTaskById(id)
203
+ if (!task) return false
204
+
205
+ const { rm } = await import('node:fs/promises')
206
+ await rm(task.filePath)
207
+
208
+ return true
209
+ }
210
+
211
+ export function getTasksDir(): string {
212
+ return TASKS_DIR
213
+ }
214
+
215
+ // Group tasks by status for board view
216
+ export function groupTasksByStatus(tasks: Task[]): Record<string, Task[]> {
217
+ const groups: Record<string, Task[]> = {
218
+ pending: [],
219
+ 'in-progress': [],
220
+ blocked: [],
221
+ done: []
222
+ }
223
+
224
+ for (const task of tasks) {
225
+ const status = task.frontmatter.status || 'pending'
226
+ if (!groups[status]) groups[status] = []
227
+ groups[status].push(task)
228
+ }
229
+
230
+ return groups
231
+ }
package/package.json CHANGED
@@ -1,24 +1,7 @@
1
1
  {
2
2
  "name": "optimal-cli",
3
- "version": "1.0.0",
4
- "description": "Optimal CLI — unified command-line interface for bot orchestration, financial analytics, content management, and infrastructure",
3
+ "version": "1.1.0",
5
4
  "type": "module",
6
- "bin": {
7
- "optimal": "./bin/optimal.ts"
8
- },
9
- "files": [
10
- "bin/",
11
- "lib/",
12
- "agents/",
13
- "docs/CLI-REFERENCE.md"
14
- ],
15
- "keywords": ["cli", "supabase", "kanban", "bot-orchestration", "financial-analytics"],
16
- "author": "Carlos Lenis <clenis@users.noreply.github.com>",
17
- "license": "MIT",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/clenisa/optimal-cli.git"
21
- },
22
5
  "scripts": {
23
6
  "build": "tsc",
24
7
  "dev": "tsx watch bin/optimal.ts",
@@ -32,9 +15,12 @@
32
15
  },
33
16
  "dependencies": {
34
17
  "@supabase/supabase-js": "^2.49.0",
18
+ "@types/glob": "^9.0.0",
35
19
  "commander": "^13.0.0",
36
20
  "dotenv": "^16.4.0",
37
21
  "exceljs": "^4.4.0",
38
- "playwright": "^1.58.2"
22
+ "glob": "^13.0.6",
23
+ "playwright": "^1.58.2",
24
+ "yaml": "^2.8.2"
39
25
  }
40
26
  }
@@ -0,0 +1,3 @@
1
+ packages:
2
+ - 'apps/*'
3
+ - 'lib'
@@ -0,0 +1,24 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+
3
+ const url = 'https://hbfalrpswysryltysonm.supabase.co'
4
+ const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhiZmFscnBzd3lzcnlsdHlzb25tIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTczODI0NTY0NywiZXhwIjoyMDUzODIxNjQ3fQ.placeholder'
5
+
6
+ const supabase = createClient(url, key)
7
+
8
+ async function checkTable() {
9
+ // Try to query the table
10
+ const { data, error } = await supabase
11
+ .from('agent_configs')
12
+ .select('*')
13
+ .limit(1)
14
+
15
+ if (error) {
16
+ console.log('Table status:', error.code === '42P01' ? 'DOES NOT EXIST' : 'ERROR')
17
+ console.log('Error:', error.message)
18
+ } else {
19
+ console.log('Table status: EXISTS')
20
+ console.log('Rows:', data.length)
21
+ }
22
+ }
23
+
24
+ checkTable()
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Run kanban migration - creates tables in supabase
3
+ */
4
+ import { createClient } from '@supabase/supabase-js'
5
+
6
+ const SUPABASE_URL = 'https://vvutttwunexshxkmygik.supabase.co'
7
+ const SUPABASE_SERVICE_KEY = 'sb_secret_zokm_eFGX9ENuRV8hKA05Q_JQDOEOOl'
8
+
9
+ const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, {
10
+ db: { schema: 'public' }
11
+ })
12
+
13
+ const sql = `
14
+ -- Projects table
15
+ CREATE TABLE IF NOT EXISTS cli_projects (
16
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
+ slug TEXT UNIQUE NOT NULL,
18
+ name TEXT NOT NULL,
19
+ description TEXT,
20
+ status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'on_hold')),
21
+ created_at TIMESTAMPTZ DEFAULT NOW(),
22
+ updated_at TIMESTAMPTZ DEFAULT NOW()
23
+ );
24
+
25
+ -- Tasks table
26
+ CREATE TABLE IF NOT EXISTS cli_tasks (
27
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
28
+ project_id UUID REFERENCES cli_projects(id) ON DELETE CASCADE,
29
+ task_id TEXT NOT NULL,
30
+ title TEXT NOT NULL,
31
+ description TEXT,
32
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'done', 'cancelled')),
33
+ priority INTEGER DEFAULT 3 CHECK (priority BETWEEN 1 AND 4),
34
+ owner TEXT,
35
+ assignee TEXT,
36
+ tags JSONB DEFAULT '[]',
37
+ source TEXT,
38
+ created_at TIMESTAMPTZ DEFAULT NOW(),
39
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
40
+ completed_at TIMESTAMPTZ,
41
+ completed_by TEXT,
42
+ metadata JSONB DEFAULT '{}',
43
+ UNIQUE(project_id, task_id)
44
+ );
45
+
46
+ -- Sync log
47
+ CREATE TABLE IF NOT EXISTS cli_sync_log (
48
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
49
+ entity_type TEXT NOT NULL,
50
+ entity_id UUID NOT NULL,
51
+ action TEXT NOT NULL,
52
+ source TEXT NOT NULL,
53
+ synced_at TIMESTAMPTZ DEFAULT NOW(),
54
+ payload JSONB
55
+ );
56
+
57
+ -- Indexes
58
+ CREATE INDEX IF NOT EXISTS idx_tasks_project ON cli_tasks(project_id);
59
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON cli_tasks(status);
60
+ CREATE INDEX IF NOT EXISTS idx_tasks_owner ON cli_tasks(owner);
61
+ CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON cli_tasks(assignee);
62
+ `
63
+
64
+ async function main() {
65
+ console.log('Running migration...')
66
+
67
+ // Run raw SQL via postgrest
68
+ const { data, error } = await supabase.rpc('pg_catalog.exec', {
69
+ query: sql
70
+ }).catch(() => {
71
+ // Fallback: try via REST directly
72
+ return supabase.from('_temp').select('*').limit(0)
73
+ })
74
+
75
+ // Actually, rpc won't work - let's just create tables one by one via insert
76
+ // Since we can't run raw SQL easily, let's try a different approach
77
+ console.log('Trying alternative...')
78
+
79
+ // Check if tables exist
80
+ const { data: tables } = await supabase
81
+ .from('information_schema.tables')
82
+ .select('table_name')
83
+ .eq('table_schema', 'public')
84
+ .in('table_name', ['cli_projects', 'cli_tasks'])
85
+
86
+ console.log('Existing tables:', tables)
87
+
88
+ if (!tables?.length) {
89
+ console.log('Tables do not exist. Trying to create via console...')
90
+ console.log('Please run manually or use supabase studio.')
91
+ }
92
+ }
93
+
94
+ main().catch(console.error)
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # Migrate kanban tables to supabase
3
+
4
+ SQL_FILE="supabase/migrations/20260306195000_create_kanban_tables.sql"
5
+
6
+ # Load env from workspace secrets
7
+ if [ -f ".secrets/dashboard-returnpro.vercel.env" ]; then
8
+ export "$(cat .secrets/dashboard-returnpro.vercel.env | grep -v '^#' | xargs)"
9
+ fi
10
+
11
+ # Check for supabase URL and key
12
+ if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then
13
+ echo "Error: SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not found"
14
+ exit 1
15
+ fi
16
+
17
+ echo "Applying migration..."
18
+ echo "SUPABASE_URL: $SUPABASE_URL"
19
+
20
+ # Use psql if available, otherwise try with supabase cli
21
+ if command -v psql &> /dev/null; then
22
+ # Extract password from the key (it's after the = sign)
23
+ DB_PASS="${SUPABASE_SERVICE_ROLE_KEY##*=}"
24
+ psql "host=vvutttwunexshxkmygik.supabase.co port=5432 dbname=postgres user=postgres password=$DB_PASS" -f "$SQL_FILE" 2>&1
25
+ else
26
+ echo "psql not available, trying supabase cli..."
27
+ npx supabase db push 2>&1
28
+ fi