optimal-cli 0.1.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 (47) hide show
  1. package/agents/.gitkeep +0 -0
  2. package/agents/content-ops.md +227 -0
  3. package/agents/financial-ops.md +184 -0
  4. package/agents/infra-ops.md +206 -0
  5. package/agents/profiles.json +5 -0
  6. package/dist/bin/optimal.d.ts +1 -1
  7. package/dist/bin/optimal.js +706 -111
  8. package/dist/lib/assets/index.d.ts +79 -0
  9. package/dist/lib/assets/index.js +153 -0
  10. package/dist/lib/assets.d.ts +20 -0
  11. package/dist/lib/assets.js +112 -0
  12. package/dist/lib/auth/index.d.ts +83 -0
  13. package/dist/lib/auth/index.js +146 -0
  14. package/dist/lib/board/index.d.ts +39 -0
  15. package/dist/lib/board/index.js +285 -0
  16. package/dist/lib/board/types.d.ts +111 -0
  17. package/dist/lib/board/types.js +1 -0
  18. package/dist/lib/bot/claim.d.ts +3 -0
  19. package/dist/lib/bot/claim.js +20 -0
  20. package/dist/lib/bot/coordinator.d.ts +27 -0
  21. package/dist/lib/bot/coordinator.js +178 -0
  22. package/dist/lib/bot/heartbeat.d.ts +6 -0
  23. package/dist/lib/bot/heartbeat.js +30 -0
  24. package/dist/lib/bot/index.d.ts +9 -0
  25. package/dist/lib/bot/index.js +6 -0
  26. package/dist/lib/bot/protocol.d.ts +12 -0
  27. package/dist/lib/bot/protocol.js +74 -0
  28. package/dist/lib/bot/reporter.d.ts +3 -0
  29. package/dist/lib/bot/reporter.js +27 -0
  30. package/dist/lib/bot/skills.d.ts +26 -0
  31. package/dist/lib/bot/skills.js +69 -0
  32. package/dist/lib/config/registry.d.ts +17 -0
  33. package/dist/lib/config/registry.js +182 -0
  34. package/dist/lib/config/schema.d.ts +31 -0
  35. package/dist/lib/config/schema.js +25 -0
  36. package/dist/lib/errors.d.ts +25 -0
  37. package/dist/lib/errors.js +91 -0
  38. package/dist/lib/format.d.ts +28 -0
  39. package/dist/lib/format.js +98 -0
  40. package/dist/lib/returnpro/validate.d.ts +37 -0
  41. package/dist/lib/returnpro/validate.js +124 -0
  42. package/dist/lib/social/meta.d.ts +90 -0
  43. package/dist/lib/social/meta.js +160 -0
  44. package/docs/CLI-REFERENCE.md +361 -0
  45. package/package.json +13 -24
  46. package/dist/lib/kanban.d.ts +0 -46
  47. package/dist/lib/kanban.js +0 -118
