optimal-cli 1.0.0 → 1.0.1

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 (135) hide show
  1. package/dist/bin/optimal.d.ts +2 -0
  2. package/dist/bin/optimal.js +1590 -0
  3. package/dist/lib/assets/index.d.ts +79 -0
  4. package/dist/lib/assets/index.js +153 -0
  5. package/dist/lib/assets.d.ts +20 -0
  6. package/dist/lib/assets.js +112 -0
  7. package/dist/lib/auth/index.d.ts +83 -0
  8. package/dist/lib/auth/index.js +146 -0
  9. package/dist/lib/board/index.d.ts +39 -0
  10. package/dist/lib/board/index.js +285 -0
  11. package/dist/lib/board/types.d.ts +111 -0
  12. package/dist/lib/board/types.js +1 -0
  13. package/dist/lib/bot/claim.d.ts +3 -0
  14. package/dist/lib/bot/claim.js +20 -0
  15. package/dist/lib/bot/coordinator.d.ts +27 -0
  16. package/dist/lib/bot/coordinator.js +178 -0
  17. package/dist/lib/bot/heartbeat.d.ts +6 -0
  18. package/dist/lib/bot/heartbeat.js +30 -0
  19. package/dist/lib/bot/index.d.ts +9 -0
  20. package/dist/lib/bot/index.js +6 -0
  21. package/dist/lib/bot/protocol.d.ts +12 -0
  22. package/dist/lib/bot/protocol.js +74 -0
  23. package/dist/lib/bot/reporter.d.ts +3 -0
  24. package/dist/lib/bot/reporter.js +27 -0
  25. package/dist/lib/bot/skills.d.ts +26 -0
  26. package/dist/lib/bot/skills.js +69 -0
  27. package/dist/lib/budget/projections.d.ts +115 -0
  28. package/dist/lib/budget/projections.js +384 -0
  29. package/dist/lib/budget/scenarios.d.ts +93 -0
  30. package/dist/lib/budget/scenarios.js +214 -0
  31. package/dist/lib/cms/publish-blog.d.ts +62 -0
  32. package/dist/lib/cms/publish-blog.js +74 -0
  33. package/dist/lib/cms/strapi-client.d.ts +123 -0
  34. package/dist/lib/cms/strapi-client.js +213 -0
  35. package/dist/lib/config/registry.d.ts +17 -0
  36. package/dist/lib/config/registry.js +182 -0
  37. package/dist/lib/config/schema.d.ts +31 -0
  38. package/dist/lib/config/schema.js +25 -0
  39. package/dist/lib/config.d.ts +55 -0
  40. package/dist/lib/config.js +206 -0
  41. package/dist/lib/errors.d.ts +25 -0
  42. package/dist/lib/errors.js +91 -0
  43. package/dist/lib/format.d.ts +28 -0
  44. package/dist/lib/format.js +98 -0
  45. package/dist/lib/infra/deploy.d.ts +29 -0
  46. package/dist/lib/infra/deploy.js +58 -0
  47. package/dist/lib/infra/migrate.d.ts +34 -0
  48. package/dist/lib/infra/migrate.js +103 -0
  49. package/dist/lib/newsletter/distribute.d.ts +52 -0
  50. package/dist/lib/newsletter/distribute.js +193 -0
  51. package/{lib/newsletter/generate-insurance.ts → dist/lib/newsletter/generate-insurance.d.ts} +7 -24
  52. package/dist/lib/newsletter/generate-insurance.js +36 -0
  53. package/dist/lib/newsletter/generate.d.ts +104 -0
  54. package/dist/lib/newsletter/generate.js +571 -0
  55. package/dist/lib/returnpro/anomalies.d.ts +64 -0
  56. package/dist/lib/returnpro/anomalies.js +166 -0
  57. package/dist/lib/returnpro/audit.d.ts +32 -0
  58. package/dist/lib/returnpro/audit.js +147 -0
  59. package/dist/lib/returnpro/diagnose.d.ts +52 -0
  60. package/dist/lib/returnpro/diagnose.js +281 -0
  61. package/dist/lib/returnpro/kpis.d.ts +32 -0
  62. package/dist/lib/returnpro/kpis.js +192 -0
  63. package/dist/lib/returnpro/templates.d.ts +48 -0
  64. package/dist/lib/returnpro/templates.js +229 -0
  65. package/dist/lib/returnpro/upload-income.d.ts +25 -0
  66. package/dist/lib/returnpro/upload-income.js +235 -0
  67. package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
  68. package/dist/lib/returnpro/upload-netsuite.js +566 -0
  69. package/dist/lib/returnpro/upload-r1.d.ts +48 -0
  70. package/dist/lib/returnpro/upload-r1.js +398 -0
  71. package/dist/lib/returnpro/validate.d.ts +37 -0
  72. package/dist/lib/returnpro/validate.js +124 -0
  73. package/dist/lib/social/meta.d.ts +90 -0
  74. package/dist/lib/social/meta.js +160 -0
  75. package/dist/lib/social/post-generator.d.ts +83 -0
  76. package/dist/lib/social/post-generator.js +333 -0
  77. package/dist/lib/social/publish.d.ts +66 -0
  78. package/dist/lib/social/publish.js +226 -0
  79. package/dist/lib/social/scraper.d.ts +67 -0
  80. package/dist/lib/social/scraper.js +361 -0
  81. package/dist/lib/supabase.d.ts +4 -0
  82. package/dist/lib/supabase.js +20 -0
  83. package/dist/lib/transactions/delete-batch.d.ts +60 -0
  84. package/dist/lib/transactions/delete-batch.js +203 -0
  85. package/dist/lib/transactions/ingest.d.ts +43 -0
  86. package/dist/lib/transactions/ingest.js +555 -0
  87. package/dist/lib/transactions/stamp.d.ts +51 -0
  88. package/dist/lib/transactions/stamp.js +524 -0
  89. package/package.json +3 -4
  90. package/bin/optimal.ts +0 -1731
  91. package/lib/assets/index.ts +0 -225
  92. package/lib/assets.ts +0 -124
  93. package/lib/auth/index.ts +0 -189
  94. package/lib/board/index.ts +0 -309
  95. package/lib/board/types.ts +0 -124
  96. package/lib/bot/claim.ts +0 -43
  97. package/lib/bot/coordinator.ts +0 -254
  98. package/lib/bot/heartbeat.ts +0 -37
  99. package/lib/bot/index.ts +0 -9
  100. package/lib/bot/protocol.ts +0 -99
  101. package/lib/bot/reporter.ts +0 -42
  102. package/lib/bot/skills.ts +0 -81
  103. package/lib/budget/projections.ts +0 -561
  104. package/lib/budget/scenarios.ts +0 -312
  105. package/lib/cms/publish-blog.ts +0 -129
  106. package/lib/cms/strapi-client.ts +0 -302
  107. package/lib/config/registry.ts +0 -228
  108. package/lib/config/schema.ts +0 -58
  109. package/lib/config.ts +0 -247
  110. package/lib/errors.ts +0 -129
  111. package/lib/format.ts +0 -120
  112. package/lib/infra/.gitkeep +0 -0
  113. package/lib/infra/deploy.ts +0 -70
  114. package/lib/infra/migrate.ts +0 -141
  115. package/lib/newsletter/.gitkeep +0 -0
  116. package/lib/newsletter/distribute.ts +0 -256
  117. package/lib/newsletter/generate.ts +0 -735
  118. package/lib/returnpro/.gitkeep +0 -0
  119. package/lib/returnpro/anomalies.ts +0 -258
  120. package/lib/returnpro/audit.ts +0 -194
  121. package/lib/returnpro/diagnose.ts +0 -400
  122. package/lib/returnpro/kpis.ts +0 -255
  123. package/lib/returnpro/templates.ts +0 -323
  124. package/lib/returnpro/upload-income.ts +0 -311
  125. package/lib/returnpro/upload-netsuite.ts +0 -696
  126. package/lib/returnpro/upload-r1.ts +0 -563
  127. package/lib/returnpro/validate.ts +0 -154
  128. package/lib/social/meta.ts +0 -228
  129. package/lib/social/post-generator.ts +0 -468
  130. package/lib/social/publish.ts +0 -301
  131. package/lib/social/scraper.ts +0 -503
  132. package/lib/supabase.ts +0 -25
  133. package/lib/transactions/delete-batch.ts +0 -258
  134. package/lib/transactions/ingest.ts +0 -659
  135. package/lib/transactions/stamp.ts +0 -654
