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/bin/optimal.ts CHANGED
@@ -2,15 +2,20 @@
2
2
  import { Command } from 'commander'
3
3
  import 'dotenv/config'
4
4
  import {
5
- createProject, getProjectBySlug, listProjects, updateProject,
6
- createMilestone, listMilestones,
7
- createLabel, listLabels,
8
- createTask, updateTask, getTask, listTasks, claimTask, completeTask,
9
- addComment, listComments,
10
- logActivity, listActivity,
11
- formatBoardTable, getNextClaimable,
12
- type Task, type TaskStatus,
13
- } from '../lib/board/index.js'
5
+ getBoard,
6
+ createTask,
7
+ updateTask,
8
+ logActivity,
9
+ type Task,
10
+ } from '../lib/kanban.js'
11
+ import {
12
+ getBoardByProject,
13
+ createTask as createObsidianTask,
14
+ updateTaskStatus as updateObsidianTaskStatus,
15
+ loadAllTasks,
16
+ type BoardColumn,
17
+ type ObsidianTask,
18
+ } from '../lib/kanban-obsidian.js'
14
19
  import { runAuditComparison } from '../lib/returnpro/audit.js'
15
20
  import { exportKpis, formatKpiTable, formatKpiCsv } from '../lib/returnpro/kpis.js'
16
21
  import { deploy, healthCheck, listApps } from '../lib/infra/deploy.js'
@@ -38,8 +43,6 @@ import { distributeNewsletter, checkDistributionStatus } from '../lib/newsletter
38
43
  import { generateSocialPosts } from '../lib/social/post-generator.js'
39
44
  import { publishSocialPosts, getPublishQueue, retryFailed } from '../lib/social/publish.js'
40
45
  import { publishBlog, createBlogPost, listBlogDrafts } from '../lib/cms/publish-blog.js'
41
- import { publishIgPhoto, getMetaConfigForBrand } from '../lib/social/meta.js'
42
- import { strapiGet, strapiPut, type StrapiPage } from '../lib/cms/strapi-client.js'
43
46
  import { migrateDb, listPendingMigrations, createMigration } from '../lib/infra/migrate.js'
44
47
  import { saveScenario, loadScenario, listScenarios, compareScenarios, deleteScenario } from '../lib/budget/scenarios.js'
45
48
  import { deleteBatch, previewBatch } from '../lib/transactions/delete-batch.js'
@@ -54,271 +57,318 @@ import {
54
57
  readLocalConfig,
55
58
  writeLocalConfig,
56
59
  } from '../lib/config/registry.js'
57
- import {
58
- sendHeartbeat, getActiveAgents,
59
- claimNextTask, releaseTask,
60
- reportProgress, reportCompletion, reportBlocked,
61
- runCoordinatorLoop, getCoordinatorStatus, assignTask, rebalance,
62
- } from '../lib/bot/index.js'
63
- import {
64
- colorize, table as fmtTable, statusBadge, priorityBadge,
65
- success, error as fmtError, warn as fmtWarn, info as fmtInfo,
66
- } from '../lib/format.js'
67
- import {
68
- listAssets, createAsset, updateAsset, getAsset, deleteAsset,
69
- trackAssetUsage, listAssetUsage, formatAssetTable,
70
- type AssetType, type AssetStatus,
71
- } from '../lib/assets/index.js'
72
60
 
73
61
  const program = new Command()
74
62
  .name('optimal')
75
63
  .description('Optimal CLI — unified skills for financial analytics, content, and infra')
76
64
  .version('0.1.0')
77
- .addHelpText('after', `
78
- Examples:
79
- $ optimal board view View the kanban board
80
- $ optimal board view -s in_progress Filter board by status
81
- $ optimal board claim --id <uuid> --agent bot1 Claim a task
82
- $ optimal project list List all projects
83
- $ optimal publish-instagram --brand CRE-11TRUST Publish to Instagram
84
- $ optimal social-queue --brand CRE-11TRUST View social post queue
85
- $ optimal generate-newsletter --brand CRE-11TRUST Generate a newsletter
86
- $ optimal audit-financials --months 2025-01 Audit a single month
87
- $ optimal export-kpis --format csv > kpis.csv Export KPIs as CSV
88
- $ optimal deploy dashboard --prod Deploy to production
89
- $ optimal bot agents List active bot agents
90
- $ optimal config doctor Validate local config
91
- `)
92
-
93
- // --- Board commands ---
94
- const board = program.command('board').description('Kanban board operations')
95
- .addHelpText('after', `
96
- Examples:
97
- $ optimal board view Show full board
98
- $ optimal board view -p cli-consolidation Filter by project
99
- $ optimal board view -s ready --mine bot1 Show bot1's ready tasks
100
- $ optimal board create -t "Fix bug" -p cli-consolidation
101
- $ optimal board claim --id <uuid> --agent bot1
102
- $ optimal board log --actor bot1 --limit 5
103
- `)
65
+
66
+ // Board commands (supabase-backed)
67
+ const board = program.command('board').description('Kanban board operations (supabase)')
104
68
 
105
69
  board
106
70
  .command('view')
107
71
  .description('Display the kanban board')
108
- .option('-p, --project <slug>', 'Project slug')
72
+ .option('-p, --project <slug>', 'Project slug', 'optimal-cli')
109
73
  .option('-s, --status <status>', 'Filter by status')
110
- .option('--mine <agent>', 'Show only tasks claimed by agent')
111
74
  .action(async (opts) => {
112
- const filters: { project_id?: string; status?: TaskStatus; claimed_by?: string } = {}
113
- if (opts.project) {
114
- const proj = await getProjectBySlug(opts.project)
115
- filters.project_id = proj.id
116
- }
117
- if (opts.status) filters.status = opts.status as TaskStatus
118
- if (opts.mine) filters.claimed_by = opts.mine
119
- const tasks = await listTasks(filters)
120
- console.log(formatBoardTable(tasks))
75
+ let tasks = await getBoard(opts.project)
76
+ if (opts.status) tasks = tasks.filter(t => t.status === opts.status)
77
+
78
+ const grouped = new Map<string, Task[]>()
79
+ for (const t of tasks) {
80
+ const list = grouped.get(t.status) ?? []
81
+ list.push(t)
82
+ grouped.set(t.status, list)
83
+ }
84
+
85
+ const order = ['in_progress', 'blocked', 'ready', 'backlog', 'review', 'done']
86
+ console.log('| Status | P | Title | Claimed By |')
87
+ console.log('|--------|---|-------|------------|')
88
+ for (const status of order) {
89
+ const list = grouped.get(status) ?? []
90
+ for (const t of list) {
91
+ console.log(
92
+ `| ${t.status} | ${t.priority} | ${t.title} | ${t.claimed_by ?? '—'} |`
93
+ )
94
+ }
95
+ }
96
+ console.log(`\nTotal: ${tasks.length} tasks`)
121
97
  })