@@ -0,0 +1,12 @@
1
+ export interface AgentMessage {
2
+ type: 'heartbeat' | 'claim' | 'progress' | 'complete' | 'blocked' | 'release';
3
+ agentId: string;
4
+ taskId?: string;
5
+ payload?: Record<string, unknown>;
6
+ }
7
+ export interface AgentResponse {
8
+ success: boolean;
9
+ data?: unknown;
10
+ error?: string;
11
+ }
12
+ export declare function processAgentMessage(msg: AgentMessage): Promise<AgentResponse>;
@@ -0,0 +1,74 @@
1
+ import { sendHeartbeat } from './heartbeat.js';
2
+ import { claimNextTask, releaseTask } from './claim.js';
3
+ import { reportProgress, reportCompletion, reportBlocked } from './reporter.js';
4
+ // --- Message processor ---
5
+ export async function processAgentMessage(msg) {
6
+ try {
7
+ switch (msg.type) {
8
+ case 'heartbeat':
9
+ return await handleHeartbeat(msg);
10
+ case 'claim':
11
+ return await handleClaim(msg);
12
+ case 'progress':
13
+ return await handleProgress(msg);
14
+ case 'complete':
15
+ return await handleComplete(msg);
16
+ case 'blocked':
17
+ return await handleBlocked(msg);
18
+ case 'release':
19
+ return await handleRelease(msg);
20
+ default:
21
+ return { success: false, error: `Unknown message type: ${msg.type}` };
22
+ }
23
+ }
24
+ catch (err) {
25
+ const errorMsg = err instanceof Error ? err.message : String(err);
26
+ return { success: false, error: errorMsg };
27
+ }
28
+ }
29
+ // --- Handlers ---
30
+ async function handleHeartbeat(msg) {
31
+ const status = msg.payload?.status ?? 'idle';
32
+ await sendHeartbeat(msg.agentId, status);
33
+ return { success: true, data: { agentId: msg.agentId, status } };
34
+ }
35
+ async function handleClaim(msg) {
36
+ const skills = msg.payload?.skills;
37
+ const task = await claimNextTask(msg.agentId, skills);
38
+ if (!task) {
39
+ return { success: true, data: null };
40
+ }
41
+ return { success: true, data: { taskId: task.id, title: task.title } };
42
+ }
43
+ async function handleProgress(msg) {
44
+ if (!msg.taskId) {
45
+ return { success: false, error: 'taskId is required for progress messages' };
46
+ }
47
+ const message = msg.payload?.message ?? 'Progress update';
48
+ await reportProgress(msg.taskId, msg.agentId, message);
49
+ return { success: true, data: { taskId: msg.taskId } };
50
+ }
51
+ async function handleComplete(msg) {
52
+ if (!msg.taskId) {
53
+ return { success: false, error: 'taskId is required for complete messages' };
54
+ }
55
+ const summary = msg.payload?.summary ?? 'Task completed';
56
+ await reportCompletion(msg.taskId, msg.agentId, summary);
57
+ return { success: true, data: { taskId: msg.taskId, status: 'done' } };
58
+ }
59
+ async function handleBlocked(msg) {
60
+ if (!msg.taskId) {
61
+ return { success: false, error: 'taskId is required for blocked messages' };
62
+ }
63
+ const reason = msg.payload?.reason ?? 'No reason given';
64
+ await reportBlocked(msg.taskId, msg.agentId, reason);
65
+ return { success: true, data: { taskId: msg.taskId, status: 'blocked' } };
66
+ }
67
+ async function handleRelease(msg) {
68
+ if (!msg.taskId) {
69
+ return { success: false, error: 'taskId is required for release messages' };
70
+ }
71
+ const reason = msg.payload?.reason;
72
+ const task = await releaseTask(msg.taskId, msg.agentId, reason);
73
+ return { success: true, data: { taskId: task.id, status: 'ready' } };
74
+ }
@@ -0,0 +1,3 @@
1
+ export declare function reportProgress(taskId: string, agentId: string, message: string): Promise<void>;
2
+ export declare function reportCompletion(taskId: string, agentId: string, summary: string): Promise<void>;
3
+ export declare function reportBlocked(taskId: string, agentId: string, reason: string): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { addComment, updateTask, completeTask } from '../board/index.js';
2
+ export async function reportProgress(taskId, agentId, message) {
3
+ await addComment({
4
+ task_id: taskId,
5
+ author: agentId,
6
+ body: message,
7
+ comment_type: 'comment',
8
+ });
9
+ }
10
+ export async function reportCompletion(taskId, agentId, summary) {
11
+ await completeTask(taskId, agentId);
12
+ await addComment({
13
+ task_id: taskId,
14
+ author: agentId,
15
+ body: summary,
16
+ comment_type: 'status_change',
17
+ });
18
+ }
19
+ export async function reportBlocked(taskId, agentId, reason) {
20
+ await updateTask(taskId, { status: 'blocked' }, agentId);
21
+ await addComment({
22
+ task_id: taskId,
23
+ author: agentId,
24
+ body: `Blocked: ${reason}`,
25
+ comment_type: 'status_change',
26
+ });
27
+ }
@@ -0,0 +1,26 @@
1
+ import type { Task } from '../board/types.js';
2
+ export interface AgentProfile {
3
+ id: string;
4
+ skills: string[];
5
+ maxConcurrent: number;
6
+ status: 'idle' | 'working' | 'error';
7
+ }
8
+ /**
9
+ * Reads agent profiles from agents/profiles.json.
10
+ * Falls back to a single default wildcard profile if the file doesn't exist.
11
+ */
12
+ export declare function getAgentProfiles(): AgentProfile[];
13
+ /**
14
+ * Filters and ranks tasks an agent can work on.
15
+ *
16
+ * - Includes tasks whose skill_required is in the agent's skills list,
17
+ * or tasks with no skill_required, or if agent has wildcard '*'.
18
+ * - Sorts by priority (P1 first), then sort_order.
19
+ * - Returns at most agent.maxConcurrent tasks.
20
+ */
21
+ export declare function matchTasksToAgent(agent: AgentProfile, tasks: Task[]): Task[];
22
+ /**
23
+ * Finds the first idle agent whose skills match the task's skill_required.
24
+ * Returns null if no suitable idle agent exists.
25
+ */
26
+ export declare function findBestAgent(profiles: AgentProfile[], task: Task): AgentProfile | null;
@@ -0,0 +1,69 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const DEFAULT_PROFILE = {
5
+ id: 'default',
6
+ skills: ['*'],
7
+ maxConcurrent: 1,
8
+ status: 'idle',
9
+ };
10
+ /**
11
+ * Reads agent profiles from agents/profiles.json.
12
+ * Falls back to a single default wildcard profile if the file doesn't exist.
13
+ */
14
+ export function getAgentProfiles() {
15
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
16
+ const profilesPath = resolve(root, 'agents', 'profiles.json');
17
+ if (!existsSync(profilesPath)) {
18
+ return [DEFAULT_PROFILE];
19
+ }
20
+ try {
21
+ const raw = readFileSync(profilesPath, 'utf-8');
22
+ const parsed = JSON.parse(raw);
23
+ return parsed;
24
+ }
25
+ catch {
26
+ return [DEFAULT_PROFILE];
27
+ }
28
+ }
29
+ /**
30
+ * Filters and ranks tasks an agent can work on.
31
+ *
32
+ * - Includes tasks whose skill_required is in the agent's skills list,
33
+ * or tasks with no skill_required, or if agent has wildcard '*'.
34
+ * - Sorts by priority (P1 first), then sort_order.
35
+ * - Returns at most agent.maxConcurrent tasks.
36
+ */
37
+ export function matchTasksToAgent(agent, tasks) {
38
+ const hasWildcard = agent.skills.includes('*');
39
+ const matched = tasks.filter((t) => {
40
+ if (hasWildcard)
41
+ return true;
42
+ if (!t.skill_required)
43
+ return true;
44
+ return agent.skills.includes(t.skill_required);
45
+ });
46
+ matched.sort((a, b) => {
47
+ if (a.priority !== b.priority)
48
+ return a.priority - b.priority;
49
+ return a.sort_order - b.sort_order;
50
+ });
51
+ return matched.slice(0, agent.maxConcurrent);
52
+ }
53
+ /**
54
+ * Finds the first idle agent whose skills match the task's skill_required.
55
+ * Returns null if no suitable idle agent exists.
56
+ */
57
+ export function findBestAgent(profiles, task) {
58
+ for (const agent of profiles) {
59
+ if (agent.status !== 'idle')
60
+ continue;
61
+ if (agent.skills.includes('*'))
62
+ return agent;
63
+ if (!task.skill_required)
64
+ return agent;
65
+ if (agent.skills.includes(task.skill_required))
66
+ return agent;
67
+ }
68
+ return null;
69
+ }
@@ -0,0 +1,17 @@
1
+ import { getSupabase } from '../supabase.js';
2
+ import { type OptimalConfigV1 } from './schema.js';
3
+ export declare function setRegistrySupabaseProviderForTests(provider: typeof getSupabase): void;
4
+ export declare function resetRegistrySupabaseProviderForTests(): void;
5
+ export declare function ensureConfigDir(): Promise<void>;
6
+ export declare function getLocalConfigPath(): string;
7
+ export declare function getHistoryPath(): string;
8
+ export declare function readLocalConfig(): Promise<OptimalConfigV1 | null>;
9
+ export declare function writeLocalConfig(config: OptimalConfigV1): Promise<void>;
10
+ export declare function hashConfig(config: OptimalConfigV1): string;
11
+ export declare function appendHistory(entry: string): Promise<void>;
12
+ export type RegistrySyncResult = {
13
+ ok: boolean;
14
+ message: string;
15
+ };
16
+ export declare function pullRegistryProfile(profile?: string): Promise<RegistrySyncResult>;
17
+ export declare function pushRegistryProfile(profile?: string, force?: boolean, agent?: string): Promise<RegistrySyncResult>;
@@ -0,0 +1,182 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import { getSupabase } from '../supabase.js';
7
+ import { assertOptimalConfigV1 } from './schema.js';
8
+ const DIR = join(homedir(), '.optimal');
9
+ const LOCAL_CONFIG_PATH = join(DIR, 'optimal.config.json');
10
+ const HISTORY_PATH = join(DIR, 'config-history.log');
11
+ const REGISTRY_TABLE = 'cli_config_registry';
12
+ let supabaseProvider = getSupabase;
13
+ function getGlobalProviderOverride() {
14
+ const candidate = globalThis.__optimalRegistrySupabaseProvider;
15
+ return candidate ?? null;
16
+ }
17
+ function getActiveSupabaseProvider() {
18
+ return getGlobalProviderOverride() ?? supabaseProvider;
19
+ }
20
+ export function setRegistrySupabaseProviderForTests(provider) {
21
+ supabaseProvider = provider;
22
+ globalThis.__optimalRegistrySupabaseProvider = provider;
23
+ }
24
+ export function resetRegistrySupabaseProviderForTests() {
25
+ supabaseProvider = getSupabase;
26
+ delete globalThis.__optimalRegistrySupabaseProvider;
27
+ }
28
+ export async function ensureConfigDir() {
29
+ await mkdir(DIR, { recursive: true });
30
+ }
31
+ export function getLocalConfigPath() {
32
+ return LOCAL_CONFIG_PATH;
33
+ }
34
+ export function getHistoryPath() {
35
+ return HISTORY_PATH;
36
+ }
37
+ export async function readLocalConfig() {
38
+ if (!existsSync(LOCAL_CONFIG_PATH))
39
+ return null;
40
+ const raw = await readFile(LOCAL_CONFIG_PATH, 'utf-8');
41
+ const parsed = JSON.parse(raw);
42
+ return assertOptimalConfigV1(parsed);
43
+ }
44
+ export async function writeLocalConfig(config) {
45
+ await ensureConfigDir();
46
+ await writeFile(LOCAL_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
47
+ }
48
+ export function hashConfig(config) {
49
+ const payload = JSON.stringify(config);
50
+ return createHash('sha256').update(payload).digest('hex');
51
+ }
52
+ export async function appendHistory(entry) {
53
+ await ensureConfigDir();
54
+ await writeFile(HISTORY_PATH, `${entry}\n`, { encoding: 'utf-8', flag: 'a' });
55
+ }
56
+ function resolveOwner(local) {
57
+ return local?.profile.owner || process.env.OPTIMAL_CONFIG_OWNER || null;
58
+ }
59
+ function parseEpoch(input) {
60
+ if (!input)
61
+ return 0;
62
+ const ts = Date.parse(input);
63
+ return Number.isNaN(ts) ? 0 : ts;
64
+ }
65
+ export async function pullRegistryProfile(profile = 'default') {
66
+ try {
67
+ const local = await readLocalConfig();
68
+ const owner = resolveOwner(local);
69
+ if (!owner) {
70
+ return {
71
+ ok: false,
72
+ message: 'registry pull failed: missing owner (set local config profile.owner or OPTIMAL_CONFIG_OWNER)',
73
+ };
74
+ }
75
+ const supabase = getActiveSupabaseProvider()('optimal');
76
+ const { data, error } = await supabase
77
+ .from(REGISTRY_TABLE)
78
+ .select('owner,profile,config_version,payload,payload_hash,updated_at')
79
+ .eq('owner', owner)
80
+ .eq('profile', profile)
81
+ .maybeSingle();
82
+ if (error) {
83
+ return { ok: false, message: `registry pull failed: ${error.message}` };
84
+ }
85
+ if (!data) {
86
+ return { ok: false, message: `registry pull failed: no remote profile found for owner=${owner} profile=${profile}` };
87
+ }
88
+ const row = data;
89
+ const payload = assertOptimalConfigV1(row.payload);
90
+ await writeLocalConfig(payload);
91
+ const localHash = local ? hashConfig(local) : null;
92
+ const changed = localHash !== row.payload_hash;
93
+ return {
94
+ ok: true,
95
+ message: changed
96
+ ? `registry pull ok: wrote owner=${owner} profile=${profile} hash=${row.payload_hash.slice(0, 12)}`
97
+ : `registry pull ok: local already matched owner=${owner} profile=${profile}`,
98
+ };
99
+ }
100
+ catch (err) {
101
+ return {
102
+ ok: false,
103
+ message: `registry pull failed: ${err instanceof Error ? err.message : String(err)}`,
104
+ };
105
+ }
106
+ }
107
+ export async function pushRegistryProfile(profile = 'default', force = false, agent) {
108
+ try {
109
+ const local = await readLocalConfig();
110
+ if (!local) {
111
+ return { ok: false, message: `registry push failed: no local config at ${LOCAL_CONFIG_PATH}` };
112
+ }
113
+ let owner = resolveOwner(local);
114
+ if (!owner && agent) {
115
+ owner = agent;
116
+ local.profile.owner = owner;
117
+ }
118
+ if (!owner) {
119
+ return {
120
+ ok: false,
121
+ message: 'registry push failed: missing owner (set local config profile.owner, OPTIMAL_CONFIG_OWNER, or use --agent)',
122
+ };
123
+ }
124
+ const localHash = hashConfig(local);
125
+ const supabase = getActiveSupabaseProvider()('optimal');
126
+ const { data: existing, error: readErr } = await supabase
127
+ .from(REGISTRY_TABLE)
128
+ .select('owner,profile,config_version,payload,payload_hash,updated_at')
129
+ .eq('owner', owner)
130
+ .eq('profile', profile)
131
+ .maybeSingle();
132
+ if (readErr) {
133
+ return { ok: false, message: `registry push failed: ${readErr.message}` };
134
+ }
135
+ if (existing) {
136
+ const row = existing;
137
+ if (row.payload_hash !== localHash && !force) {
138
+ const remotePayload = assertOptimalConfigV1(row.payload);
139
+ const remoteTs = Math.max(parseEpoch(row.updated_at), parseEpoch(remotePayload.profile.updated_at));
140
+ const localTs = parseEpoch(local.profile.updated_at);
141
+ if (remoteTs >= localTs) {
142
+ return {
143
+ ok: false,
144
+ message: `registry push conflict: remote is newer/different for owner=${owner} profile=${profile}. ` +
145
+ 'run `optimal config sync pull` or retry with --force',
146
+ };
147
+ }
148
+ }
149
+ }
150
+ const payload = {
151
+ ...local,
152
+ profile: {
153
+ ...local.profile,
154
+ name: profile,
155
+ owner,
156
+ updated_at: new Date().toISOString(),
157
+ },
158
+ };
159
+ const { error: upsertErr } = await supabase.from(REGISTRY_TABLE).upsert({
160
+ owner,
161
+ profile,
162
+ config_version: payload.version,
163
+ payload,
164
+ payload_hash: hashConfig(payload),
165
+ source: 'optimal-cli',
166
+ updated_by: process.env.USER || 'oracle',
167
+ }, { onConflict: 'owner,profile' });
168
+ if (upsertErr) {
169
+ return { ok: false, message: `registry push failed: ${upsertErr.message}` };
170
+ }
171
+ return {
172
+ ok: true,
173
+ message: `registry push ok: owner=${owner} profile=${profile} hash=${hashConfig(payload).slice(0, 12)} force=${force}`,
174
+ };
175
+ }
176
+ catch (err) {
177
+ return {
178
+ ok: false,
179
+ message: `registry push failed: ${err instanceof Error ? err.message : String(err)}`,
180
+ };
181
+ }
182
+ }
@@ -0,0 +1,31 @@
1
+ export type ConfigSchemaVersion = '1.0.0';
2
+ export interface OptimalConfigV1 {
3
+ version: ConfigSchemaVersion;
4
+ profile: {
5
+ name: string;
6
+ owner: string;
7
+ updated_at: string;
8
+ };
9
+ providers: {
10
+ supabase: {
11
+ project_ref: string;
12
+ url: string;
13
+ anon_key_present: boolean;
14
+ };
15
+ strapi: {
16
+ base_url: string;
17
+ token_present: boolean;
18
+ };
19
+ };
20
+ defaults: {
21
+ brand: string;
22
+ timezone: string;
23
+ };
24
+ features: {
25
+ cms: boolean;
26
+ tasks: boolean;
27
+ deploy: boolean;
28
+ };
29
+ }
30
+ export declare function isOptimalConfigV1(value: unknown): value is OptimalConfigV1;
31
+ export declare function assertOptimalConfigV1(value: unknown): OptimalConfigV1;
@@ -0,0 +1,25 @@
1
+ export function isOptimalConfigV1(value) {
2
+ if (!value || typeof value !== 'object')
3
+ return false;
4
+ const v = value;
5
+ return (v.version === '1.0.0' &&
6
+ typeof v.profile?.name === 'string' &&
7
+ typeof v.profile?.owner === 'string' &&
8
+ typeof v.profile?.updated_at === 'string' &&
9
+ typeof v.providers?.supabase?.project_ref === 'string' &&
10
+ typeof v.providers?.supabase?.url === 'string' &&
11
+ typeof v.providers?.supabase?.anon_key_present === 'boolean' &&
12
+ typeof v.providers?.strapi?.base_url === 'string' &&
13
+ typeof v.providers?.strapi?.token_present === 'boolean' &&
14
+ typeof v.defaults?.brand === 'string' &&
15
+ typeof v.defaults?.timezone === 'string' &&
16
+ typeof v.features?.cms === 'boolean' &&
17
+ typeof v.features?.tasks === 'boolean' &&
18
+ typeof v.features?.deploy === 'boolean');
19
+ }
20
+ export function assertOptimalConfigV1(value) {
21
+ if (!isOptimalConfigV1(value)) {
22
+ throw new Error('Invalid optimal config payload (v1)');
23
+ }
24
+ return value;
25
+ }
@@ -0,0 +1,25 @@
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
+ export type ErrorCode = 'MISSING_ENV' | 'NOT_FOUND' | 'SUPABASE_ERROR' | 'VALIDATION_ERROR' | 'AUTH_ERROR' | 'NETWORK_ERROR' | 'FILE_ERROR' | 'UNKNOWN';
8
+ export declare class CliError extends Error {
9
+ code: ErrorCode;
10
+ suggestion?: string | undefined;
11
+ constructor(message: string, code: ErrorCode, suggestion?: string | undefined);
12
+ }
13
+ /**
14
+ * Format an error for CLI output, print it to stderr, and exit with code 1.
15
+ */
16
+ export declare function handleError(err: unknown): never;
17
+ /**
18
+ * Wrap a Commander action handler so any thrown error is routed through
19
+ * handleError, giving the user a consistent, friendly message instead of
20
+ * an unhandled-rejection stack trace.
21
+ *
22
+ * Usage:
23
+ * .action(wrapCommand(async (opts) => { ... }))
24
+ */
25
+ export declare function wrapCommand<A extends unknown[]>(fn: (...args: A) => Promise<void>): (...args: A) => Promise<void>;
@@ -0,0 +1,91 @@
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
+ // ── CliError ─────────────────────────────────────────────────────────────────
8
+ export class CliError extends Error {
9
+ code;
10
+ suggestion;
11
+ constructor(message, code, suggestion) {
12
+ super(message);
13
+ this.code = code;
14
+ this.suggestion = suggestion;
15
+ this.name = 'CliError';
16
+ }
17
+ }
18
+ // ── Helpers ──────────────────────────────────────────────────────────────────
19
+ const SUGGESTIONS = {
20
+ MISSING_ENV: 'Ensure the required environment variables are set in your .env file or shell.',
21
+ NOT_FOUND: 'Double-check the identifier (slug, ID, or name) and try again.',
22
+ SUPABASE_ERROR: 'Verify your Supabase URL and service key are correct and the database is reachable.',
23
+ VALIDATION_ERROR: 'Review the command options with --help.',
24
+ AUTH_ERROR: 'Check your API token or credentials and make sure they have not expired.',
25
+ NETWORK_ERROR: 'Check your internet connection and verify the remote service is available.',
26
+ FILE_ERROR: 'Verify the file path exists and you have read/write permissions.',
27
+ };
28
+ function classifyError(err) {
29
+ if (err instanceof CliError) {
30
+ return { code: err.code, message: err.message };
31
+ }
32
+ if (err instanceof Error) {
33
+ const msg = err.message;
34
+ // Supabase / fetch errors
35
+ if (msg.includes('PGRST') || msg.includes('supabase') || msg.includes('relation')) {
36
+ return { code: 'SUPABASE_ERROR', message: msg };
37
+ }
38
+ if (msg.includes('ENOENT') || msg.includes('no such file')) {
39
+ return { code: 'FILE_ERROR', message: msg };
40
+ }
41
+ if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed') || msg.includes('ETIMEDOUT')) {
42
+ return { code: 'NETWORK_ERROR', message: msg };
43
+ }
44
+ if (msg.includes('OPTIMAL_SUPABASE_URL') ||
45
+ msg.includes('OPTIMAL_SUPABASE_SERVICE_KEY') ||
46
+ msg.includes('env')) {
47
+ return { code: 'MISSING_ENV', message: msg };
48
+ }
49
+ return { code: 'UNKNOWN', message: msg };
50
+ }
51
+ return { code: 'UNKNOWN', message: String(err) };
52
+ }
53
+ // ── handleError ──────────────────────────────────────────────────────────────
54
+ /**
55
+ * Format an error for CLI output, print it to stderr, and exit with code 1.
56
+ */
57
+ export function handleError(err) {
58
+ const { code, message } = classifyError(err);
59
+ const suggestion = err instanceof CliError && err.suggestion
60
+ ? err.suggestion
61
+ : SUGGESTIONS[code] ?? '';
62
+ const lines = [
63
+ '',
64
+ ` Error [${code}]: ${message}`,
65
+ ];
66
+ if (suggestion) {
67
+ lines.push(` Suggestion: ${suggestion}`);
68
+ }
69
+ lines.push('');
70
+ process.stderr.write(lines.join('\n'));
71
+ process.exit(1);
72
+ }
73
+ // ── wrapCommand ──────────────────────────────────────────────────────────────
74
+ /**
75
+ * Wrap a Commander action handler so any thrown error is routed through
76
+ * handleError, giving the user a consistent, friendly message instead of
77
+ * an unhandled-rejection stack trace.
78
+ *
79
+ * Usage:
80
+ * .action(wrapCommand(async (opts) => { ... }))
81
+ */
82
+ export function wrapCommand(fn) {
83
+ return async (...args) => {
84
+ try {
85
+ await fn(...args);
86
+ }
87
+ catch (err) {
88
+ handleError(err);
89
+ }
90
+ };
91
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Lightweight CLI output formatting — ANSI colors, tables, badges.
3
+ * Zero external dependencies. Respects NO_COLOR env var.
4
+ */
5
+ type Color = 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'gray' | 'bold' | 'dim';
6
+ /**
7
+ * Wrap text in ANSI escape codes for the given color/style.
8
+ * Returns plain text when NO_COLOR env var is set.
9
+ */
10
+ export declare function colorize(text: string, color: Color): string;
11
+ /**
12
+ * Render a bordered ASCII table with auto-sized columns.
13
+ * Headers are rendered in bold.
14
+ */
15
+ export declare function table(headers: string[], rows: string[][]): string;
16
+ /** Return a colored status string (e.g. "done" in green). */
17
+ export declare function statusBadge(status: string): string;
18
+ /** Return a colored priority label (e.g. "P1" in red). */
19
+ export declare function priorityBadge(p: number): string;
20
+ /** Print a green success message with a check mark prefix. */
21
+ export declare function success(msg: string): void;
22
+ /** Print a red error message with an X prefix. */
23
+ export declare function error(msg: string): void;
24
+ /** Print a yellow warning message with a warning prefix. */
25
+ export declare function warn(msg: string): void;
26
+ /** Print a blue info message with an info prefix. */
27
+ export declare function info(msg: string): void;
28
+ export {};