package/lib/config.ts DELETED
@@ -1,247 +0,0 @@
1
- import { createClient, SupabaseClient } from '@supabase/supabase-js'
2
- import { readFileSync, writeFileSync, existsSync } from 'node:fs'
3
- import { homedir } from 'node:os'
4
- import { join } from 'node:path'
5
-
6
- const CONFIG_DIR = join(homedir(), '.optimal')
7
- const LOCAL_CONFIG_PATH = join(CONFIG_DIR, 'config.json')
8
- const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json')
9
-
10
- // Get Supabase client for OptimalOS instance (stores CLI configs)
11
- function getOptimalSupabase(): SupabaseClient {
12
- const url = process.env.OPTIMAL_SUPABASE_URL
13
- const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY
14
- if (!url || !key) {
15
- throw new Error('OPTIMAL_SUPABASE_URL and OPTIMAL_SUPABASE_SERVICE_KEY must be set')
16
- }
17
- return createClient(url, key)
18
- }
19
-
20
- interface ConfigRecord {
21
- id: string
22
- agent_name: string
23
- config_json: Record<string, unknown>
24
- version: string
25
- created_at: string
26
- updated_at: string
27
- }
28
-
29
- /**
30
- * Initialize local config directory
31
- */
32
- export function initConfigDir(): void {
33
- if (!existsSync(CONFIG_DIR)) {
34
- import('node:fs').then(fs => fs.mkdirSync(CONFIG_DIR, { recursive: true }))
35
- }
36
- }
37
-
38
- /**
39
- * Load local openclaw.json
40
- */
41
- export function loadLocalConfig(): Record<string, unknown> | null {
42
- if (!existsSync(OPENCLAW_CONFIG_PATH)) {
43
- return null
44
- }
45
- try {
46
- const raw = readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8')
47
- return JSON.parse(raw)
48
- } catch {
49
- return null
50
- }
51
- }
52
-
53
- /**
54
- * Save config to local openclaw.json
55
- */
56
- export function saveLocalConfig(config: Record<string, unknown>): void {
57
- writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2))
58
- }
59
-
60
- /**
61
- * Push current openclaw.json to Supabase
62
- */
63
- export async function pushConfig(agentName: string): Promise<{ id: string; version: string }> {
64
- const supabase = getOptimalSupabase()
65
- const config = loadLocalConfig()
66
-
67
- if (!config) {
68
- throw new Error(`No config found at ${OPENCLAW_CONFIG_PATH}`)
69
- }
70
-
71
- // Generate version timestamp
72
- const version = new Date().toISOString()
73
-
74
- // Check if config exists for this agent
75
- const { data: existing } = await supabase
76
- .from('agent_configs')
77
- .select('id')
78
- .eq('agent_name', agentName)
79
- .single()
80
-
81
- let result
82
- if (existing) {
83
- // Update existing
84
- const { data, error } = await supabase
85
- .from('agent_configs')
86
- .update({
87
- config_json: config,
88
- version,
89
- updated_at: version,
90
- })
91
- .eq('id', existing.id)
92
- .select()
93
- .single()
94
-
95
- if (error) throw error
96
- result = data
97
- } else {
98
- // Insert new
99
- const { data, error } = await supabase
100
- .from('agent_configs')
101
- .insert({
102
- agent_name: agentName,
103
- config_json: config,
104
- version,
105
- })
106
- .select()
107
- .single()
108
-
109
- if (error) throw error
110
- result = data
111
- }
112
-
113
- return { id: result.id, version }
114
- }
115
-
116
- /**
117
- * Pull config from Supabase and save to local openclaw.json
118
- */
119
- export async function pullConfig(agentName: string): Promise<ConfigRecord> {
120
- const supabase = getOptimalSupabase()
121
-
122
- const { data, error } = await supabase
123
- .from('agent_configs')
124
- .select('*')
125
- .eq('agent_name', agentName)
126
- .order('updated_at', { ascending: false })
127
- .limit(1)
128
- .single()
129
-
130
- if (error) {
131
- throw new Error(`No config found for agent: ${agentName}`)
132
- }
133
-
134
- // Save to local
135
- saveLocalConfig(data.config_json)
136
-
137
- return data as ConfigRecord
138
- }
139
-
140
- /**
141
- * List all saved agent configs
142
- */
143
- export async function listConfigs(): Promise<Array<{ agent_name: string; version: string; updated_at: string }>> {
144
- const supabase = getOptimalSupabase()
145
-
146
- const { data, error } = await supabase
147
- .from('agent_configs')
148
- .select('agent_name, version, updated_at')
149
- .order('updated_at', { ascending: false })
150
-
151
- if (error) {
152
- throw new Error(`Failed to list configs: ${error.message}`)
153
- }
154
-
155
- return data || []
156
- }
157
-
158
- /**
159
- * Compare local config with cloud version
160
- */
161
- export async function diffConfig(agentName: string): Promise<{
162
- local: Record<string, unknown> | null
163
- cloud: ConfigRecord | null
164
- differences: string[]
165
- }> {
166
- const local = loadLocalConfig()
167
- let cloud: ConfigRecord | null = null
168
-
169
- try {
170
- const supabase = getOptimalSupabase()
171
- const { data } = await supabase
172
- .from('agent_configs')
173
- .select('*')
174
- .eq('agent_name', agentName)
175
- .single()
176
- cloud = data as ConfigRecord
177
- } catch {
178
- // Cloud config doesn't exist
179
- }
180
-
181
- const differences: string[] = []
182
-
183
- if (!local && !cloud) {
184
- differences.push('No local or cloud config found')
185
- } else if (!local) {
186
- differences.push('No local config (cloud exists)')
187
- } else if (!cloud) {
188
- differences.push('No cloud config (local exists)')
189
- } else {
190
- // Simple diff on top-level keys
191
- const localKeys = Object.keys(local).sort()
192
- const cloudKeys = Object.keys(cloud.config_json).sort()
193
-
194
- if (JSON.stringify(localKeys) !== JSON.stringify(cloudKeys)) {
195
- differences.push('Top-level keys differ')
196
- }
197
-
198
- // Check version
199
- const localMeta = (local as any).meta
200
- if (localMeta?.lastTouchedVersion !== cloud.version) {
201
- differences.push(`Version mismatch: local=${localMeta?.lastTouchedVersion}, cloud=${cloud.version}`)
202
- }
203
- }
204
-
205
- return { local, cloud, differences }
206
- }
207
-
208
- /**
209
- * Sync config (two-way merge)
210
- */
211
- export async function syncConfig(agentName: string): Promise<{
212
- action: 'pushed' | 'pulled' | 'merged' | 'none'
213
- message: string
214
- }> {
215
- const { local, cloud, differences } = await diffConfig(agentName)
216
-
217
- if (!local && !cloud) {
218
- return { action: 'none', message: 'No configs to sync' }
219
- }
220
-
221
- if (!cloud) {
222
- // Only local exists - push
223
- const result = await pushConfig(agentName)
224
- return { action: 'pushed', message: `Pushed to cloud (version ${result.version})` }
225
- }
226
-
227
- if (!local) {
228
- // Only cloud exists - pull
229
- await pullConfig(agentName)
230
- return { action: 'pulled', message: `Pulled from cloud (version ${cloud.version})` }
231
- }
232
-
233
- // Both exist - compare timestamps
234
- const localTime = (local as any).meta?.lastTouchedAt || '1970-01-01'
235
- const localVersion = (local as any).meta?.lastTouchedVersion || 'unknown'
236
- const cloudTime = cloud.updated_at
237
-
238
- if (localTime > cloudTime) {
239
- const result = await pushConfig(agentName)
240
- return { action: 'pushed', message: `Local is newer - pushed to cloud (version ${result.version})` }
241
- } else if (cloudTime > localTime) {
242
- await pullConfig(agentName)
243
- return { action: 'pulled', message: `Cloud is newer - pulled from cloud (version ${cloud.version})` }
244
- } else {
245
- return { action: 'none', message: 'Configs are in sync' }
246
- }
247
- }
package/lib/errors.ts DELETED
@@ -1,129 +0,0 @@
1
- /**
2
- * Centralized error handling for the Optimal CLI.
3
- *
4
- * Provides a typed CliError class, a user-friendly formatter,
5
- * and a wrapCommand helper for Commander action handlers.
6
- */
7
-
8
- // ── Error codes ──────────────────────────────────────────────────────────────
9
-
10
- export type ErrorCode =
11
- | 'MISSING_ENV'
12
- | 'NOT_FOUND'
13
- | 'SUPABASE_ERROR'
14
- | 'VALIDATION_ERROR'
15
- | 'AUTH_ERROR'
16
- | 'NETWORK_ERROR'
17
- | 'FILE_ERROR'
18
- | 'UNKNOWN'
19
-
20
- // ── CliError ─────────────────────────────────────────────────────────────────
21
-
22
- export class CliError extends Error {
23
- constructor(
24
- message: string,
25
- public code: ErrorCode,
26
- public suggestion?: string,
27
- ) {
28
- super(message)
29
- this.name = 'CliError'
30
- }
31
- }
32
-
33
- // ── Helpers ──────────────────────────────────────────────────────────────────
34
-
35
- const SUGGESTIONS: Record<string, string> = {
36
- MISSING_ENV:
37
- 'Ensure the required environment variables are set in your .env file or shell.',
38
- NOT_FOUND: 'Double-check the identifier (slug, ID, or name) and try again.',
39
- SUPABASE_ERROR:
40
- 'Verify your Supabase URL and service key are correct and the database is reachable.',
41
- VALIDATION_ERROR: 'Review the command options with --help.',
42
- AUTH_ERROR:
43
- 'Check your API token or credentials and make sure they have not expired.',
44
- NETWORK_ERROR:
45
- 'Check your internet connection and verify the remote service is available.',
46
- FILE_ERROR: 'Verify the file path exists and you have read/write permissions.',
47
- }
48
-
49
- function classifyError(err: unknown): { code: ErrorCode; message: string } {
50
- if (err instanceof CliError) {
51
- return { code: err.code, message: err.message }
52
- }
53
-
54
- if (err instanceof Error) {
55
- const msg = err.message
56
-
57
- // Supabase / fetch errors
58
- if (msg.includes('PGRST') || msg.includes('supabase') || msg.includes('relation')) {
59
- return { code: 'SUPABASE_ERROR', message: msg }
60
- }
61
- if (msg.includes('ENOENT') || msg.includes('no such file')) {
62
- return { code: 'FILE_ERROR', message: msg }
63
- }
64
- if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed') || msg.includes('ETIMEDOUT')) {
65
- return { code: 'NETWORK_ERROR', message: msg }
66
- }
67
- if (
68
- msg.includes('OPTIMAL_SUPABASE_URL') ||
69
- msg.includes('OPTIMAL_SUPABASE_SERVICE_KEY') ||
70
- msg.includes('env')
71
- ) {
72
- return { code: 'MISSING_ENV', message: msg }
73
- }
74
-
75
- return { code: 'UNKNOWN', message: msg }
76
- }
77
-
78
- return { code: 'UNKNOWN', message: String(err) }
79
- }
80
-
81
- // ── handleError ──────────────────────────────────────────────────────────────
82
-
83
- /**
84
- * Format an error for CLI output, print it to stderr, and exit with code 1.
85
- */
86
- export function handleError(err: unknown): never {
87
- const { code, message } = classifyError(err)
88
-
89
- const suggestion =
90
- err instanceof CliError && err.suggestion
91
- ? err.suggestion
92
- : SUGGESTIONS[code] ?? ''
93
-
94
- const lines: string[] = [
95
- '',
96
- ` Error [${code}]: ${message}`,
97
- ]
98
-
99
- if (suggestion) {
100
- lines.push(` Suggestion: ${suggestion}`)
101
- }
102
-
103
- lines.push('')
104
-
105
- process.stderr.write(lines.join('\n'))
106
- process.exit(1)
107
- }
108
-
109
- // ── wrapCommand ──────────────────────────────────────────────────────────────
110
-
111
- /**
112
- * Wrap a Commander action handler so any thrown error is routed through
113
- * handleError, giving the user a consistent, friendly message instead of
114
- * an unhandled-rejection stack trace.
115
- *
116
- * Usage:
117
- * .action(wrapCommand(async (opts) => { ... }))
118
- */
119
- export function wrapCommand<A extends unknown[]>(
120
- fn: (...args: A) => Promise<void>,
121
- ): (...args: A) => Promise<void> {
122
- return async (...args: A) => {
123
- try {
124
- await fn(...args)
125
- } catch (err) {
126
- handleError(err)
127
- }
128
- }
129
- }
package/lib/format.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Lightweight CLI output formatting — ANSI colors, tables, badges.
3
- * Zero external dependencies. Respects NO_COLOR env var.
4
- */
5
-
6
- // ── ANSI escape codes ───────────────────────────────────────────────
7
-
8
- const CODES: Record<string, [number, number]> = {
9
- red: [31, 39],
10
- green: [32, 39],
11
- yellow: [33, 39],
12
- blue: [34, 39],
13
- cyan: [36, 39],
14
- gray: [90, 39],
15
- bold: [1, 22],
16
- dim: [2, 22],
17
- }
18
-
19
- type Color = 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'gray' | 'bold' | 'dim'
20
-
21
- /**
22
- * Wrap text in ANSI escape codes for the given color/style.
23
- * Returns plain text when NO_COLOR env var is set.
24
- */
25
- export function colorize(text: string, color: Color): string {
26
- if (process.env.NO_COLOR !== undefined) return text
27
- const [open, close] = CODES[color]
28
- return `\x1b[${open}m${text}\x1b[${close}m`
29
- }
30
-
31
- // ── Table rendering ─────────────────────────────────────────────────
32
-
33
- /** Strip ANSI escape sequences to measure visible string width. */
34
- function stripAnsi(s: string): string {
35
- return s.replace(/\x1b\[\d+m/g, '')
36
- }
37
-
38
- /**
39
- * Render a bordered ASCII table with auto-sized columns.
40
- * Headers are rendered in bold.
41
- */
42
- export function table(headers: string[], rows: string[][]): string {
43
- // Compute column widths from headers and all rows
44
- const colWidths = headers.map((h, i) => {
45
- let max = stripAnsi(h).length
46
- for (const row of rows) {
47
- const cell = row[i] ?? ''
48
- const len = stripAnsi(cell).length
49
- if (len > max) max = len
50
- }
51
- return max
52
- })
53
-
54
- function padCell(cell: string, width: number): string {
55
- const visible = stripAnsi(cell).length
56
- return cell + ' '.repeat(Math.max(0, width - visible))
57
- }
58
-
59
- const sep = '+-' + colWidths.map(w => '-'.repeat(w)).join('-+-') + '-+'
60
- const headerRow = '| ' + headers.map((h, i) => padCell(colorize(h, 'bold'), colWidths[i])).join(' | ') + ' |'
61
-
62
- const bodyRows = rows.map(row =>
63
- '| ' + row.map((cell, i) => padCell(cell ?? '', colWidths[i])).join(' | ') + ' |'
64
- )
65
-
66
- return [sep, headerRow, sep, ...bodyRows, sep].join('\n')
67
- }
68
-
69
- // ── Status & priority badges ────────────────────────────────────────
70
-
71
- const STATUS_COLORS: Record<string, Color> = {
72
- done: 'green',
73
- in_progress: 'blue',
74
- blocked: 'red',
75
- ready: 'cyan',
76
- backlog: 'gray',
77
- cancelled: 'dim',
78
- review: 'yellow',
79
- }
80
-
81
- /** Return a colored status string (e.g. "done" in green). */
82
- export function statusBadge(status: string): string {
83
- const color = STATUS_COLORS[status] ?? 'dim'
84
- return colorize(status, color)
85
- }
86
-
87
- const PRIORITY_COLORS: Record<number, Color> = {
88
- 1: 'red',
89
- 2: 'yellow',
90
- 3: 'blue',
91
- 4: 'gray',
92
- }
93
-
94
- /** Return a colored priority label (e.g. "P1" in red). */
95
- export function priorityBadge(p: number): string {
96
- const color = PRIORITY_COLORS[p] ?? 'gray'
97
- return colorize(`P${p}`, color)
98
- }
99
-
100
- // ── Logging helpers ─────────────────────────────────────────────────
101
-
102
- /** Print a green success message with a check mark prefix. */
103
- export function success(msg: string): void {
104
- console.log(`${colorize('\u2713', 'green')} ${msg}`)
105
- }
106
-
107
- /** Print a red error message with an X prefix. */
108
- export function error(msg: string): void {
109
- console.error(`${colorize('\u2717', 'red')} ${msg}`)
110
- }
111
-
112
- /** Print a yellow warning message with a warning prefix. */
113
- export function warn(msg: string): void {
114
- console.warn(`${colorize('\u26a0', 'yellow')} ${msg}`)
115
- }
116
-
117
- /** Print a blue info message with an info prefix. */
118
- export function info(msg: string): void {
119
- console.log(`${colorize('\u2139', 'blue')} ${msg}`)
120
- }
File without changes
@@ -1,70 +0,0 @@
1
- import { execFile } from 'node:child_process'
2
- import { promisify } from 'node:util'
3
-
4
- const run = promisify(execFile)
5
-
6
- /** Map of short app names to absolute filesystem paths. */
7
- const APP_PATHS: Record<string, string> = {
8
- 'dashboard-returnpro': '/home/optimal/dashboard-returnpro',
9
- 'optimalos': '/home/optimal/optimalos',
10
- 'portfolio': '/home/optimal/portfolio-2026',
11
- 'newsletter-preview': '/home/optimal/projects/newsletter-preview',
12
- 'wes': '/home/optimal/wes-dashboard',
13
- }
14
-
15
- /**
16
- * List all available app names that can be deployed.
17
- */
18
- export function listApps(): string[] {
19
- return Object.keys(APP_PATHS)
20
- }
21
-
22
- /**
23
- * Resolve an app name to its absolute filesystem path.
24
- * Throws if the app name is unknown.
25
- */
26
- export function getAppPath(appName: string): string {
27
- const appPath = APP_PATHS[appName]
28
- if (!appPath) {
29
- throw new Error(
30
- `Unknown app: ${appName}. Available: ${Object.keys(APP_PATHS).join(', ')}`
31
- )
32
- }
33
- return appPath
34
- }
35
-
36
- /**
37
- * Deploy an app to Vercel using the `vercel` CLI.
38
- *
39
- * Uses `execFile` (not `exec`) to avoid shell injection.
40
- * The `--cwd` flag tells Vercel which project directory to deploy.
41
- *
42
- * @param appName - Short name from APP_PATHS (e.g. 'portfolio', 'dashboard-returnpro')
43
- * @param prod - If true, deploys to production (--prod flag). Otherwise preview.
44
- * @returns The deployment URL printed by Vercel CLI.
45
- */
46
- export async function deploy(appName: string, prod = false): Promise<string> {
47
- const appPath = getAppPath(appName)
48
- const args = prod
49
- ? ['--prod', '--cwd', appPath]
50
- : ['--cwd', appPath]
51
- const { stdout } = await run('vercel', args, { timeout: 120_000 })
52
- return stdout.trim()
53
- }
54
-
55
- /**
56
- * Run the Optimal workstation health check script.
57
- *
58
- * Checks: n8n, Affine (Docker + HTTP), Strapi CMS (systemd + HTTP),
59
- * Git repo sync status, Docker containers, and OptimalOS dev server.
60
- *
61
- * @returns The full text output of the health check script.
62
- */
63
- export async function healthCheck(): Promise<string> {
64
- const { stdout } = await run(
65
- 'bash',
66
- ['/home/optimal/scripts/health-check.sh'],
67
- { timeout: 30_000 }
68
- )
69
- return stdout.trim()
70
- }