122
98
 
123
99
  board
124
100
  .command('create')
125
101
  .description('Create a new task')
126
- .addHelpText('after', `
127
- Example:
128
- $ optimal board create -t "Migrate auth" -p cli-consolidation --priority 1 --labels infra,migration
129
- `)
130
102
  .requiredOption('-t, --title <title>', 'Task title')
131
- .requiredOption('-p, --project <slug>', 'Project slug')
103
+ .option('-p, --project <slug>', 'Project slug', 'optimal-cli')
132
104
  .option('-d, --description <desc>', 'Task description')
133
105
  .option('--priority <n>', 'Priority 1-4', '3')
134
- .option('--skill <ref>', 'Skill reference')
106
+ .option('--skill <ref>', 'Skill required')
135
107
  .option('--source <repo>', 'Source repo')
136
- .option('--target <module>', 'Target module')
137
- .option('--effort <size>', 'Effort: xs, s, m, l, xl')
138
- .option('--blocked-by <ids>', 'Comma-separated blocking task IDs')
139
- .option('--labels <labels>', 'Comma-separated labels')
140
108
  .action(async (opts) => {
141
- const project = await getProjectBySlug(opts.project)
142
109
  const task = await createTask({
143
- project_id: project.id,
110
+ project_slug: opts.project,
144
111
  title: opts.title,
145
112
  description: opts.description,
146
- priority: parseInt(opts.priority) as 1 | 2 | 3 | 4,
113
+ priority: parseInt(opts.priority),
147
114
  skill_required: opts.skill,
148
115
  source_repo: opts.source,
149
- target_module: opts.target,
150
- estimated_effort: opts.effort,
151
- blocked_by: opts.blockedBy?.split(',') ?? [],
152
- labels: opts.labels?.split(',') ?? [],
153
116
  })
154
- success(`Created task: ${colorize(task.id, 'dim')}\n ${task.title} [${statusBadge(task.status)}] ${priorityBadge(task.priority)}`)
117
+ console.log(`Created task: ${task.id} — "${task.title}" (priority ${task.priority}, status ${task.status})`)
155
118
  })
156
119
 
157
120
  board
158
121
  .command('update')
159
122
  .description('Update a task')
160
- .requiredOption('--id <uuid>', 'Task ID')
123
+ .requiredOption('--id <taskId>', 'Task UUID')
161
124
  .option('-s, --status <status>', 'New status')
162
125
  .option('-a, --agent <name>', 'Assign to agent')
163
126
  .option('--priority <n>', 'New priority')
164
- .option('-m, --message <msg>', 'Log message (adds comment)')
127
+ .option('-m, --message <msg>', 'Log message')
165
128
  .action(async (opts) => {
166
129
  const updates: Record<string, unknown> = {}
167
130
  if (opts.status) updates.status = opts.status
168
131
  if (opts.agent) updates.assigned_to = opts.agent
169
132
  if (opts.priority) updates.priority = parseInt(opts.priority)
170
- if (opts.status === 'done') updates.completed_at = new Date().toISOString()
171
- const task = await updateTask(opts.id, updates, opts.agent ?? 'cli')
172
- if (opts.message) await addComment({ task_id: task.id, author: opts.agent ?? 'cli', body: opts.message })
173
- success(`Updated: ${task.title} -> ${statusBadge(task.status)}`)
133
+
134
+ const task = await updateTask(opts.id, updates)
135
+ if (opts.message) {
136
+ await logActivity(opts.id, {
137
+ agent: opts.agent ?? 'cli',
138
+ action: 'status_change',
139
+ details: opts.message,
140
+ })
141
+ }
142
+ console.log(`Updated task ${task.id}: status → ${task.status}, assigned_to → ${task.assigned_to ?? '—'}`)
174
143
  })
175
144
 
176
145
  board
177
146
  .command('claim')
178
- .description('Claim a task (bot pull model)')
179
- .requiredOption('--id <uuid>', 'Task ID')
180
- .requiredOption('--agent <name>', 'Agent name')
147
+ .description('Claim a task for an agent')
148
+ .requiredOption('--id <taskId>', 'Task UUID')
149
+ .option('-a, --agent <name>', 'Agent name', 'oracle')
181
150
  .action(async (opts) => {
151
+ const { claimTask } = await import('../lib/kanban.js')
182
152
  const task = await claimTask(opts.id, opts.agent)
183
- success(`Claimed: ${colorize(task.title, 'cyan')} by ${colorize(opts.agent, 'bold')}`)
153
+ console.log(`Claimed task: ${task.title} by ${task.claimed_by} (status: ${task.status})`)
184
154
  })
185
155
 
186
156
  board
187
- .command('comment')
188
- .description('Add a comment to a task')
189
- .requiredOption('--id <uuid>', 'Task ID')
190
- .requiredOption('--author <name>', 'Author name')
191
- .requiredOption('--body <text>', 'Comment body')
157
+ .command('my-tasks')
158
+ .description('Show tasks claimed by an agent')
159
+ .option('-a, --agent <name>', 'Agent name', 'oracle')
160
+ .option('-p, --project <slug>', 'Project slug', 'optimal-cli')
192
161
  .action(async (opts) => {
193
- const comment = await addComment({ task_id: opts.id, author: opts.author, body: opts.body })
194
- success(`Comment added by ${colorize(comment.author, 'bold')} at ${colorize(comment.created_at, 'dim')}`)
162
+ const { getBoard } = await import('../lib/kanban.js')
163
+ const tasks = await getBoard(opts.project)
164
+ const myTasks = tasks.filter(t => t.claimed_by === opts.agent)
165
+
166
+ console.log(`\n📋 Tasks claimed by ${opts.agent}:\n`)
167
+ for (const t of myTasks) {
168
+ console.log(` [${t.status}] ${t.title} (priority ${t.priority})`)
169
+ }
170
+ console.log(`\nTotal: ${myTasks.length} tasks`)
195
171
  })
