checkbox-cli 1.1.0 → 2.0.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.
@@ -1,25 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Checkbox CLI
3
+ * Checkbox CLI — Beautiful, interactive compliance management from your terminal.
4
4
  *
5
- * Command-line interface for the Checkbox capability API.
5
+ * First-time users: just run `checkbox` and follow the guided setup.
6
+ * Returning users: `checkbox` opens the interactive menu.
6
7
  *
7
- * Usage:
8
- * checkbox capabilities list all capabilities
9
- * checkbox command <type> <payload-json> [--org <id>] execute a command
10
- * checkbox query <type> <payload-json> --org <id> execute a query
11
- * checkbox workspace --org <id> get workspace graph
12
- * checkbox entities --org <id> [--type <t>] [--search <s>] — find entities
13
- * checkbox schema --org <id> [--type <t>] — get entity schemas
14
- * checkbox permissions --org <id> — check permissions
15
- *
16
- * Environment variables:
17
- * CHECKBOX_API_URL — Base URL (default: https://checkbox.my)
18
- * CHECKBOX_API_TOKEN — API key (ck_live_...) or Supabase JWT
19
- *
20
- * Get your API key:
21
- * 1. Log in to checkbox.my
22
- * 2. Go to Developer Hub (sidebar)
23
- * 3. Create an API key — it never expires unless you set an expiry
8
+ * Direct commands (for scripts & automation):
9
+ * checkbox plans List your compliance plans
10
+ * checkbox tasks Browse tasks
11
+ * checkbox complete <id> Complete a task
12
+ * checkbox status Workspace overview
13
+ * checkbox setup — Re-run the setup wizard
24
14
  */
25
15
  export {};
package/dist/checkbox.js CHANGED
@@ -1,207 +1,855 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Checkbox CLI
3
+ * Checkbox CLI — Beautiful, interactive compliance management from your terminal.
4
4
  *
5
- * Command-line interface for the Checkbox capability API.
5
+ * First-time users: just run `checkbox` and follow the guided setup.
6
+ * Returning users: `checkbox` opens the interactive menu.
6
7
  *
7
- * Usage:
8
- * checkbox capabilities list all capabilities
9
- * checkbox command <type> <payload-json> [--org <id>] execute a command
10
- * checkbox query <type> <payload-json> --org <id> execute a query
11
- * checkbox workspace --org <id> get workspace graph
12
- * checkbox entities --org <id> [--type <t>] [--search <s>] — find entities
13
- * checkbox schema --org <id> [--type <t>] — get entity schemas
14
- * checkbox permissions --org <id> — check permissions
15
- *
16
- * Environment variables:
17
- * CHECKBOX_API_URL — Base URL (default: https://checkbox.my)
18
- * CHECKBOX_API_TOKEN — API key (ck_live_...) or Supabase JWT
19
- *
20
- * Get your API key:
21
- * 1. Log in to checkbox.my
22
- * 2. Go to Developer Hub (sidebar)
23
- * 3. Create an API key — it never expires unless you set an expiry
8
+ * Direct commands (for scripts & automation):
9
+ * checkbox plans List your compliance plans
10
+ * checkbox tasks Browse tasks
11
+ * checkbox complete <id> Complete a task
12
+ * checkbox status Workspace overview
13
+ * checkbox setup — Re-run the setup wizard
24
14
  */
25
- const API_URL = process.env.CHECKBOX_API_URL ?? 'https://checkbox.my';
26
- const API_TOKEN = process.env.CHECKBOX_API_TOKEN ?? '';
27
- function getHeaders() {
28
- return {
15
+ import * as p from '@clack/prompts';
16
+ import pc from 'picocolors';
17
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
18
+ import { homedir } from 'os';
19
+ import { join } from 'path';
20
+ // ── Config ──────────────────────────────────────────────────────────
21
+ const CONFIG_DIR = join(homedir(), '.checkbox');
22
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
23
+ const VERSION = '2.0.0';
24
+ function loadConfig() {
25
+ try {
26
+ if (!existsSync(CONFIG_FILE))
27
+ return null;
28
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
29
+ const cfg = JSON.parse(raw);
30
+ if (!cfg.apiKey)
31
+ return null;
32
+ return {
33
+ apiKey: cfg.apiKey,
34
+ apiUrl: cfg.apiUrl || 'https://checkbox.my',
35
+ orgId: cfg.orgId || '',
36
+ orgName: cfg.orgName || '',
37
+ };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function saveConfig(cfg) {
44
+ mkdirSync(CONFIG_DIR, { recursive: true });
45
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
46
+ }
47
+ // Allow env vars to override config file
48
+ function resolveConfig() {
49
+ const envKey = process.env.CHECKBOX_API_KEY || process.env.CHECKBOX_API_TOKEN;
50
+ const fileCfg = loadConfig();
51
+ if (envKey) {
52
+ return {
53
+ apiKey: envKey,
54
+ apiUrl: process.env.CHECKBOX_API_URL || fileCfg?.apiUrl || 'https://checkbox.my',
55
+ orgId: fileCfg?.orgId || '',
56
+ orgName: fileCfg?.orgName || '',
57
+ };
58
+ }
59
+ return fileCfg;
60
+ }
61
+ // ── API ─────────────────────────────────────────────────────────────
62
+ function api(cfg) {
63
+ const headers = {
29
64
  'Content-Type': 'application/json',
30
- ...(API_TOKEN ? { Authorization: `Bearer ${API_TOKEN}` } : {}),
65
+ 'X-API-Key': cfg.apiKey,
31
66
  };
67
+ async function post(path, body) {
68
+ const res = await fetch(`${cfg.apiUrl}${path}`, {
69
+ method: 'POST',
70
+ headers,
71
+ body: JSON.stringify(body),
72
+ });
73
+ if (!res.ok) {
74
+ const err = await res.json().catch(() => ({}));
75
+ throw new Error(err.error || `Request failed (${res.status})`);
76
+ }
77
+ return res.json();
78
+ }
79
+ async function get(path) {
80
+ const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
81
+ if (!res.ok) {
82
+ const err = await res.json().catch(() => ({}));
83
+ throw new Error(err.error || `Request failed (${res.status})`);
84
+ }
85
+ return res.json();
86
+ }
87
+ return { post, get };
88
+ }
89
+ // ── Formatters ──────────────────────────────────────────────────────
90
+ function truncate(s, max) {
91
+ return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
32
92
  }
33
- async function apiGet(path) {
34
- const res = await fetch(`${API_URL}${path}`, { method: 'GET', headers: getHeaders() });
35
- return res.json();
93
+ function formatDate(iso) {
94
+ return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
36
95
  }
37
- async function apiPost(path, body) {
38
- const res = await fetch(`${API_URL}${path}`, {
39
- method: 'POST',
40
- headers: getHeaders(),
41
- body: JSON.stringify(body),
96
+ function pad(s, n) {
97
+ return s.padEnd(n);
98
+ }
99
+ // ── Setup Wizard ────────────────────────────────────────────────────
100
+ async function runSetup(existingConfig) {
101
+ p.intro(pc.bgCyan(pc.black(' Checkbox Setup ')));
102
+ if (existingConfig?.apiKey) {
103
+ p.log.info(`Current API key: ${pc.dim(existingConfig.apiKey.slice(0, 16) + '...')}`);
104
+ p.log.info(`Current workspace: ${pc.cyan(existingConfig.orgName || 'not set')}`);
105
+ }
106
+ const result = await p.group({
107
+ apiKey: () => p.password({
108
+ message: 'Paste your API key',
109
+ validate: (v) => {
110
+ if (!v)
111
+ return 'API key is required';
112
+ if (!v.startsWith('ck_live_'))
113
+ return 'API key must start with ck_live_';
114
+ if (v.length < 30)
115
+ return 'That looks too short for an API key';
116
+ },
117
+ }),
118
+ confirm: ({ results }) => p.confirm({
119
+ message: `Connect with key ${pc.dim(String(results.apiKey).slice(0, 16) + '...')}?`,
120
+ initialValue: true,
121
+ }),
122
+ }, {
123
+ onCancel: () => {
124
+ p.cancel('Setup cancelled.');
125
+ process.exit(0);
126
+ },
42
127
  });
43
- return res.json();
128
+ if (!result.confirm) {
129
+ p.cancel('Setup cancelled.');
130
+ process.exit(0);
131
+ }
132
+ const cfg = {
133
+ apiKey: result.apiKey,
134
+ apiUrl: existingConfig?.apiUrl || 'https://checkbox.my',
135
+ orgId: '',
136
+ orgName: '',
137
+ };
138
+ // Test the key & fetch organizations
139
+ const s = p.spinner();
140
+ s.start('Connecting to Checkbox...');
141
+ let orgs = [];
142
+ try {
143
+ const { post } = api(cfg);
144
+ const res = await post('/api/v2/query', { type: 'list_my_organizations', payload: {} });
145
+ if (!res.success || !res.data)
146
+ throw new Error(res.error?.message || 'Failed to fetch organizations');
147
+ orgs = res.data;
148
+ s.stop(`Connected! Found ${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
149
+ }
150
+ catch (err) {
151
+ s.stop(pc.red('Connection failed'));
152
+ p.log.error(err.message || 'Could not connect with that API key');
153
+ p.log.info(`Make sure your key is active at ${pc.underline('checkbox.my/developers')}`);
154
+ p.outro(pc.dim('Try again with a valid key.'));
155
+ process.exit(1);
156
+ }
157
+ // Pick workspace
158
+ if (orgs.length === 0) {
159
+ p.log.warn('No workspaces found. Create one at checkbox.my first.');
160
+ saveConfig(cfg);
161
+ p.outro(pc.green('API key saved. Run checkbox again after creating a workspace.'));
162
+ return cfg;
163
+ }
164
+ if (orgs.length === 1) {
165
+ cfg.orgId = orgs[0].id;
166
+ cfg.orgName = orgs[0].name;
167
+ p.log.success(`Workspace: ${pc.cyan(cfg.orgName)}`);
168
+ }
169
+ else {
170
+ const choice = await p.select({
171
+ message: 'Choose your default workspace',
172
+ options: orgs.map((o) => ({
173
+ value: o.id,
174
+ label: o.name,
175
+ hint: o.role,
176
+ })),
177
+ });
178
+ if (p.isCancel(choice)) {
179
+ p.cancel('Setup cancelled.');
180
+ process.exit(0);
181
+ }
182
+ cfg.orgId = choice;
183
+ cfg.orgName = orgs.find((o) => o.id === choice)?.name || '';
184
+ }
185
+ saveConfig(cfg);
186
+ p.note([
187
+ `${pad('Workspace', 14)} ${pc.cyan(cfg.orgName)}`,
188
+ `${pad('API Key', 14)} ${pc.dim(cfg.apiKey.slice(0, 16) + '...')}`,
189
+ `${pad('Config saved', 14)} ${pc.dim(CONFIG_FILE)}`,
190
+ ].join('\n'), 'Ready to go');
191
+ p.outro(pc.green('Setup complete! Run `checkbox` to get started.'));
192
+ return cfg;
44
193
  }
45
- function print(data) {
46
- console.log(JSON.stringify(data, null, 2));
194
+ // ── Interactive Menu ────────────────────────────────────────────────
195
+ async function interactiveMenu(cfg) {
196
+ const { post, get } = api(cfg);
197
+ console.log();
198
+ p.intro(pc.bgCyan(pc.black(' Checkbox ')) +
199
+ pc.dim(` v${VERSION}`) +
200
+ pc.dim(' \u2022 ') +
201
+ pc.cyan(cfg.orgName || 'no workspace'));
202
+ const action = await p.select({
203
+ message: 'What would you like to do?',
204
+ options: [
205
+ { value: 'plans', label: 'View plans', hint: 'see your compliance plans' },
206
+ { value: 'tasks', label: 'Browse tasks', hint: 'view and complete tasks' },
207
+ { value: 'status', label: 'Workspace overview', hint: 'summary of your workspace' },
208
+ { value: 'search', label: 'Search', hint: 'find anything in your workspace' },
209
+ { value: 'complete', label: 'Complete a task', hint: 'mark a task as done' },
210
+ { value: 'create-task', label: 'Create a task', hint: 'add a new task' },
211
+ { value: 'switch', label: 'Switch workspace', hint: 'change organization' },
212
+ { value: 'setup', label: 'Settings', hint: 'API key, workspace config' },
213
+ ],
214
+ });
215
+ if (p.isCancel(action)) {
216
+ p.cancel('Goodbye!');
217
+ return;
218
+ }
219
+ switch (action) {
220
+ case 'plans':
221
+ return menuPlans(cfg, post);
222
+ case 'tasks':
223
+ return menuTasks(cfg, post);
224
+ case 'status':
225
+ return menuStatus(cfg, post);
226
+ case 'search':
227
+ return menuSearch(cfg, post);
228
+ case 'complete':
229
+ return menuComplete(cfg, post);
230
+ case 'create-task':
231
+ return menuCreateTask(cfg, post);
232
+ case 'switch':
233
+ return menuSwitch(cfg, post);
234
+ case 'setup':
235
+ await runSetup(cfg);
236
+ return;
237
+ }
47
238
  }
48
- function fail(message) {
49
- console.error(`Error: ${message}`);
50
- process.exit(1);
239
+ // ── Plans ───────────────────────────────────────────────────────────
240
+ async function menuPlans(cfg, post) {
241
+ const s = p.spinner();
242
+ s.start('Loading plans...');
243
+ try {
244
+ const res = await post('/api/v2/query', {
245
+ type: 'list_plans',
246
+ organization_id: cfg.orgId,
247
+ payload: { organization_id: cfg.orgId },
248
+ });
249
+ if (!res.success)
250
+ throw new Error(res.error?.message || 'Failed');
251
+ const plans = res.data || [];
252
+ s.stop(`Found ${plans.length} plan${plans.length === 1 ? '' : 's'}`);
253
+ if (plans.length === 0) {
254
+ p.log.info('No plans yet. Create one at checkbox.my');
255
+ p.outro('');
256
+ return;
257
+ }
258
+ const lines = plans.map((plan) => {
259
+ const name = truncate(plan.name || 'Untitled', 40);
260
+ const created = plan.created_at ? formatDate(plan.created_at) : '';
261
+ return `${pc.cyan(pad(name, 42))} ${pc.dim(created)}`;
262
+ });
263
+ p.note(lines.join('\n'), `Plans in ${cfg.orgName}`);
264
+ // Offer drill-down
265
+ const selected = await p.select({
266
+ message: 'Select a plan to view its tasks',
267
+ options: [
268
+ ...plans.map((plan) => ({
269
+ value: plan.id,
270
+ label: plan.name || 'Untitled',
271
+ })),
272
+ { value: '__back', label: pc.dim('Back to menu') },
273
+ ],
274
+ });
275
+ if (p.isCancel(selected) || selected === '__back') {
276
+ return interactiveMenu(cfg);
277
+ }
278
+ return menuPlanTasks(cfg, post, selected, plans.find((p) => p.id === selected)?.name || '');
279
+ }
280
+ catch (err) {
281
+ s.stop(pc.red('Failed'));
282
+ p.log.error(err.message);
283
+ }
51
284
  }
52
- function getFlag(args, flag) {
53
- const idx = args.indexOf(flag);
54
- if (idx === -1 || idx + 1 >= args.length)
55
- return undefined;
56
- return args[idx + 1];
285
+ async function menuPlanTasks(cfg, post, planId, planName) {
286
+ const s = p.spinner();
287
+ s.start('Loading tasks...');
288
+ try {
289
+ const res = await post('/api/v2/query', {
290
+ type: 'list_tasks',
291
+ organization_id: cfg.orgId,
292
+ payload: { organization_id: cfg.orgId, plan_id: planId },
293
+ });
294
+ if (!res.success)
295
+ throw new Error(res.error?.message || 'Failed');
296
+ const tasks = res.data || [];
297
+ s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} in ${pc.cyan(planName)}`);
298
+ if (tasks.length === 0) {
299
+ p.log.info('No tasks in this plan.');
300
+ return interactiveMenu(cfg);
301
+ }
302
+ const lines = tasks.map((t) => {
303
+ const name = truncate(t.name || 'Untitled', 50);
304
+ const freq = t.frequency || '';
305
+ return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)}`;
306
+ });
307
+ p.note(lines.join('\n'), planName);
308
+ return interactiveMenu(cfg);
309
+ }
310
+ catch (err) {
311
+ s.stop(pc.red('Failed'));
312
+ p.log.error(err.message);
313
+ return interactiveMenu(cfg);
314
+ }
315
+ }
316
+ // ── Tasks ───────────────────────────────────────────────────────────
317
+ async function menuTasks(cfg, post) {
318
+ const s = p.spinner();
319
+ s.start('Loading tasks...');
320
+ try {
321
+ const res = await post('/api/v2/query', {
322
+ type: 'list_tasks',
323
+ organization_id: cfg.orgId,
324
+ payload: { organization_id: cfg.orgId },
325
+ });
326
+ if (!res.success)
327
+ throw new Error(res.error?.message || 'Failed');
328
+ const tasks = res.data || [];
329
+ s.stop(`Found ${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
330
+ if (tasks.length === 0) {
331
+ p.log.info('No tasks yet.');
332
+ return interactiveMenu(cfg);
333
+ }
334
+ const selected = await p.select({
335
+ message: 'Select a task',
336
+ options: [
337
+ ...tasks.slice(0, 25).map((t) => ({
338
+ value: t.id,
339
+ label: truncate(t.name || 'Untitled', 60),
340
+ hint: t.frequency || '',
341
+ })),
342
+ ...(tasks.length > 25 ? [{ value: '__more', label: pc.dim(`...and ${tasks.length - 25} more`) }] : []),
343
+ { value: '__back', label: pc.dim('Back to menu') },
344
+ ],
345
+ });
346
+ if (p.isCancel(selected) || selected === '__back' || selected === '__more') {
347
+ return interactiveMenu(cfg);
348
+ }
349
+ // Show task detail
350
+ const task = tasks.find((t) => t.id === selected);
351
+ if (task) {
352
+ const lines = [
353
+ `${pad('Name', 14)} ${pc.cyan(task.name || 'Untitled')}`,
354
+ `${pad('Frequency', 14)} ${task.frequency || pc.dim('none')}`,
355
+ `${pad('Plan', 14)} ${task.plan_name || task.plan_id || pc.dim('none')}`,
356
+ `${pad('Assigned to', 14)} ${task.assigned_to || pc.dim('unassigned')}`,
357
+ `${pad('ID', 14)} ${pc.dim(task.id)}`,
358
+ ];
359
+ p.note(lines.join('\n'), task.name || 'Task');
360
+ const action = await p.select({
361
+ message: 'What would you like to do?',
362
+ options: [
363
+ { value: 'complete', label: 'Complete this task' },
364
+ { value: 'back', label: pc.dim('Back to menu') },
365
+ ],
366
+ });
367
+ if (action === 'complete' && !p.isCancel(action)) {
368
+ await doComplete(cfg, post, task.id, task.name);
369
+ }
370
+ }
371
+ return interactiveMenu(cfg);
372
+ }
373
+ catch (err) {
374
+ s.stop(pc.red('Failed'));
375
+ p.log.error(err.message);
376
+ return interactiveMenu(cfg);
377
+ }
378
+ }
379
+ // ── Status ──────────────────────────────────────────────────────────
380
+ async function menuStatus(cfg, post) {
381
+ const s = p.spinner();
382
+ s.start('Loading workspace...');
383
+ try {
384
+ const res = await post('/api/v2/introspection/workspace', {
385
+ organization_id: cfg.orgId,
386
+ });
387
+ if (!res.success)
388
+ throw new Error(res.error?.message || 'Failed');
389
+ const ws = res.data || {};
390
+ s.stop('Workspace loaded');
391
+ const counts = ws.entity_counts || {};
392
+ const lines = [
393
+ `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
394
+ '',
395
+ `${pad('Plans', 16)} ${pc.bold(String(counts.plans || 0))}`,
396
+ `${pad('Tasks', 16)} ${pc.bold(String(counts.tasks || 0))}`,
397
+ `${pad('Requirements', 16)} ${pc.bold(String(counts.requirements || 0))}`,
398
+ `${pad('Goals', 16)} ${pc.bold(String(counts.goals || 0))}`,
399
+ `${pad('Documents', 16)} ${pc.bold(String(counts.documents || 0))}`,
400
+ `${pad('Tables', 16)} ${pc.bold(String(counts.tables || 0))}`,
401
+ `${pad('Members', 16)} ${pc.bold(String(counts.members || counts.users || 0))}`,
402
+ ];
403
+ p.note(lines.join('\n'), 'Workspace Overview');
404
+ }
405
+ catch (err) {
406
+ s.stop(pc.red('Failed'));
407
+ p.log.error(err.message);
408
+ }
409
+ return interactiveMenu(cfg);
410
+ }
411
+ // ── Search ──────────────────────────────────────────────────────────
412
+ async function menuSearch(cfg, post) {
413
+ const query = await p.text({
414
+ message: 'Search for...',
415
+ placeholder: 'task name, plan, document...',
416
+ validate: (v) => {
417
+ if (!v)
418
+ return 'Enter a search term';
419
+ },
420
+ });
421
+ if (p.isCancel(query))
422
+ return interactiveMenu(cfg);
423
+ const s = p.spinner();
424
+ s.start('Searching...');
425
+ try {
426
+ const res = await post('/api/v2/introspection/entities', {
427
+ organization_id: cfg.orgId,
428
+ search: query,
429
+ limit: 20,
430
+ });
431
+ if (!res.success)
432
+ throw new Error(res.error?.message || 'Failed');
433
+ const entities = res.data || [];
434
+ s.stop(`Found ${entities.length} result${entities.length === 1 ? '' : 's'}`);
435
+ if (entities.length === 0) {
436
+ p.log.info('No results found.');
437
+ }
438
+ else {
439
+ const lines = entities.map((e) => {
440
+ const type = pad(e.type || 'unknown', 14);
441
+ const name = truncate(e.name || 'Untitled', 50);
442
+ return ` ${pc.dim(type)} ${pc.cyan(name)}`;
443
+ });
444
+ p.note(lines.join('\n'), `Results for "${query}"`);
445
+ }
446
+ }
447
+ catch (err) {
448
+ s.stop(pc.red('Failed'));
449
+ p.log.error(err.message);
450
+ }
451
+ return interactiveMenu(cfg);
57
452
  }
58
- // ── Subcommands ─────────────────────────────────────────────────────
59
- async function cmdCapabilities() {
60
- print(await apiGet('/api/v2/introspection/capabilities'));
61
- }
62
- async function cmdCommand(args) {
63
- const type = args[0];
64
- const payloadRaw = args[1];
65
- if (!type)
66
- fail('Usage: checkbox command <type> <payload-json> [--org <id>]');
67
- const payload = payloadRaw ? JSON.parse(payloadRaw) : {};
68
- const orgId = getFlag(args, '--org');
453
+ // ── Complete Task ───────────────────────────────────────────────────
454
+ async function menuComplete(cfg, post) {
455
+ const s = p.spinner();
456
+ s.start('Loading tasks...');
457
+ try {
458
+ const res = await post('/api/v2/query', {
459
+ type: 'list_tasks',
460
+ organization_id: cfg.orgId,
461
+ payload: { organization_id: cfg.orgId },
462
+ });
463
+ if (!res.success)
464
+ throw new Error(res.error?.message || 'Failed');
465
+ const tasks = res.data || [];
466
+ s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} available`);
467
+ if (tasks.length === 0) {
468
+ p.log.info('No tasks to complete.');
469
+ return interactiveMenu(cfg);
470
+ }
471
+ const selected = await p.select({
472
+ message: 'Which task did you complete?',
473
+ options: [
474
+ ...tasks.slice(0, 30).map((t) => ({
475
+ value: t.id,
476
+ label: truncate(t.name || 'Untitled', 60),
477
+ hint: t.frequency || '',
478
+ })),
479
+ { value: '__back', label: pc.dim('Back to menu') },
480
+ ],
481
+ });
482
+ if (p.isCancel(selected) || selected === '__back') {
483
+ return interactiveMenu(cfg);
484
+ }
485
+ const task = tasks.find((t) => t.id === selected);
486
+ await doComplete(cfg, post, selected, task?.name || '');
487
+ }
488
+ catch (err) {
489
+ s.stop(pc.red('Failed'));
490
+ p.log.error(err.message);
491
+ }
492
+ return interactiveMenu(cfg);
493
+ }
494
+ async function doComplete(cfg, post, taskId, taskName) {
495
+ const s = p.spinner();
496
+ s.start('Completing task...');
497
+ try {
498
+ const res = await post('/api/v2/commands', {
499
+ type: 'complete_task',
500
+ organization_id: cfg.orgId,
501
+ payload: { task_id: taskId },
502
+ });
503
+ if (!res.success)
504
+ throw new Error(res.error?.message || 'Failed');
505
+ s.stop(pc.green(`Done! "${taskName}" completed`));
506
+ }
507
+ catch (err) {
508
+ s.stop(pc.red('Failed'));
509
+ p.log.error(err.message);
510
+ }
511
+ }
512
+ // ── Create Task ─────────────────────────────────────────────────────
513
+ async function menuCreateTask(cfg, post) {
514
+ // First, get plans to attach task to
515
+ const s = p.spinner();
516
+ s.start('Loading plans...');
517
+ let plans = [];
518
+ try {
519
+ const res = await post('/api/v2/query', {
520
+ type: 'list_plans',
521
+ organization_id: cfg.orgId,
522
+ payload: { organization_id: cfg.orgId },
523
+ });
524
+ plans = res.data || [];
525
+ s.stop('');
526
+ }
527
+ catch {
528
+ s.stop('');
529
+ }
530
+ const result = await p.group({
531
+ name: () => p.text({
532
+ message: 'Task name',
533
+ placeholder: 'e.g. Daily safety walkthrough',
534
+ validate: (v) => {
535
+ if (!v)
536
+ return 'Task name is required';
537
+ },
538
+ }),
539
+ planId: () => {
540
+ if (plans.length === 0)
541
+ return Promise.resolve(undefined);
542
+ return p.select({
543
+ message: 'Add to which plan?',
544
+ options: [
545
+ { value: undefined, label: pc.dim('No plan (standalone task)') },
546
+ ...plans.map((plan) => ({
547
+ value: plan.id,
548
+ label: plan.name || 'Untitled',
549
+ })),
550
+ ],
551
+ });
552
+ },
553
+ frequency: () => p.select({
554
+ message: 'How often?',
555
+ options: [
556
+ { value: 'daily', label: 'Daily' },
557
+ { value: 'weekly', label: 'Weekly' },
558
+ { value: 'monthly', label: 'Monthly' },
559
+ { value: 'one-time', label: 'One-time' },
560
+ ],
561
+ }),
562
+ }, {
563
+ onCancel: () => {
564
+ p.cancel('Cancelled.');
565
+ return;
566
+ },
567
+ });
568
+ if (!result.name)
569
+ return interactiveMenu(cfg);
570
+ const spinner = p.spinner();
571
+ spinner.start('Creating task...');
572
+ try {
573
+ const payload = {
574
+ name: result.name,
575
+ organization_id: cfg.orgId,
576
+ frequency: result.frequency || 'daily',
577
+ };
578
+ if (result.planId)
579
+ payload.plan_id = result.planId;
580
+ const res = await post('/api/v2/commands', {
581
+ type: 'create_task',
582
+ organization_id: cfg.orgId,
583
+ payload,
584
+ });
585
+ if (!res.success)
586
+ throw new Error(res.error?.message || 'Failed');
587
+ spinner.stop(pc.green(`Task "${result.name}" created!`));
588
+ }
589
+ catch (err) {
590
+ spinner.stop(pc.red('Failed'));
591
+ p.log.error(err.message);
592
+ }
593
+ return interactiveMenu(cfg);
594
+ }
595
+ // ── Switch Workspace ────────────────────────────────────────────────
596
+ async function menuSwitch(cfg, post) {
597
+ const s = p.spinner();
598
+ s.start('Loading workspaces...');
599
+ try {
600
+ const res = await post('/api/v2/query', {
601
+ type: 'list_my_organizations',
602
+ payload: {},
603
+ });
604
+ if (!res.success)
605
+ throw new Error(res.error?.message || 'Failed');
606
+ const orgs = res.data || [];
607
+ s.stop(`${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
608
+ const selected = await p.select({
609
+ message: 'Switch to workspace',
610
+ options: orgs.map((o) => ({
611
+ value: o.id,
612
+ label: o.name,
613
+ hint: o.id === cfg.orgId ? pc.green('current') : o.role,
614
+ })),
615
+ });
616
+ if (p.isCancel(selected))
617
+ return interactiveMenu(cfg);
618
+ const org = orgs.find((o) => o.id === selected);
619
+ cfg.orgId = selected;
620
+ cfg.orgName = org?.name || '';
621
+ saveConfig(cfg);
622
+ p.log.success(`Switched to ${pc.cyan(cfg.orgName)}`);
623
+ }
624
+ catch (err) {
625
+ s.stop(pc.red('Failed'));
626
+ p.log.error(err.message);
627
+ }
628
+ return interactiveMenu(cfg);
629
+ }
630
+ // ── Direct Commands (non-interactive, for scripts) ──────────────────
631
+ async function directPlans(cfg) {
632
+ const { post } = api(cfg);
633
+ const s = p.spinner();
634
+ s.start('Loading plans...');
635
+ const res = await post('/api/v2/query', {
636
+ type: 'list_plans',
637
+ organization_id: cfg.orgId,
638
+ payload: { organization_id: cfg.orgId },
639
+ });
640
+ if (!res.success)
641
+ throw new Error(res.error?.message || 'Failed');
642
+ s.stop('');
643
+ const plans = res.data || [];
644
+ if (plans.length === 0) {
645
+ p.log.info('No plans found.');
646
+ return;
647
+ }
648
+ const lines = plans.map((plan) => {
649
+ const name = truncate(plan.name || 'Untitled', 40);
650
+ const id = plan.id;
651
+ return ` ${pc.cyan(pad(name, 42))} ${pc.dim(id)}`;
652
+ });
653
+ p.note(lines.join('\n'), `${plans.length} plan${plans.length === 1 ? '' : 's'}`);
654
+ }
655
+ async function directTasks(cfg, planId) {
656
+ const { post } = api(cfg);
657
+ const s = p.spinner();
658
+ s.start('Loading tasks...');
659
+ const payload = { organization_id: cfg.orgId };
660
+ if (planId)
661
+ payload.plan_id = planId;
662
+ const res = await post('/api/v2/query', {
663
+ type: 'list_tasks',
664
+ organization_id: cfg.orgId,
665
+ payload,
666
+ });
667
+ if (!res.success)
668
+ throw new Error(res.error?.message || 'Failed');
669
+ s.stop('');
670
+ const tasks = res.data || [];
671
+ if (tasks.length === 0) {
672
+ p.log.info('No tasks found.');
673
+ return;
674
+ }
675
+ const lines = tasks.map((t) => {
676
+ const name = truncate(t.name || 'Untitled', 50);
677
+ const freq = t.frequency || '';
678
+ return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)} ${pc.dim(t.id)}`;
679
+ });
680
+ p.note(lines.join('\n'), `${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
681
+ }
682
+ async function directComplete(cfg, taskId) {
683
+ const { post } = api(cfg);
684
+ const s = p.spinner();
685
+ s.start('Completing task...');
686
+ const res = await post('/api/v2/commands', {
687
+ type: 'complete_task',
688
+ organization_id: cfg.orgId,
689
+ payload: { task_id: taskId },
690
+ });
691
+ if (!res.success)
692
+ throw new Error(res.error?.message || 'Failed');
693
+ s.stop(pc.green('Task completed!'));
694
+ }
695
+ async function directStatus(cfg) {
696
+ const { post } = api(cfg);
697
+ const s = p.spinner();
698
+ s.start('Loading workspace...');
699
+ const res = await post('/api/v2/introspection/workspace', {
700
+ organization_id: cfg.orgId,
701
+ });
702
+ if (!res.success)
703
+ throw new Error(res.error?.message || 'Failed');
704
+ s.stop('');
705
+ const counts = res.data?.entity_counts || {};
706
+ const lines = [
707
+ `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
708
+ '',
709
+ `${pad('Plans', 16)} ${pc.bold(String(counts.plans || 0))}`,
710
+ `${pad('Tasks', 16)} ${pc.bold(String(counts.tasks || 0))}`,
711
+ `${pad('Requirements', 16)} ${pc.bold(String(counts.requirements || 0))}`,
712
+ `${pad('Goals', 16)} ${pc.bold(String(counts.goals || 0))}`,
713
+ `${pad('Documents', 16)} ${pc.bold(String(counts.documents || 0))}`,
714
+ `${pad('Tables', 16)} ${pc.bold(String(counts.tables || 0))}`,
715
+ ];
716
+ p.note(lines.join('\n'), 'Workspace Overview');
717
+ }
718
+ // Raw command passthrough for power users and automation
719
+ async function directRaw(cfg, type, args) {
720
+ const { post } = api(cfg);
721
+ const payloadRaw = args[0];
722
+ const orgId = getFlag(args, '--org') || cfg.orgId;
723
+ const payload = payloadRaw && !payloadRaw.startsWith('--') ? JSON.parse(payloadRaw) : {};
69
724
  const body = { type, payload };
70
725
  if (orgId)
71
726
  body.organization_id = orgId;
72
- print(await apiPost('/api/v2/commands', body));
73
- }
74
- async function cmdQuery(args) {
75
- const type = args[0];
76
- const payloadRaw = args[1];
77
- const orgId = getFlag(args, '--org');
78
- if (!type || !orgId)
79
- fail('Usage: checkbox query <type> <payload-json> --org <id>');
80
- const payload = payloadRaw ? JSON.parse(payloadRaw) : {};
81
- print(await apiPost('/api/v2/query', {
727
+ const res = await post('/api/v2/commands', body);
728
+ console.log(JSON.stringify(res, null, 2));
729
+ }
730
+ async function directQuery(cfg, type, args) {
731
+ const { post } = api(cfg);
732
+ const payloadRaw = args[0];
733
+ const orgId = getFlag(args, '--org') || cfg.orgId;
734
+ const payload = payloadRaw && !payloadRaw.startsWith('--') ? JSON.parse(payloadRaw) : {};
735
+ const res = await post('/api/v2/query', {
82
736
  type,
83
737
  organization_id: orgId,
84
738
  payload: { ...payload, organization_id: orgId },
85
- }));
86
- }
87
- async function cmdWorkspace(args) {
88
- const orgId = getFlag(args, '--org');
89
- if (!orgId)
90
- fail('Usage: checkbox workspace --org <id>');
91
- print(await apiPost('/api/v2/introspection/workspace', { organization_id: orgId }));
92
- }
93
- async function cmdEntities(args) {
94
- const orgId = getFlag(args, '--org');
95
- if (!orgId)
96
- fail('Usage: checkbox entities --org <id> [--type <t>] [--search <s>]');
97
- const entityType = getFlag(args, '--type');
98
- const search = getFlag(args, '--search');
99
- const limit = getFlag(args, '--limit');
100
- print(await apiPost('/api/v2/introspection/entities', {
101
- organization_id: orgId,
102
- entity_type: entityType,
103
- search,
104
- limit: limit ? parseInt(limit, 10) : undefined,
105
- }));
106
- }
107
- async function cmdSchema(args) {
108
- const orgId = getFlag(args, '--org');
109
- if (!orgId)
110
- fail('Usage: checkbox schema --org <id> [--type <t>] [--entity-id <id>]');
111
- const entityType = getFlag(args, '--type');
112
- const entityId = getFlag(args, '--entity-id');
113
- print(await apiPost('/api/v2/introspection/schema', {
114
- organization_id: orgId,
115
- entity_type: entityType,
116
- entity_id: entityId,
117
- }));
118
- }
119
- async function cmdPermissions(args) {
120
- const orgId = getFlag(args, '--org');
121
- if (!orgId)
122
- fail('Usage: checkbox permissions --org <id>');
123
- print(await apiPost('/api/v2/introspection/permissions', { organization_id: orgId }));
124
- }
125
- async function cmdOrganizations() {
126
- print(await apiPost('/api/v2/query', {
127
- type: 'list_my_organizations',
128
- payload: {},
129
- }));
130
- }
131
- function printUsage() {
132
- console.log(`Checkbox CLI — interact with the Checkbox capability API
133
-
134
- Usage:
135
- checkbox orgs List your organizations
136
- checkbox capabilities List all capabilities
137
- checkbox command <type> [payload-json] [--org <id>] Execute a command
138
- checkbox query <type> [payload-json] --org <id> Execute a query
139
- checkbox workspace --org <id> Get workspace graph
140
- checkbox entities --org <id> [--type <t>] [--search <s>] Find entities
141
- checkbox schema --org <id> [--type <t>] Get entity schemas
142
- checkbox permissions --org <id> Check permissions
143
-
144
- Environment:
145
- CHECKBOX_API_URL Base URL (default: https://checkbox.my)
146
- CHECKBOX_API_TOKEN API key (ck_live_...) or Supabase JWT
147
-
148
- Get your API key:
149
- 1. Log in to checkbox.my
150
- 2. Go to Developer Hub (sidebar)
151
- 3. Create an API key — it never expires unless you configure an expiry
152
-
153
- Examples:
154
- export CHECKBOX_API_TOKEN="ck_live_your_key_here"
155
-
156
- checkbox orgs
157
- checkbox capabilities
158
- checkbox command create_plan '{"name":"Q2 Safety"}' --org <org-id>
159
- checkbox command complete_task '{"task_id":"abc-123"}' --org <org-id>
160
- checkbox query get_task_states '{"date":"2026-03-12"}' --org <org-id>
161
- checkbox entities --org <org-id> --type task --search "Daily check"
162
- `);
739
+ });
740
+ console.log(JSON.stringify(res, null, 2));
741
+ }
742
+ function getFlag(args, flag) {
743
+ const idx = args.indexOf(flag);
744
+ if (idx === -1 || idx + 1 >= args.length)
745
+ return undefined;
746
+ return args[idx + 1];
163
747
  }
164
748
  // ── Main ────────────────────────────────────────────────────────────
165
749
  async function main() {
166
750
  const args = process.argv.slice(2);
167
751
  const sub = args[0];
168
752
  const rest = args.slice(1);
169
- if (!API_TOKEN && sub !== undefined && sub !== 'help' && sub !== '--help') {
170
- console.error('Warning: CHECKBOX_API_TOKEN is not set. Authenticated requests will fail.');
171
- console.error('Get an API key: log in to checkbox.my → Developer Hub → Create Key\n');
172
- }
173
- switch (sub) {
174
- case 'orgs':
175
- case 'organizations':
176
- return cmdOrganizations();
177
- case 'capabilities':
178
- return cmdCapabilities();
179
- case 'command':
180
- case 'cmd':
181
- return cmdCommand(rest);
182
- case 'query':
183
- return cmdQuery(rest);
184
- case 'workspace':
185
- return cmdWorkspace(rest);
186
- case 'entities':
187
- return cmdEntities(rest);
188
- case 'schema':
189
- return cmdSchema(rest);
190
- case 'permissions':
191
- return cmdPermissions(rest);
192
- case 'help':
193
- case '--help':
194
- case '-h':
195
- case undefined:
196
- return printUsage();
197
- default:
198
- console.error(`Unknown subcommand: ${sub}\n`);
199
- printUsage();
200
- process.exit(1);
753
+ // Setup command always available
754
+ if (sub === 'setup' || sub === 'init' || sub === 'login') {
755
+ await runSetup(resolveConfig());
756
+ return;
757
+ }
758
+ // Version
759
+ if (sub === '--version' || sub === '-v') {
760
+ console.log(`checkbox v${VERSION}`);
761
+ return;
762
+ }
763
+ // Help
764
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
765
+ console.log();
766
+ p.intro(pc.bgCyan(pc.black(' Checkbox CLI ')) + pc.dim(` v${VERSION}`));
767
+ p.note([
768
+ `${pc.cyan('checkbox')} Interactive menu`,
769
+ `${pc.cyan('checkbox setup')} Setup wizard (API key & workspace)`,
770
+ `${pc.cyan('checkbox plans')} List your plans`,
771
+ `${pc.cyan('checkbox tasks')} List your tasks`,
772
+ `${pc.cyan('checkbox complete <id>')} Complete a task`,
773
+ `${pc.cyan('checkbox status')} Workspace overview`,
774
+ `${pc.cyan('checkbox search <term>')} Search your workspace`,
775
+ '',
776
+ pc.dim('Advanced (for automation):'),
777
+ `${pc.dim('checkbox cmd <type> [json] [--org <id>]')}`,
778
+ `${pc.dim('checkbox query <type> [json] [--org <id>]')}`,
779
+ ].join('\n'), 'Commands');
780
+ p.outro(pc.dim('Run `checkbox setup` to get started'));
781
+ return;
782
+ }
783
+ // Check if configured
784
+ const cfg = resolveConfig();
785
+ if (!cfg) {
786
+ // First-time user — run setup
787
+ await runSetup(null);
788
+ return;
789
+ }
790
+ // Direct commands
791
+ try {
792
+ switch (sub) {
793
+ case 'plans':
794
+ return await directPlans(cfg);
795
+ case 'tasks':
796
+ return await directTasks(cfg, getFlag(rest, '--plan'));
797
+ case 'complete':
798
+ if (!rest[0]) {
799
+ // Interactive complete
800
+ const { post } = api(cfg);
801
+ return await menuComplete(cfg, post);
802
+ }
803
+ return await directComplete(cfg, rest[0]);
804
+ case 'status':
805
+ case 'overview':
806
+ return await directStatus(cfg);
807
+ case 'search':
808
+ if (!rest[0]) {
809
+ const { post } = api(cfg);
810
+ return await menuSearch(cfg, post);
811
+ }
812
+ // Direct search
813
+ const { post: searchPost } = api(cfg);
814
+ const searchRes = await searchPost('/api/v2/introspection/entities', {
815
+ organization_id: cfg.orgId,
816
+ search: rest.join(' '),
817
+ limit: 20,
818
+ });
819
+ if (searchRes.data?.length) {
820
+ const lines = searchRes.data.map((e) => ` ${pc.dim(pad(e.type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
821
+ p.note(lines.join('\n'), `Results for "${rest.join(' ')}"`);
822
+ }
823
+ else {
824
+ p.log.info('No results found.');
825
+ }
826
+ return;
827
+ case 'switch':
828
+ return await menuSwitch(cfg, api(cfg).post);
829
+ // Power user commands
830
+ case 'cmd':
831
+ case 'command':
832
+ return await directRaw(cfg, rest[0], rest.slice(1));
833
+ case 'query':
834
+ return await directQuery(cfg, rest[0], rest.slice(1));
835
+ case 'orgs':
836
+ case 'organizations':
837
+ return await directQuery(cfg, 'list_my_organizations', []);
838
+ case undefined:
839
+ // No args → interactive menu
840
+ return await interactiveMenu(cfg);
841
+ default:
842
+ p.log.error(`Unknown command: ${sub}`);
843
+ p.log.info(`Run ${pc.cyan('checkbox --help')} to see available commands`);
844
+ process.exit(1);
845
+ }
846
+ }
847
+ catch (err) {
848
+ p.log.error(err.message || 'Something went wrong');
849
+ process.exit(1);
201
850
  }
202
851
  }
203
852
  main().catch((err) => {
204
- console.error('Fatal:', err);
853
+ p.log.error(err.message || 'Fatal error');
205
854
  process.exit(1);
206
855
  });
207
- export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "checkbox-cli",
3
- "version": "1.1.0",
4
- "description": "CLI for the Checkbox capability API — create compliance plans, manage tasks, query workspaces, and automate from the terminal.",
3
+ "version": "2.0.0",
4
+ "description": "Beautiful, interactive CLI for Checkbox compliance management. Setup wizard, guided workflows, and full workspace control from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/checkbox.js",
7
7
  "bin": {
@@ -36,8 +36,12 @@
36
36
  "node": ">=18.0.0"
37
37
  },
38
38
  "devDependencies": {
39
- "typescript": "^5.7.0",
39
+ "@types/node": "^22.0.0",
40
40
  "tsx": "^4.19.0",
41
- "@types/node": "^22.0.0"
41
+ "typescript": "^5.7.0"
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^1.1.0",
45
+ "picocolors": "^1.1.1"
42
46
  }
43
47
  }