196
172
 
197
- board
198
- .command('log')
199
- .description('View activity log')
200
- .option('--task <uuid>', 'Filter by task ID')
201
- .option('--actor <name>', 'Filter by actor')
202
- .option('--limit <n>', 'Max entries', '20')
173
+ // Sync commands for 3-way sync
174
+ const syncCmd = program.command('sync').description('Sync tasks between obsidian and supabase')
175
+
176
+ syncCmd
177
+ .command('push')
178
+ .description('Push obsidian tasks to supabase')
179
+ .option('--project <slug>', 'Target project slug', 'optimal-tasks')
180
+ .option('--dry-run', 'Show what would be synced without writing', false)
203
181
  .action(async (opts) => {
204
- const entries = await listActivity({
205
- task_id: opts.task,
206
- actor: opts.actor,
207
- limit: parseInt(opts.limit),
208
- })
209
- for (const e of entries) {
210
- console.log(`${e.created_at} | ${e.actor.padEnd(8)} | ${e.action.padEnd(15)} | ${JSON.stringify(e.new_value ?? {})}`)
211
- }
212
- console.log(`\n${entries.length} entries`)
182
+ const { syncObsidianToSupabase } = await import('../lib/kanban-sync.js')
183
+ await syncObsidianToSupabase(opts.project, opts.dryRun)
213
184
  })
214
185
 
215
- // --- Project commands ---
216
- const proj = program.command('project').description('Project management')
217
- .addHelpText('after', `
218
- Examples:
219
- $ optimal project list
220
- $ optimal project create --slug my-proj --name "My Project" --priority 1
221
- $ optimal project update --slug my-proj -s active
222
- `)
223
-
224
- proj
225
- .command('list')
226
- .description('List all projects')
227
- .action(async () => {
228
- const projects = await listProjects()
229
- console.log('| Status | P | Slug | Owner | Name |')
230
- console.log('|----------|---|-------------------------|---------|------|')
231
- for (const p of projects) {
232
- console.log(`| ${p.status.padEnd(8)} | ${p.priority} | ${p.slug.padEnd(23)} | ${(p.owner ?? '—').padEnd(7)} | ${p.name} |`)
233
- }
186
+ syncCmd
187
+ .command('pull')
188
+ .description('Pull supabase tasks to obsidian')
189
+ .option('--project <slug>', 'Source project slug', 'optimal-tasks')
190
+ .option('--dry-run', 'Show what would be synced without writing', false)
191
+ .action(async (opts) => {
192
+ const { syncSupabaseToObsidian } = await import('../lib/kanban-sync.js')
193
+ await syncSupabaseToObsidian(opts.project, opts.dryRun)
234
194
  })
235
195
 
236
- proj
237
- .command('create')
238
- .description('Create a project')
239
- .requiredOption('--slug <slug>', 'Project slug')
240
- .requiredOption('--name <name>', 'Project name')
241
- .option('--owner <name>', 'Owner')
242
- .option('--priority <n>', 'Priority 1-4', '3')
196
+ syncCmd
197
+ .command('status')
198
+ .description('Show sync status between obsidian and supabase')
199
+ .option('--project <slug>', 'Project slug', 'optimal-tasks')
243
200
  .action(async (opts) => {
244
- const p = await createProject({
245
- slug: opts.slug,
246
- name: opts.name,
247
- owner: opts.owner,
248
- priority: parseInt(opts.priority) as 1 | 2 | 3 | 4,
249
- })
250
- success(`Created project: ${colorize(p.slug, 'cyan')} (${colorize(p.id, 'dim')})`)
201
+ const { getSyncStatus } = await import('../lib/kanban-sync.js')
202
+ await getSyncStatus(opts.project)
251
203
  })
252
204
 
253
- proj
254
- .command('update')
255
- .description('Update a project')
256
- .requiredOption('--slug <slug>', 'Project slug')
257
- .option('-s, --status <status>', 'New status')
258
- .option('--owner <name>', 'New owner')
205
+ // Obsidian-backed board commands (primary)
206
+ const oboard = program.command('oboard').description('Obsidian Kanban board (primary)')
207
+
208
+ oboard
209
+ .command('view')
210
+ .description('Display the obsidian task board')
211
+ .option('-p, --project <slug>', 'Project filter')
212
+ .option('-s, --status <status>', 'Filter by status (pending|in-progress|done)')
259
213
  .action(async (opts) => {
260
- const updates: Record<string, unknown> = {}
261
- if (opts.status) updates.status = opts.status
262
- if (opts.owner) updates.owner = opts.owner
263
- const p = await updateProject(opts.slug, updates)
264
- success(`Updated project: ${colorize(p.slug, 'cyan')} -> ${statusBadge(p.status)}`)
214
+ const columns = await getBoardByProject(opts.project)
215
+
216
+ if (opts.status) {
217
+ const col = columns.find(c => c.status === opts.status)
218
+ if (col) {
219
+ console.log(`\n## ${col.name}\n`)
220
+ for (const task of col.tasks) {
221
+ console.log(`- [${task.priority}] ${task.title} (${task.assignee || 'unassigned'})`)
222
+ }
223
+ console.log(`\nTotal: ${col.tasks.length} tasks`)
224
+ }
225
+ return
226
+ }
227
+
228
+ for (const col of columns) {
229
+ console.log(`\n## ${col.name} (${col.tasks.length})\n`)
230
+ for (const task of col.tasks) {
231
+ const tags = task.tags.length ? ` [${task.tags.join(', ')}]` : ''
232
+ console.log(`- [${task.priority}] ${task.title}${tags}`)
233
+ }
234
+ }
235
+ console.log(`\nTotal: ${columns.reduce((sum, c) => sum + c.tasks.length, 0)} tasks`)
265
236
  })
266
237
 
267
- // --- Milestone commands ---
268
- const ms = program.command('milestone').description('Milestone management')
269
- .addHelpText('after', `
270
- Examples:
271
- $ optimal milestone list --project cli-consolidation
272
- $ optimal milestone create --project cli-consolidation --name "v1.0" --due 2026-04-01
273
- `)
238
+ oboard
239
+ .command('list')
240
+ .description('List all tasks with details')
241
+ .option('-p, --project <slug>', 'Project filter')
242
+ .option('-s, --status <status>', 'Status filter')
243
+ .option('--assignee <name>', 'Filter by assignee')
244
+ .option('--owner <name>', 'Filter by owner')
245
+ .option('--json', 'Output as JSON (for agent consumption)', false)
246
+ .action(async (opts: { project?: string; status?: string; assignee?: string; owner?: string; json?: boolean }) => {
247
+ let tasks = await loadAllTasks()
248
+
249
+ if (opts.project) tasks = tasks.filter(t => t.project === opts.project)
250
+ if (opts.status) tasks = tasks.filter(t => t.status === opts.status)
251
+ if (opts.assignee) tasks = tasks.filter(t => t.assignee === opts.assignee)
252
+ if (opts.owner) tasks = tasks.filter(t => t.owner === opts.owner)
253
+
254
+ if (opts.json) {
255
+ // Agent-friendly JSON output
256
+ console.log(JSON.stringify(tasks.map(t => ({
257
+ id: t.id,
258
+ title: t.title,
259
+ status: t.status,
260
+ priority: t.priority,
261
+ owner: t.owner,
262
+ assignee: t.assignee,
263
+ project: t.project,
264
+ tags: t.tags,
265
+ updatedAt: t.updatedAt,
266
+ createdAt: t.createdAt
267
+ })), null, 2))
268
+ return
269
+ }
270
+
271
+ console.log(`\nFound ${tasks.length} tasks:\n`)
272
+ for (const task of tasks) {
273
+ console.log(`ID: ${task.id}`)
274
+ console.log(` Title: ${task.title}`)
275
+ console.log(` Status: ${task.status} | Priority: ${task.priority}`)
276
+ console.log(` Owner: ${task.owner || '—'} | Assignee: ${task.assignee || '—'}`)
277
+ console.log(` Project: ${task.project || '—'}`)
278
+ console.log(` Tags: ${task.tags.join(', ') || 'none'}`)
279
+ console.log(` Updated: ${task.updatedAt || 'never'}`)
280
+ console.log('')
281
+ }
282
+ })
274
283
 
275
- ms
284
+ oboard
276
285
  .command('create')
277
- .description('Create a milestone')
278
- .requiredOption('--project <slug>', 'Project slug')
279
- .requiredOption('--name <name>', 'Milestone name')
280
- .option('--due <date>', 'Due date (YYYY-MM-DD)')
286
+ .description('Create a new obsidian task')
287
+ .requiredOption('-t, --title <title>', 'Task title')
288
+ .option('-d, --description <desc>', 'Task description')
289
+ .option('-p, --project <slug>', 'Project slug')
290
+ .option('--priority <n>', 'Priority 1-4', '3')
291
+ .option('--owner <name>', 'Owner', 'oracle')
292
+ .option('--assignee <name>', 'Assignee')
293
+ .option('--tags <labels>', 'Comma-separated tags')
281
294
  .action(async (opts) => {
282
- const project = await getProjectBySlug(opts.project)
283
- const m = await createMilestone({ project_id: project.id, name: opts.name, due_date: opts.due })
284
- success(`Created milestone: ${colorize(m.name, 'cyan')} (${colorize(m.id, 'dim')})`)
295
+ const result = await createObsidianTask({
296
+ title: opts.title,
297
+ description: opts.description,
298
+ project: opts.project,
299
+ priority: parseInt(opts.priority) || 3,
300
+ owner: opts.owner,
301
+ assignee: opts.assignee,
302
+ tags: opts.tags?.split(',').map((t: string) => t.trim()),
303
+ })
304
+
305
+ if (result.ok) {
306
+ console.log(`✓ ${result.message}`)
307
+ console.log(` ID: ${result.task?.id}`)
308
+ } else {
309
+ console.error(`✗ ${result.message}`)
310
+ process.exit(1)
311
+ }
285
312
  })
286
313
 
287
- ms
288
- .command('list')
289
- .description('List milestones')
290
- .option('--project <slug>', 'Filter by project')
314
+ oboard
315
+ .command('update')
316
+ .description('Update an obsidian task')
317
+ .requiredOption('--id <taskId>', 'Task ID (or partial match)')
318
+ .option('-s, --status <status>', 'New status (pending|in-progress|done)')
319
+ .option('-a, --assignee <name>', 'Assign to agent')
320
+ .option('--owner <name>', 'Set owner')
321
+ .option('--priority <n>', 'Set priority 1-4')
322
+ .option('-m, --message <msg>', 'Log progress message')
291
323
  .action(async (opts) => {
292
- let projectId: string | undefined
293
- if (opts.project) {
294
- const p = await getProjectBySlug(opts.project)
295
- projectId = p.id
324
+ if (!opts.status && !opts.assignee && !opts.owner && !opts.priority && !opts.message) {
325
+ console.error('No updates specified. Use --status, --assignee, --owner, --priority, or --message')
326
+ process.exit(1)
296
327
  }
297
- const milestones = await listMilestones(projectId)
298
- for (const m of milestones) {
299
- console.log(`${m.status.padEnd(10)} | ${m.due_date ?? 'no date'} | ${m.name}`)
328
+
329
+ if (opts.status) {
330
+ const result = await updateObsidianTaskStatus(opts.id, opts.status, opts.message)
331
+ if (result.ok) {
332
+ console.log(`✓ ${result.message}`)
333
+ } else {
334
+ console.error(`✗ ${result.message}`)
335
+ process.exit(1)
336
+ }
337
+ }
338
+
339
+ // TODO: Add other update fields
340
+ if (opts.assignee || opts.owner || opts.priority) {
341
+ console.log('Note: Full field updates not yet implemented, use status for now')
300
342
  }
301
343
  })
302
344
 
303
- // --- Label commands ---
304
- const lbl = program.command('label').description('Label management')
305
-
306
- lbl
307
- .command('create')
308
- .description('Create a label')
309
- .requiredOption('--name <name>', 'Label name')
310
- .option('--color <hex>', 'Color hex code')
345
+ oboard
346
+ .command('done')
347
+ .description('Mark a task as done')
348
+ .requiredOption('--id <taskId>', 'Task ID (or partial match)')
349
+ .option('-m, --message <msg>', 'Completion message')
311
350
  .action(async (opts) => {
312
- const l = await createLabel(opts.name, opts.color)
313
- success(`Created label: ${colorize(l.name, 'cyan')} (${colorize(l.id, 'dim')})`)
351
+ const result = await updateObsidianTaskStatus(opts.id, 'done', opts.message)
352
+ if (result.ok) {
353
+ console.log(`✓ Completed: ${result.message}`)
354
+ } else {
355
+ console.error(`✗ ${result.message}`)
356
+ process.exit(1)
357
+ }
314
358
  })
315
359
 
316
- lbl
317
- .command('list')
318
- .description('List all labels')
319
- .action(async () => {
320
- const labels = await listLabels()
321
- for (const l of labels) console.log(`${l.name}${l.color ? ` (${l.color})` : ''}`)
360
+ oboard
361
+ .command('start')
362
+ .description('Mark a task as in-progress')
363
+ .requiredOption('--id <taskId>', 'Task ID (or partial match)')
364
+ .action(async (opts) => {
365
+ const result = await updateObsidianTaskStatus(opts.id, 'in-progress')
366
+ if (result.ok) {
367
+ console.log(`✓ Started: ${result.message}`)
368
+ } else {
369
+ console.error(`✗ ${result.message}`)
370
+ process.exit(1)
371
+ }
322
372
  })
323
373
 
324
374
  // Audit financials command
@@ -426,13 +476,13 @@ program
426
476
  .argument('<app>', `App to deploy (${listApps().join(', ')})`)
427
477
  .option('--prod', 'Deploy to production', false)
428
478
  .action(async (app: string, opts: { prod: boolean }) => {
429
- fmtInfo(`Deploying ${colorize(app, 'cyan')}${opts.prod ? colorize(' (production)', 'yellow') : ' (preview)'}...`)
479
+ console.log(`Deploying ${app}${opts.prod ? ' (production)' : ' (preview)'}...`)
430
480
  try {
431
481
  const url = await deploy(app, opts.prod)
432
- success(`Deployed: ${colorize(url, 'green')}`)
482
+ console.log(`Deployed: ${url}`)
433
483
  } catch (err) {
434
484
  const msg = err instanceof Error ? err.message : String(err)
435
- fmtError(`Deploy failed: ${msg}`)
485
+ console.error(`Deploy failed: ${msg}`)
436
486
  process.exit(1)
437
487
  }
438
488
  })
@@ -562,11 +612,11 @@ program
562
612
  })
563
613
 
564
614
  if (result.strapiDocumentId) {
565
- success(`Strapi documentId: ${colorize(result.strapiDocumentId, 'cyan')}`)
615
+ console.log(`\nStrapi documentId: ${result.strapiDocumentId}`)
566
616
  }
567
617
  } catch (err) {
568
618
  const msg = err instanceof Error ? err.message : String(err)
569
- fmtError(`Newsletter generation failed: ${msg}`)
619
+ console.error(`Newsletter generation failed: ${msg}`)
570
620
  process.exit(1)
571
621
  }
572
622
  })
@@ -956,78 +1006,6 @@ program
956
1006
  console.log(`\n${queue.length} posts queued`)
957
1007
  })
958
1008
 
959
- // ── Publish to Instagram via Meta Graph API ─────────────────────────
960
- program
961
- .command('publish-instagram')
962
- .description('Publish pending social posts to Instagram via Meta Graph API')
963
- .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
964
- .option('--limit <n>', 'Max posts to publish')
965
- .option('--dry-run', 'Preview without publishing', false)
966
- .action(async (opts: { brand: string; limit?: string; dryRun: boolean }) => {
967
- try {
968
- const config = getMetaConfigForBrand(opts.brand)
969
-
970
- // Fetch pending instagram posts from Strapi
971
- const result = await strapiGet<StrapiPage>('/api/social-posts', {
972
- 'filters[brand][$eq]': opts.brand,
973
- 'filters[delivery_status][$eq]': 'pending',
974
- 'filters[platform][$eq]': 'instagram',
975
- 'sort': 'scheduled_date:asc',
976
- 'pagination[pageSize]': opts.limit ?? '50',
977
- })
978
-
979
- const posts = result.data
980
- if (posts.length === 0) {
981
- console.log('No pending Instagram posts found')
982
- return
983
- }
984
-
985
- console.log(`Found ${posts.length} pending Instagram post(s) for ${opts.brand}`)
986
- let published = 0
987
- let failed = 0
988
-
989
- for (const post of posts) {
990
- const headline = (post.headline as string) ?? '(no headline)'
991
- const imageUrl = post.image_url as string | undefined
992
- const caption = ((post.body as string) ?? (post.headline as string) ?? '').trim()
993
-
994
- if (!imageUrl) {
995
- console.log(` SKIP | ${headline} — no image_url`)
996
- failed++
997
- continue
998
- }
999
-
1000
- if (opts.dryRun) {
1001
- console.log(` DRY | ${headline}`)
1002
- continue
1003
- }
1004
-
1005
- try {
1006
- const igResult = await publishIgPhoto(config, { imageUrl, caption })
1007
- await strapiPut('/api/social-posts', post.documentId, {
1008
- delivery_status: 'delivered',
1009
- platform_post_id: igResult.mediaId,
1010
- })
1011
- console.log(` OK | ${headline} → ${igResult.mediaId}`)
1012
- published++
1013
- } catch (err) {
1014
- const errMsg = err instanceof Error ? err.message : String(err)
1015
- await strapiPut('/api/social-posts', post.documentId, {
1016
- delivery_status: 'failed',
1017
- delivery_errors: [{ timestamp: new Date().toISOString(), error: errMsg }],
1018
- }).catch(() => {})
1019
- console.log(` FAIL | ${headline} — ${errMsg}`)
1020
- failed++
1021
- }
1022
- }
1023
-
1024
- console.log(`\nPublished: ${published} | Failed: ${failed}${opts.dryRun ? ' | (dry run)' : ''}`)
1025
- } catch (err) {
1026
- console.error(`Instagram publish failed: ${err instanceof Error ? err.message : String(err)}`)
1027
- process.exit(1)
1028
- }
1029
- })
1030
-
1031
1009
  // ── Publish blog ────────────────────────────────────────────────────
1032
1010
  program
1033
1011
  .command('publish-blog')
@@ -1065,12 +1043,6 @@ program
1065
1043
 
1066
1044
  // ── Database migration ──────────────────────────────────────────────
1067
1045
  const migrate = program.command('migrate').description('Supabase database migration operations')
1068
- .addHelpText('after', `
1069
- Examples:
1070
- $ optimal migrate pending --target optimalos
1071
- $ optimal migrate push --target returnpro --dry-run
1072
- $ optimal migrate create --target optimalos --name "add-index"
1073
- `)
1074
1046
 
1075
1047
  migrate
1076
1048
  .command('push')
@@ -1119,13 +1091,6 @@ migrate
1119
1091
 
1120
1092
  // ── Budget scenarios ────────────────────────────────────────────────
1121
1093
  const scenario = program.command('scenario').description('Budget scenario management')
1122
- .addHelpText('after', `
1123
- Examples:
1124
- $ optimal scenario list
1125
- $ optimal scenario save --name "4pct-growth" --adjustment-type percentage --adjustment-value 4
1126
- $ optimal scenario compare --names "baseline,4pct-growth"
1127
- $ optimal scenario delete --name "old-scenario"
1128
- `)
1129
1094
 
1130
1095
  scenario
1131
1096
  .command('save')
@@ -1265,15 +1230,6 @@ program
1265
1230
 
1266
1231
  // ── Config registry (v1 scaffold) ─────────────────────────────────
1267
1232
  const config = program.command('config').description('Manage optimal-cli local/shared config profile')
1268
- .addHelpText('after', `
1269
- Examples:
1270
- $ optimal config init --owner oracle --brand CRE-11TRUST
1271
- $ optimal config doctor
1272
- $ optimal config export --out ./backup.json
1273
- $ optimal config import --in ./backup.json
1274
- $ optimal config sync pull
1275
- $ optimal config sync push --agent bot1
1276
- `)
1277
1233
 
1278
1234
  config
1279
1235
  .command('init')
@@ -1410,7 +1366,15 @@ configSync
1410
1366
  .command('pull')
1411
1367
  .description('Pull config profile from shared registry into local config')
1412
1368
  .option('--profile <name>', 'Registry profile name', 'default')
1413
- .action(async (opts: { profile: string }) => {
1369
+ .option('--dry-run', 'Show what would be pulled without writing', false)
1370
+ .action(async (opts: { profile: string; dryRun?: boolean }) => {
1371
+ if (opts.dryRun) {
1372
+ const local = await readLocalConfig()
1373
+ const owner = local?.profile.owner || process.env.OPTIMAL_CONFIG_OWNER || 'unknown'
1374
+ console.log(`[dry-run] Would pull profile '${opts.profile}' for owner '${owner}' from registry`)
1375
+ console.log(`[dry-run] Local config: ${local ? 'exists' : 'none'}`)
1376
+ return
1377
+ }
1414
1378
  const result = await pullRegistryProfile(opts.profile)
1415
1379
  const stamp = new Date().toISOString()
1416
1380
  await appendHistory(`${stamp} sync.pull profile=${opts.profile} ok=${result.ok} msg=${result.message}`)
@@ -1427,7 +1391,20 @@ configSync
1427
1391
  .requiredOption('--agent <name>', 'Agent/owner name for the config profile')
1428
1392
  .option('--profile <name>', 'Registry profile name', 'default')
1429
1393
  .option('--force', 'Force write even on conflict', false)
1430
- .action(async (opts: { agent: string; profile: string; force?: boolean }) => {
1394
+ .option('--dry-run', 'Show what would be pushed without writing', false)
1395
+ .action(async (opts: { agent: string; profile: string; force?: boolean; dryRun?: boolean }) => {
1396
+ if (opts.dryRun) {
1397
+ const local = await readLocalConfig()
1398
+ if (!local) {
1399
+ console.error('[dry-run] No local config to push')
1400
+ process.exit(1)
1401
+ }
1402
+ const hash = hashConfig(local)
1403
+ console.log(`[dry-run] Would push profile '${opts.profile}' for agent '${opts.agent}' to registry`)
1404
+ console.log(`[dry-run] Local config hash: ${hash}`)
1405
+ console.log(`[dry-run] Force: ${Boolean(opts.force)}`)
1406
+ return
1407
+ }
1431
1408
  const result = await pushRegistryProfile(opts.profile, Boolean(opts.force), opts.agent)
1432
1409
  const stamp = new Date().toISOString()
1433
1410
  await appendHistory(`${stamp} sync.push agent=${opts.agent} profile=${opts.profile} force=${Boolean(opts.force)} ok=${result.ok} msg=${result.message}`)
@@ -1438,294 +1415,4 @@ configSync
1438
1415
  console.log(result.message)
1439
1416
  })
1440
1417
 
1441
- // --- Bot commands ---
1442
- const bot = program.command('bot').description('Bot agent orchestration')
1443
- .addHelpText('after', `
1444
- Examples:
1445
- $ optimal bot agents List active agents
1446
- $ optimal bot heartbeat --agent bot1 Send heartbeat
1447
- $ optimal bot claim --agent bot1 Claim next task
1448
- $ optimal bot report --task <id> --agent bot1 --message "50% done"
1449
- $ optimal bot complete --task <id> --agent bot1 --summary "All tests pass"
1450
- `)
1451
-
1452
- bot
1453
- .command('heartbeat')
1454
- .description('Send agent heartbeat')
1455
- .requiredOption('--agent <id>', 'Agent ID')
1456
- .option('--status <s>', 'Status: idle, working, error', 'idle')
1457
- .action(async (opts) => {
1458
- await sendHeartbeat(opts.agent, opts.status as 'idle' | 'working' | 'error')
1459
- success(`Heartbeat sent: ${colorize(opts.agent, 'bold')} [${colorize(opts.status, 'cyan')}]`)
1460
- })
1461
-
1462
- bot
1463
- .command('agents')
1464
- .description('List active agents (heartbeat in last 5 min)')
1465
- .action(async () => {
1466
- const agents = await getActiveAgents()
1467
- if (agents.length === 0) {
1468
- console.log('No active agents.')
1469
- return
1470
- }
1471
- console.log('| Agent | Status | Last Seen |')
1472
- console.log('|------------------|---------|---------------------|')
1473
- for (const a of agents) {
1474
- console.log(`| ${a.agent.padEnd(16)} | ${a.status.padEnd(7)} | ${a.lastSeen} |`)
1475
- }
1476
- })
1477
-
1478
- bot
1479
- .command('claim')
1480
- .description('Claim the next available task')
1481
- .requiredOption('--agent <id>', 'Agent ID')
1482
- .option('--skill <s>', 'Skill filter (comma-separated)')
1483
- .action(async (opts) => {
1484
- const skills = opts.skill ? opts.skill.split(',') : undefined
1485
- const task = await claimNextTask(opts.agent, skills)
1486
- if (!task) {
1487
- console.log('No claimable tasks found.')
1488
- return
1489
- }
1490
- success(`Claimed: ${colorize(task.title, 'cyan')} (${colorize(task.id, 'dim')}) by ${colorize(opts.agent, 'bold')}`)
1491
- })
1492
-
1493
- bot
1494
- .command('report')
1495
- .description('Report progress on a task')
1496
- .requiredOption('--task <id>', 'Task ID')
1497
- .requiredOption('--agent <id>', 'Agent ID')
1498
- .requiredOption('--message <msg>', 'Progress message')
1499
- .action(async (opts) => {
1500
- await reportProgress(opts.task, opts.agent, opts.message)
1501
- success(`Progress reported on ${colorize(opts.task, 'dim')}`)
1502
- })
1503
-
1504
- bot
1505
- .command('complete')
1506
- .description('Mark a task as done')
1507
- .requiredOption('--task <id>', 'Task ID')
1508
- .requiredOption('--agent <id>', 'Agent ID')
1509
- .requiredOption('--summary <s>', 'Completion summary')
1510
- .action(async (opts) => {
1511
- await reportCompletion(opts.task, opts.agent, opts.summary)
1512
- success(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('done')} by ${colorize(opts.agent, 'bold')}`)
1513
- })
1514
-
1515
- bot
1516
- .command('release')
1517
- .description('Release a claimed task back to ready')
1518
- .requiredOption('--task <id>', 'Task ID')
1519
- .requiredOption('--agent <id>', 'Agent ID')
1520
- .option('--reason <r>', 'Release reason')
1521
- .action(async (opts) => {
1522
- await releaseTask(opts.task, opts.agent, opts.reason)
1523
- fmtInfo(`Task ${colorize(opts.task, 'dim')} released by ${colorize(opts.agent, 'bold')}`)
1524
- })
1525
-
1526
- bot
1527
- .command('blocked')
1528
- .description('Mark a task as blocked')
1529
- .requiredOption('--task <id>', 'Task ID')
1530
- .requiredOption('--agent <id>', 'Agent ID')
1531
- .requiredOption('--reason <r>', 'Block reason')
1532
- .action(async (opts) => {
1533
- await reportBlocked(opts.task, opts.agent, opts.reason)
1534
- fmtWarn(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('blocked')}: ${opts.reason}`)
1535
- })
1536
-
1537
- // --- Coordinator commands ---
1538
- const coordinator = program.command('coordinator').description('Multi-agent coordination')
1539
- .addHelpText('after', `
1540
- Examples:
1541
- $ optimal coordinator start Run coordinator loop
1542
- $ optimal coordinator start --interval 10000 Poll every 10s
1543
- $ optimal coordinator status Show coordinator status
1544
- $ optimal coordinator assign --task <id> --agent bot1
1545
- $ optimal coordinator rebalance Release stale tasks
1546
- `)
1547
-
1548
- coordinator
1549
- .command('start')
1550
- .description('Run the coordinator loop')
1551
- .option('--interval <ms>', 'Poll interval in milliseconds', '30000')
1552
- .option('--max-agents <n>', 'Maximum agents to manage', '10')
1553
- .action(async (opts) => {
1554
- await runCoordinatorLoop({
1555
- pollIntervalMs: parseInt(opts.interval),
1556
- maxAgents: parseInt(opts.maxAgents),
1557
- })
1558
- })
1559
-
1560
- coordinator
1561
- .command('status')
1562
- .description('Show coordinator status')
1563
- .action(async () => {
1564
- const s = await getCoordinatorStatus()
1565
- console.log(`Last poll: ${s.lastPollAt ?? 'never'}`)
1566
- console.log(`Tasks — ready: ${s.tasksReady}, in progress: ${s.tasksInProgress}, blocked: ${s.tasksBlocked}`)
1567
- console.log(`\nActive agents (${s.activeAgents.length}):`)
1568
- for (const a of s.activeAgents) {
1569
- console.log(` ${a.agent.padEnd(16)} ${a.status.padEnd(8)} last seen ${a.lastSeen}`)
1570
- }
1571
- console.log(`\nIdle agents (${s.idleAgents.length}):`)
1572
- for (const a of s.idleAgents) {
1573
- console.log(` ${a.id.padEnd(16)} skills: ${a.skills.join(', ')}`)
1574
- }
1575
- })
1576
-
1577
- coordinator
1578
- .command('assign')
1579
- .description('Manually assign a task to an agent')
1580
- .requiredOption('--task <id>', 'Task ID')
1581
- .requiredOption('--agent <id>', 'Agent ID')
1582
- .action(async (opts) => {
1583
- const task = await assignTask(opts.task, opts.agent)
1584
- success(`Assigned: ${colorize(task.title, 'cyan')} -> ${colorize(opts.agent, 'bold')}`)
1585
- })
1586
-
1587
- coordinator
1588
- .command('rebalance')
1589
- .description('Release stale tasks and rebalance')
1590
- .action(async () => {
1591
- const result = await rebalance()
1592
- if (result.releasedTasks.length === 0) {
1593
- fmtInfo('No stale tasks found.')
1594
- return
1595
- }
1596
- console.log(`Released ${result.releasedTasks.length} stale task(s):`)
1597
- for (const t of result.releasedTasks) {
1598
- console.log(` ${colorize(t.id, 'dim')} ${t.title}`)
1599
- }
1600
- if (result.reassignedTasks.length > 0) {
1601
- console.log(`Reassigned ${result.reassignedTasks.length} task(s):`)
1602
- for (const t of result.reassignedTasks) {
1603
- console.log(` ${colorize(t.id, 'dim')} ${t.title} -> ${t.claimed_by}`)
1604
- }
1605
- }
1606
- })
1607
-
1608
- // --- Asset commands ---
1609
- const asset = program.command('asset').description('Digital asset tracking (domains, servers, API keys, services, repos)')
1610
- .addHelpText('after', `
1611
- Examples:
1612
- $ optimal asset list List all assets
1613
- $ optimal asset list --type domain --status active Filter by type/status
1614
- $ optimal asset add --name "op-hub.com" --type domain --owner clenisa
1615
- $ optimal asset update --id <uuid> --status inactive
1616
- $ optimal asset usage --id <uuid> View usage log
1617
- `)
1618
-
1619
- asset
1620
- .command('list')
1621
- .description('List tracked assets')
1622
- .option('-t, --type <type>', 'Filter by type (domain, server, api_key, service, repo, other)')
1623
- .option('-s, --status <status>', 'Filter by status (active, inactive, expired, pending)')
1624
- .option('-o, --owner <owner>', 'Filter by owner')
1625
- .option('--json', 'Output as JSON')
1626
- .action(async (opts) => {
1627
- const assets = await listAssets({
1628
- type: opts.type as AssetType | undefined,
1629
- status: opts.status as AssetStatus | undefined,
1630
- owner: opts.owner,
1631
- })
1632
- if (opts.json) {
1633
- console.log(JSON.stringify(assets, null, 2))
1634
- } else {
1635
- console.log(formatAssetTable(assets))
1636
- }
1637
- })
1638
-
1639
- asset
1640
- .command('add')
1641
- .description('Add a new asset')
1642
- .requiredOption('-n, --name <name>', 'Asset name')
1643
- .requiredOption('-t, --type <type>', 'Asset type (domain, server, api_key, service, repo, other)')
1644
- .option('-s, --status <status>', 'Status (default: active)')
1645
- .option('-o, --owner <owner>', 'Owner')
1646
- .option('--expires <date>', 'Expiration date (YYYY-MM-DD or ISO)')
1647
- .option('--meta <json>', 'Metadata JSON string')
1648
- .action(async (opts) => {
1649
- const metadata = opts.meta ? JSON.parse(opts.meta) : undefined
1650
- const created = await createAsset({
1651
- name: opts.name,
1652
- type: opts.type as AssetType,
1653
- status: opts.status as AssetStatus | undefined,
1654
- owner: opts.owner,
1655
- expires_at: opts.expires,
1656
- metadata,
1657
- })
1658
- success(`Created asset: ${colorize(created.name, 'cyan')} [${created.type}] (${colorize(created.id, 'dim')})`)
1659
- })
1660
-
1661
- asset
1662
- .command('update')
1663
- .description('Update an existing asset')
1664
- .requiredOption('--id <uuid>', 'Asset ID')
1665
- .option('-n, --name <name>', 'New name')
1666
- .option('-t, --type <type>', 'New type')
1667
- .option('-s, --status <status>', 'New status')
1668
- .option('-o, --owner <owner>', 'New owner')
1669
- .option('--expires <date>', 'New expiration date')
1670
- .option('--meta <json>', 'New metadata JSON')
1671
- .action(async (opts) => {
1672
- const updates: Record<string, unknown> = {}
1673
- if (opts.name) updates.name = opts.name
1674
- if (opts.type) updates.type = opts.type
1675
- if (opts.status) updates.status = opts.status
1676
- if (opts.owner) updates.owner = opts.owner
1677
- if (opts.expires) updates.expires_at = opts.expires
1678
- if (opts.meta) updates.metadata = JSON.parse(opts.meta)
1679
- const updated = await updateAsset(opts.id, updates)
1680
- success(`Updated: ${colorize(updated.name, 'cyan')} -> status=${colorize(updated.status, 'bold')}`)
1681
- })
1682
-
1683
- asset
1684
- .command('get')
1685
- .description('Get a single asset by ID')
1686
- .requiredOption('--id <uuid>', 'Asset ID')
1687
- .action(async (opts) => {
1688
- const a = await getAsset(opts.id)
1689
- console.log(JSON.stringify(a, null, 2))
1690
- })
1691
-
1692
- asset
1693
- .command('remove')
1694
- .description('Delete an asset')
1695
- .requiredOption('--id <uuid>', 'Asset ID')
1696
- .action(async (opts) => {
1697
- await deleteAsset(opts.id)
1698
- success(`Deleted asset ${colorize(opts.id, 'dim')}`)
1699
- })
1700
-
1701
- asset
1702
- .command('track')
1703
- .description('Log a usage event for an asset')
1704
- .requiredOption('--id <uuid>', 'Asset ID')
1705
- .requiredOption('-e, --event <event>', 'Event name (e.g. "renewed", "deployed", "rotated")')
1706
- .option('--actor <name>', 'Who performed the action')
1707
- .option('--meta <json>', 'Event metadata JSON')
1708
- .action(async (opts) => {
1709
- const metadata = opts.meta ? JSON.parse(opts.meta) : undefined
1710
- const entry = await trackAssetUsage(opts.id, opts.event, opts.actor, metadata)
1711
- success(`Tracked: ${colorize(opts.event, 'cyan')} on ${colorize(opts.id, 'dim')} at ${colorize(entry.created_at, 'dim')}`)
1712
- })
1713
-
1714
- asset
1715
- .command('usage')
1716
- .description('View usage log for an asset')
1717
- .requiredOption('--id <uuid>', 'Asset ID')
1718
- .option('--limit <n>', 'Max entries', '20')
1719
- .action(async (opts) => {
1720
- const events = await listAssetUsage(opts.id, parseInt(opts.limit))
1721
- if (events.length === 0) {
1722
- console.log('No usage events found.')
1723
- return
1724
- }
1725
- for (const e of events) {
1726
- console.log(`${e.created_at} | ${(e.actor ?? '-').padEnd(10)} | ${e.event} ${Object.keys(e.metadata).length > 0 ? JSON.stringify(e.metadata) : ''}`)
1727
- }
1728
- console.log(`\n${events.length} events`)
1729
- })
1730
-
1731
1418
  program.parseAsync()