checkbox-cli 2.0.0 → 2.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.
- package/dist/checkbox.js +154 -183
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -20,7 +20,7 @@ import { join } from 'path';
|
|
|
20
20
|
// ── Config ──────────────────────────────────────────────────────────
|
|
21
21
|
const CONFIG_DIR = join(homedir(), '.checkbox');
|
|
22
22
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
23
|
-
const VERSION = '2.0.
|
|
23
|
+
const VERSION = '2.0.1';
|
|
24
24
|
function loadConfig() {
|
|
25
25
|
try {
|
|
26
26
|
if (!existsSync(CONFIG_FILE))
|
|
@@ -44,7 +44,6 @@ function saveConfig(cfg) {
|
|
|
44
44
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
45
45
|
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
46
46
|
}
|
|
47
|
-
// Allow env vars to override config file
|
|
48
47
|
function resolveConfig() {
|
|
49
48
|
const envKey = process.env.CHECKBOX_API_KEY || process.env.CHECKBOX_API_TOKEN;
|
|
50
49
|
const fileCfg = loadConfig();
|
|
@@ -59,6 +58,20 @@ function resolveConfig() {
|
|
|
59
58
|
return fileCfg;
|
|
60
59
|
}
|
|
61
60
|
// ── API ─────────────────────────────────────────────────────────────
|
|
61
|
+
/** Extract a human-readable error message from any API error shape */
|
|
62
|
+
function extractError(obj) {
|
|
63
|
+
if (!obj)
|
|
64
|
+
return 'Unknown error';
|
|
65
|
+
if (typeof obj === 'string')
|
|
66
|
+
return obj;
|
|
67
|
+
if (typeof obj.error === 'string')
|
|
68
|
+
return obj.error;
|
|
69
|
+
if (obj.error?.message)
|
|
70
|
+
return obj.error.message;
|
|
71
|
+
if (obj.message)
|
|
72
|
+
return obj.message;
|
|
73
|
+
return JSON.stringify(obj);
|
|
74
|
+
}
|
|
62
75
|
function api(cfg) {
|
|
63
76
|
const headers = {
|
|
64
77
|
'Content-Type': 'application/json',
|
|
@@ -70,22 +83,58 @@ function api(cfg) {
|
|
|
70
83
|
headers,
|
|
71
84
|
body: JSON.stringify(body),
|
|
72
85
|
});
|
|
86
|
+
const json = await res.json().catch(() => null);
|
|
73
87
|
if (!res.ok) {
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
throw new Error(extractError(json) || `Request failed (${res.status})`);
|
|
89
|
+
}
|
|
90
|
+
if (json && json.success === false) {
|
|
91
|
+
throw new Error(extractError(json) || 'Request failed');
|
|
76
92
|
}
|
|
77
|
-
return
|
|
93
|
+
return json;
|
|
78
94
|
}
|
|
79
95
|
async function get(path) {
|
|
80
96
|
const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
|
|
97
|
+
const json = await res.json().catch(() => null);
|
|
81
98
|
if (!res.ok) {
|
|
82
|
-
|
|
83
|
-
throw new Error(err.error || `Request failed (${res.status})`);
|
|
99
|
+
throw new Error(extractError(json) || `Request failed (${res.status})`);
|
|
84
100
|
}
|
|
85
|
-
|
|
101
|
+
if (json && json.success === false) {
|
|
102
|
+
throw new Error(extractError(json) || 'Request failed');
|
|
103
|
+
}
|
|
104
|
+
return json;
|
|
86
105
|
}
|
|
87
106
|
return { post, get };
|
|
88
107
|
}
|
|
108
|
+
// ── Query helpers ───────────────────────────────────────────────────
|
|
109
|
+
// The actual query types are: list_entities, get_entity, list_my_organizations,
|
|
110
|
+
// get_task_states, workspace_graph, entity_discovery, schema_discovery,
|
|
111
|
+
// permission_discovery, list_approvals.
|
|
112
|
+
// There is NO list_plans or list_tasks — use list_entities with entity_type.
|
|
113
|
+
async function listEntities(post, orgId, entityType) {
|
|
114
|
+
const res = await post('/api/v2/query', {
|
|
115
|
+
type: 'list_entities',
|
|
116
|
+
organization_id: orgId,
|
|
117
|
+
payload: { entity_type: entityType, organization_id: orgId },
|
|
118
|
+
});
|
|
119
|
+
// list_entities returns { success, data: [...], meta }
|
|
120
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
121
|
+
}
|
|
122
|
+
async function searchEntities(post, orgId, search, limit = 20) {
|
|
123
|
+
// entity_discovery returns { success, data: { organization_id, entities: [...] } }
|
|
124
|
+
const res = await post('/api/v2/introspection/entities', {
|
|
125
|
+
organization_id: orgId,
|
|
126
|
+
search,
|
|
127
|
+
limit,
|
|
128
|
+
});
|
|
129
|
+
return res.data?.entities || [];
|
|
130
|
+
}
|
|
131
|
+
async function getWorkspaceNodes(post, orgId) {
|
|
132
|
+
// workspace returns { success, data: { organization_id, nodes: [...], edges: [...] } }
|
|
133
|
+
const res = await post('/api/v2/introspection/workspace', {
|
|
134
|
+
organization_id: orgId,
|
|
135
|
+
});
|
|
136
|
+
return res.data?.nodes || [];
|
|
137
|
+
}
|
|
89
138
|
// ── Formatters ──────────────────────────────────────────────────────
|
|
90
139
|
function truncate(s, max) {
|
|
91
140
|
return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
|
|
@@ -135,16 +184,13 @@ async function runSetup(existingConfig) {
|
|
|
135
184
|
orgId: '',
|
|
136
185
|
orgName: '',
|
|
137
186
|
};
|
|
138
|
-
// Test the key & fetch organizations
|
|
139
187
|
const s = p.spinner();
|
|
140
188
|
s.start('Connecting to Checkbox...');
|
|
141
189
|
let orgs = [];
|
|
142
190
|
try {
|
|
143
191
|
const { post } = api(cfg);
|
|
144
192
|
const res = await post('/api/v2/query', { type: 'list_my_organizations', payload: {} });
|
|
145
|
-
|
|
146
|
-
throw new Error(res.error?.message || 'Failed to fetch organizations');
|
|
147
|
-
orgs = res.data;
|
|
193
|
+
orgs = Array.isArray(res.data) ? res.data : [];
|
|
148
194
|
s.stop(`Connected! Found ${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
|
|
149
195
|
}
|
|
150
196
|
catch (err) {
|
|
@@ -154,7 +200,6 @@ async function runSetup(existingConfig) {
|
|
|
154
200
|
p.outro(pc.dim('Try again with a valid key.'));
|
|
155
201
|
process.exit(1);
|
|
156
202
|
}
|
|
157
|
-
// Pick workspace
|
|
158
203
|
if (orgs.length === 0) {
|
|
159
204
|
p.log.warn('No workspaces found. Create one at checkbox.my first.');
|
|
160
205
|
saveConfig(cfg);
|
|
@@ -193,7 +238,7 @@ async function runSetup(existingConfig) {
|
|
|
193
238
|
}
|
|
194
239
|
// ── Interactive Menu ────────────────────────────────────────────────
|
|
195
240
|
async function interactiveMenu(cfg) {
|
|
196
|
-
const { post
|
|
241
|
+
const { post } = api(cfg);
|
|
197
242
|
console.log();
|
|
198
243
|
p.intro(pc.bgCyan(pc.black(' Checkbox ')) +
|
|
199
244
|
pc.dim(` v${VERSION}`) +
|
|
@@ -217,20 +262,13 @@ async function interactiveMenu(cfg) {
|
|
|
217
262
|
return;
|
|
218
263
|
}
|
|
219
264
|
switch (action) {
|
|
220
|
-
case 'plans':
|
|
221
|
-
|
|
222
|
-
case '
|
|
223
|
-
|
|
224
|
-
case '
|
|
225
|
-
|
|
226
|
-
case '
|
|
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);
|
|
265
|
+
case 'plans': return menuPlans(cfg, post);
|
|
266
|
+
case 'tasks': return menuTasks(cfg, post);
|
|
267
|
+
case 'status': return menuStatus(cfg, post);
|
|
268
|
+
case 'search': return menuSearch(cfg, post);
|
|
269
|
+
case 'complete': return menuComplete(cfg, post);
|
|
270
|
+
case 'create-task': return menuCreateTask(cfg, post);
|
|
271
|
+
case 'switch': return menuSwitch(cfg, post);
|
|
234
272
|
case 'setup':
|
|
235
273
|
await runSetup(cfg);
|
|
236
274
|
return;
|
|
@@ -241,27 +279,19 @@ async function menuPlans(cfg, post) {
|
|
|
241
279
|
const s = p.spinner();
|
|
242
280
|
s.start('Loading plans...');
|
|
243
281
|
try {
|
|
244
|
-
const
|
|
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 || [];
|
|
282
|
+
const plans = await listEntities(post, cfg.orgId, 'plan');
|
|
252
283
|
s.stop(`Found ${plans.length} plan${plans.length === 1 ? '' : 's'}`);
|
|
253
284
|
if (plans.length === 0) {
|
|
254
285
|
p.log.info('No plans yet. Create one at checkbox.my');
|
|
255
|
-
|
|
256
|
-
return;
|
|
286
|
+
return interactiveMenu(cfg);
|
|
257
287
|
}
|
|
258
288
|
const lines = plans.map((plan) => {
|
|
259
289
|
const name = truncate(plan.name || 'Untitled', 40);
|
|
260
|
-
const
|
|
290
|
+
const meta = plan.metadata || {};
|
|
291
|
+
const created = meta.created_at ? formatDate(meta.created_at) : '';
|
|
261
292
|
return `${pc.cyan(pad(name, 42))} ${pc.dim(created)}`;
|
|
262
293
|
});
|
|
263
294
|
p.note(lines.join('\n'), `Plans in ${cfg.orgName}`);
|
|
264
|
-
// Offer drill-down
|
|
265
295
|
const selected = await p.select({
|
|
266
296
|
message: 'Select a plan to view its tasks',
|
|
267
297
|
options: [
|
|
@@ -275,33 +305,36 @@ async function menuPlans(cfg, post) {
|
|
|
275
305
|
if (p.isCancel(selected) || selected === '__back') {
|
|
276
306
|
return interactiveMenu(cfg);
|
|
277
307
|
}
|
|
278
|
-
return menuPlanTasks(cfg, post, selected, plans.find((
|
|
308
|
+
return menuPlanTasks(cfg, post, selected, plans.find((pl) => pl.id === selected)?.name || '');
|
|
279
309
|
}
|
|
280
310
|
catch (err) {
|
|
281
311
|
s.stop(pc.red('Failed'));
|
|
282
|
-
p.log.error(err.message);
|
|
312
|
+
p.log.error(err.message || 'Could not load plans');
|
|
313
|
+
return interactiveMenu(cfg);
|
|
283
314
|
}
|
|
284
315
|
}
|
|
285
316
|
async function menuPlanTasks(cfg, post, planId, planName) {
|
|
286
317
|
const s = p.spinner();
|
|
287
318
|
s.start('Loading tasks...');
|
|
288
319
|
try {
|
|
320
|
+
// Search entities by plan — use entity_discovery with search for plan-specific tasks
|
|
321
|
+
// Or query list_entities for tasks and filter by parent_id
|
|
289
322
|
const res = await post('/api/v2/query', {
|
|
290
|
-
type: '
|
|
323
|
+
type: 'list_entities',
|
|
291
324
|
organization_id: cfg.orgId,
|
|
292
|
-
payload: { organization_id: cfg.orgId
|
|
325
|
+
payload: { entity_type: 'task', organization_id: cfg.orgId },
|
|
293
326
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const tasks =
|
|
327
|
+
const allTasks = Array.isArray(res.data) ? res.data : [];
|
|
328
|
+
// Filter tasks that belong to this plan via parent_id chain
|
|
329
|
+
const tasks = allTasks.filter((t) => t.parent_id === planId || t.metadata?.plan_id === planId);
|
|
297
330
|
s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} in ${pc.cyan(planName)}`);
|
|
298
331
|
if (tasks.length === 0) {
|
|
299
|
-
p.log.info('No tasks
|
|
332
|
+
p.log.info('No tasks found for this plan.');
|
|
300
333
|
return interactiveMenu(cfg);
|
|
301
334
|
}
|
|
302
335
|
const lines = tasks.map((t) => {
|
|
303
336
|
const name = truncate(t.name || 'Untitled', 50);
|
|
304
|
-
const freq = t.frequency || '';
|
|
337
|
+
const freq = t.metadata?.frequency || '';
|
|
305
338
|
return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)}`;
|
|
306
339
|
});
|
|
307
340
|
p.note(lines.join('\n'), planName);
|
|
@@ -309,7 +342,7 @@ async function menuPlanTasks(cfg, post, planId, planName) {
|
|
|
309
342
|
}
|
|
310
343
|
catch (err) {
|
|
311
344
|
s.stop(pc.red('Failed'));
|
|
312
|
-
p.log.error(err.message);
|
|
345
|
+
p.log.error(err.message || 'Could not load tasks');
|
|
313
346
|
return interactiveMenu(cfg);
|
|
314
347
|
}
|
|
315
348
|
}
|
|
@@ -318,14 +351,7 @@ async function menuTasks(cfg, post) {
|
|
|
318
351
|
const s = p.spinner();
|
|
319
352
|
s.start('Loading tasks...');
|
|
320
353
|
try {
|
|
321
|
-
const
|
|
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 || [];
|
|
354
|
+
const tasks = await listEntities(post, cfg.orgId, 'task');
|
|
329
355
|
s.stop(`Found ${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
|
|
330
356
|
if (tasks.length === 0) {
|
|
331
357
|
p.log.info('No tasks yet.');
|
|
@@ -337,7 +363,7 @@ async function menuTasks(cfg, post) {
|
|
|
337
363
|
...tasks.slice(0, 25).map((t) => ({
|
|
338
364
|
value: t.id,
|
|
339
365
|
label: truncate(t.name || 'Untitled', 60),
|
|
340
|
-
hint: t.frequency || '',
|
|
366
|
+
hint: t.metadata?.frequency || '',
|
|
341
367
|
})),
|
|
342
368
|
...(tasks.length > 25 ? [{ value: '__more', label: pc.dim(`...and ${tasks.length - 25} more`) }] : []),
|
|
343
369
|
{ value: '__back', label: pc.dim('Back to menu') },
|
|
@@ -346,14 +372,13 @@ async function menuTasks(cfg, post) {
|
|
|
346
372
|
if (p.isCancel(selected) || selected === '__back' || selected === '__more') {
|
|
347
373
|
return interactiveMenu(cfg);
|
|
348
374
|
}
|
|
349
|
-
// Show task detail
|
|
350
375
|
const task = tasks.find((t) => t.id === selected);
|
|
351
376
|
if (task) {
|
|
377
|
+
const meta = task.metadata || {};
|
|
352
378
|
const lines = [
|
|
353
379
|
`${pad('Name', 14)} ${pc.cyan(task.name || 'Untitled')}`,
|
|
354
|
-
`${pad('Frequency', 14)} ${
|
|
355
|
-
`${pad('
|
|
356
|
-
`${pad('Assigned to', 14)} ${task.assigned_to || pc.dim('unassigned')}`,
|
|
380
|
+
`${pad('Frequency', 14)} ${meta.frequency || pc.dim('none')}`,
|
|
381
|
+
`${pad('Assigned to', 14)} ${meta.assigned_to || pc.dim('unassigned')}`,
|
|
357
382
|
`${pad('ID', 14)} ${pc.dim(task.id)}`,
|
|
358
383
|
];
|
|
359
384
|
p.note(lines.join('\n'), task.name || 'Task');
|
|
@@ -372,7 +397,7 @@ async function menuTasks(cfg, post) {
|
|
|
372
397
|
}
|
|
373
398
|
catch (err) {
|
|
374
399
|
s.stop(pc.red('Failed'));
|
|
375
|
-
p.log.error(err.message);
|
|
400
|
+
p.log.error(err.message || 'Could not load tasks');
|
|
376
401
|
return interactiveMenu(cfg);
|
|
377
402
|
}
|
|
378
403
|
}
|
|
@@ -381,30 +406,30 @@ async function menuStatus(cfg, post) {
|
|
|
381
406
|
const s = p.spinner();
|
|
382
407
|
s.start('Loading workspace...');
|
|
383
408
|
try {
|
|
384
|
-
const
|
|
385
|
-
organization_id: cfg.orgId,
|
|
386
|
-
});
|
|
387
|
-
if (!res.success)
|
|
388
|
-
throw new Error(res.error?.message || 'Failed');
|
|
389
|
-
const ws = res.data || {};
|
|
409
|
+
const nodes = await getWorkspaceNodes(post, cfg.orgId);
|
|
390
410
|
s.stop('Workspace loaded');
|
|
391
|
-
|
|
411
|
+
// Count nodes by type
|
|
412
|
+
const counts = {};
|
|
413
|
+
for (const node of nodes) {
|
|
414
|
+
const t = node.type || 'unknown';
|
|
415
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
416
|
+
}
|
|
392
417
|
const lines = [
|
|
393
418
|
`${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
|
|
394
419
|
'',
|
|
395
|
-
`${pad('Plans', 16)} ${pc.bold(String(counts.
|
|
396
|
-
`${pad('Tasks', 16)} ${pc.bold(String(counts.
|
|
397
|
-
`${pad('Requirements', 16)} ${pc.bold(String(counts.
|
|
398
|
-
`${pad('Goals', 16)} ${pc.bold(String(counts.
|
|
399
|
-
`${pad('
|
|
400
|
-
`${pad('
|
|
401
|
-
`${pad('
|
|
420
|
+
`${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
|
|
421
|
+
`${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
|
|
422
|
+
`${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
|
|
423
|
+
`${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
|
|
424
|
+
`${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
|
|
425
|
+
`${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
|
|
426
|
+
`${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
|
|
402
427
|
];
|
|
403
428
|
p.note(lines.join('\n'), 'Workspace Overview');
|
|
404
429
|
}
|
|
405
430
|
catch (err) {
|
|
406
431
|
s.stop(pc.red('Failed'));
|
|
407
|
-
p.log.error(err.message);
|
|
432
|
+
p.log.error(err.message || 'Could not load workspace');
|
|
408
433
|
}
|
|
409
434
|
return interactiveMenu(cfg);
|
|
410
435
|
}
|
|
@@ -423,21 +448,14 @@ async function menuSearch(cfg, post) {
|
|
|
423
448
|
const s = p.spinner();
|
|
424
449
|
s.start('Searching...');
|
|
425
450
|
try {
|
|
426
|
-
const
|
|
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 || [];
|
|
451
|
+
const entities = await searchEntities(post, cfg.orgId, query);
|
|
434
452
|
s.stop(`Found ${entities.length} result${entities.length === 1 ? '' : 's'}`);
|
|
435
453
|
if (entities.length === 0) {
|
|
436
454
|
p.log.info('No results found.');
|
|
437
455
|
}
|
|
438
456
|
else {
|
|
439
457
|
const lines = entities.map((e) => {
|
|
440
|
-
const type = pad(e.
|
|
458
|
+
const type = pad(e.entity_type || 'unknown', 14);
|
|
441
459
|
const name = truncate(e.name || 'Untitled', 50);
|
|
442
460
|
return ` ${pc.dim(type)} ${pc.cyan(name)}`;
|
|
443
461
|
});
|
|
@@ -446,7 +464,7 @@ async function menuSearch(cfg, post) {
|
|
|
446
464
|
}
|
|
447
465
|
catch (err) {
|
|
448
466
|
s.stop(pc.red('Failed'));
|
|
449
|
-
p.log.error(err.message);
|
|
467
|
+
p.log.error(err.message || 'Search failed');
|
|
450
468
|
}
|
|
451
469
|
return interactiveMenu(cfg);
|
|
452
470
|
}
|
|
@@ -455,14 +473,7 @@ async function menuComplete(cfg, post) {
|
|
|
455
473
|
const s = p.spinner();
|
|
456
474
|
s.start('Loading tasks...');
|
|
457
475
|
try {
|
|
458
|
-
const
|
|
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 || [];
|
|
476
|
+
const tasks = await listEntities(post, cfg.orgId, 'task');
|
|
466
477
|
s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} available`);
|
|
467
478
|
if (tasks.length === 0) {
|
|
468
479
|
p.log.info('No tasks to complete.');
|
|
@@ -474,7 +485,7 @@ async function menuComplete(cfg, post) {
|
|
|
474
485
|
...tasks.slice(0, 30).map((t) => ({
|
|
475
486
|
value: t.id,
|
|
476
487
|
label: truncate(t.name || 'Untitled', 60),
|
|
477
|
-
hint: t.frequency || '',
|
|
488
|
+
hint: t.metadata?.frequency || '',
|
|
478
489
|
})),
|
|
479
490
|
{ value: '__back', label: pc.dim('Back to menu') },
|
|
480
491
|
],
|
|
@@ -487,7 +498,7 @@ async function menuComplete(cfg, post) {
|
|
|
487
498
|
}
|
|
488
499
|
catch (err) {
|
|
489
500
|
s.stop(pc.red('Failed'));
|
|
490
|
-
p.log.error(err.message);
|
|
501
|
+
p.log.error(err.message || 'Could not load tasks');
|
|
491
502
|
}
|
|
492
503
|
return interactiveMenu(cfg);
|
|
493
504
|
}
|
|
@@ -495,33 +506,25 @@ async function doComplete(cfg, post, taskId, taskName) {
|
|
|
495
506
|
const s = p.spinner();
|
|
496
507
|
s.start('Completing task...');
|
|
497
508
|
try {
|
|
498
|
-
|
|
509
|
+
await post('/api/v2/commands', {
|
|
499
510
|
type: 'complete_task',
|
|
500
511
|
organization_id: cfg.orgId,
|
|
501
512
|
payload: { task_id: taskId },
|
|
502
513
|
});
|
|
503
|
-
if (!res.success)
|
|
504
|
-
throw new Error(res.error?.message || 'Failed');
|
|
505
514
|
s.stop(pc.green(`Done! "${taskName}" completed`));
|
|
506
515
|
}
|
|
507
516
|
catch (err) {
|
|
508
517
|
s.stop(pc.red('Failed'));
|
|
509
|
-
p.log.error(err.message);
|
|
518
|
+
p.log.error(err.message || 'Could not complete task');
|
|
510
519
|
}
|
|
511
520
|
}
|
|
512
521
|
// ── Create Task ─────────────────────────────────────────────────────
|
|
513
522
|
async function menuCreateTask(cfg, post) {
|
|
514
|
-
// First, get plans to attach task to
|
|
515
523
|
const s = p.spinner();
|
|
516
524
|
s.start('Loading plans...');
|
|
517
525
|
let plans = [];
|
|
518
526
|
try {
|
|
519
|
-
|
|
520
|
-
type: 'list_plans',
|
|
521
|
-
organization_id: cfg.orgId,
|
|
522
|
-
payload: { organization_id: cfg.orgId },
|
|
523
|
-
});
|
|
524
|
-
plans = res.data || [];
|
|
527
|
+
plans = await listEntities(post, cfg.orgId, 'plan');
|
|
525
528
|
s.stop('');
|
|
526
529
|
}
|
|
527
530
|
catch {
|
|
@@ -577,18 +580,16 @@ async function menuCreateTask(cfg, post) {
|
|
|
577
580
|
};
|
|
578
581
|
if (result.planId)
|
|
579
582
|
payload.plan_id = result.planId;
|
|
580
|
-
|
|
583
|
+
await post('/api/v2/commands', {
|
|
581
584
|
type: 'create_task',
|
|
582
585
|
organization_id: cfg.orgId,
|
|
583
586
|
payload,
|
|
584
587
|
});
|
|
585
|
-
if (!res.success)
|
|
586
|
-
throw new Error(res.error?.message || 'Failed');
|
|
587
588
|
spinner.stop(pc.green(`Task "${result.name}" created!`));
|
|
588
589
|
}
|
|
589
590
|
catch (err) {
|
|
590
591
|
spinner.stop(pc.red('Failed'));
|
|
591
|
-
p.log.error(err.message);
|
|
592
|
+
p.log.error(err.message || 'Could not create task');
|
|
592
593
|
}
|
|
593
594
|
return interactiveMenu(cfg);
|
|
594
595
|
}
|
|
@@ -601,9 +602,7 @@ async function menuSwitch(cfg, post) {
|
|
|
601
602
|
type: 'list_my_organizations',
|
|
602
603
|
payload: {},
|
|
603
604
|
});
|
|
604
|
-
|
|
605
|
-
throw new Error(res.error?.message || 'Failed');
|
|
606
|
-
const orgs = res.data || [];
|
|
605
|
+
const orgs = Array.isArray(res.data) ? res.data : [];
|
|
607
606
|
s.stop(`${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
|
|
608
607
|
const selected = await p.select({
|
|
609
608
|
message: 'Switch to workspace',
|
|
@@ -623,58 +622,40 @@ async function menuSwitch(cfg, post) {
|
|
|
623
622
|
}
|
|
624
623
|
catch (err) {
|
|
625
624
|
s.stop(pc.red('Failed'));
|
|
626
|
-
p.log.error(err.message);
|
|
625
|
+
p.log.error(err.message || 'Could not load workspaces');
|
|
627
626
|
}
|
|
628
627
|
return interactiveMenu(cfg);
|
|
629
628
|
}
|
|
630
|
-
// ── Direct Commands (non-interactive
|
|
629
|
+
// ── Direct Commands (non-interactive) ───────────────────────────────
|
|
631
630
|
async function directPlans(cfg) {
|
|
632
631
|
const { post } = api(cfg);
|
|
633
632
|
const s = p.spinner();
|
|
634
633
|
s.start('Loading plans...');
|
|
635
|
-
const
|
|
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');
|
|
634
|
+
const plans = await listEntities(post, cfg.orgId, 'plan');
|
|
642
635
|
s.stop('');
|
|
643
|
-
const plans = res.data || [];
|
|
644
636
|
if (plans.length === 0) {
|
|
645
637
|
p.log.info('No plans found.');
|
|
646
638
|
return;
|
|
647
639
|
}
|
|
648
640
|
const lines = plans.map((plan) => {
|
|
649
641
|
const name = truncate(plan.name || 'Untitled', 40);
|
|
650
|
-
|
|
651
|
-
return ` ${pc.cyan(pad(name, 42))} ${pc.dim(id)}`;
|
|
642
|
+
return ` ${pc.cyan(pad(name, 42))} ${pc.dim(plan.id)}`;
|
|
652
643
|
});
|
|
653
644
|
p.note(lines.join('\n'), `${plans.length} plan${plans.length === 1 ? '' : 's'}`);
|
|
654
645
|
}
|
|
655
|
-
async function directTasks(cfg
|
|
646
|
+
async function directTasks(cfg) {
|
|
656
647
|
const { post } = api(cfg);
|
|
657
648
|
const s = p.spinner();
|
|
658
649
|
s.start('Loading tasks...');
|
|
659
|
-
const
|
|
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');
|
|
650
|
+
const tasks = await listEntities(post, cfg.orgId, 'task');
|
|
669
651
|
s.stop('');
|
|
670
|
-
const tasks = res.data || [];
|
|
671
652
|
if (tasks.length === 0) {
|
|
672
653
|
p.log.info('No tasks found.');
|
|
673
654
|
return;
|
|
674
655
|
}
|
|
675
656
|
const lines = tasks.map((t) => {
|
|
676
657
|
const name = truncate(t.name || 'Untitled', 50);
|
|
677
|
-
const freq = t.frequency || '';
|
|
658
|
+
const freq = t.metadata?.frequency || '';
|
|
678
659
|
return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)} ${pc.dim(t.id)}`;
|
|
679
660
|
});
|
|
680
661
|
p.note(lines.join('\n'), `${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
|
|
@@ -683,39 +664,52 @@ async function directComplete(cfg, taskId) {
|
|
|
683
664
|
const { post } = api(cfg);
|
|
684
665
|
const s = p.spinner();
|
|
685
666
|
s.start('Completing task...');
|
|
686
|
-
|
|
667
|
+
await post('/api/v2/commands', {
|
|
687
668
|
type: 'complete_task',
|
|
688
669
|
organization_id: cfg.orgId,
|
|
689
670
|
payload: { task_id: taskId },
|
|
690
671
|
});
|
|
691
|
-
if (!res.success)
|
|
692
|
-
throw new Error(res.error?.message || 'Failed');
|
|
693
672
|
s.stop(pc.green('Task completed!'));
|
|
694
673
|
}
|
|
695
674
|
async function directStatus(cfg) {
|
|
696
675
|
const { post } = api(cfg);
|
|
697
676
|
const s = p.spinner();
|
|
698
677
|
s.start('Loading workspace...');
|
|
699
|
-
const
|
|
700
|
-
organization_id: cfg.orgId,
|
|
701
|
-
});
|
|
702
|
-
if (!res.success)
|
|
703
|
-
throw new Error(res.error?.message || 'Failed');
|
|
678
|
+
const nodes = await getWorkspaceNodes(post, cfg.orgId);
|
|
704
679
|
s.stop('');
|
|
705
|
-
const counts =
|
|
680
|
+
const counts = {};
|
|
681
|
+
for (const node of nodes) {
|
|
682
|
+
const t = node.type || 'unknown';
|
|
683
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
684
|
+
}
|
|
706
685
|
const lines = [
|
|
707
686
|
`${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
|
|
708
687
|
'',
|
|
709
|
-
`${pad('Plans', 16)} ${pc.bold(String(counts.
|
|
710
|
-
`${pad('Tasks', 16)} ${pc.bold(String(counts.
|
|
711
|
-
`${pad('Requirements', 16)} ${pc.bold(String(counts.
|
|
712
|
-
`${pad('Goals', 16)} ${pc.bold(String(counts.
|
|
713
|
-
`${pad('
|
|
714
|
-
`${pad('
|
|
688
|
+
`${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
|
|
689
|
+
`${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
|
|
690
|
+
`${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
|
|
691
|
+
`${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
|
|
692
|
+
`${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
|
|
693
|
+
`${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
|
|
694
|
+
`${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
|
|
715
695
|
];
|
|
716
696
|
p.note(lines.join('\n'), 'Workspace Overview');
|
|
717
697
|
}
|
|
718
|
-
|
|
698
|
+
async function directSearch(cfg, term) {
|
|
699
|
+
const { post } = api(cfg);
|
|
700
|
+
const s = p.spinner();
|
|
701
|
+
s.start('Searching...');
|
|
702
|
+
const entities = await searchEntities(post, cfg.orgId, term);
|
|
703
|
+
s.stop('');
|
|
704
|
+
if (entities.length === 0) {
|
|
705
|
+
p.log.info('No results found.');
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
|
|
709
|
+
p.note(lines.join('\n'), `${entities.length} result${entities.length === 1 ? '' : 's'} for "${term}"`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Raw passthrough for power users
|
|
719
713
|
async function directRaw(cfg, type, args) {
|
|
720
714
|
const { post } = api(cfg);
|
|
721
715
|
const payloadRaw = args[0];
|
|
@@ -750,17 +744,14 @@ async function main() {
|
|
|
750
744
|
const args = process.argv.slice(2);
|
|
751
745
|
const sub = args[0];
|
|
752
746
|
const rest = args.slice(1);
|
|
753
|
-
// Setup command always available
|
|
754
747
|
if (sub === 'setup' || sub === 'init' || sub === 'login') {
|
|
755
748
|
await runSetup(resolveConfig());
|
|
756
749
|
return;
|
|
757
750
|
}
|
|
758
|
-
// Version
|
|
759
751
|
if (sub === '--version' || sub === '-v') {
|
|
760
752
|
console.log(`checkbox v${VERSION}`);
|
|
761
753
|
return;
|
|
762
754
|
}
|
|
763
|
-
// Help
|
|
764
755
|
if (sub === '--help' || sub === '-h' || sub === 'help') {
|
|
765
756
|
console.log();
|
|
766
757
|
p.intro(pc.bgCyan(pc.black(' Checkbox CLI ')) + pc.dim(` v${VERSION}`));
|
|
@@ -780,23 +771,19 @@ async function main() {
|
|
|
780
771
|
p.outro(pc.dim('Run `checkbox setup` to get started'));
|
|
781
772
|
return;
|
|
782
773
|
}
|
|
783
|
-
// Check if configured
|
|
784
774
|
const cfg = resolveConfig();
|
|
785
775
|
if (!cfg) {
|
|
786
|
-
// First-time user — run setup
|
|
787
776
|
await runSetup(null);
|
|
788
777
|
return;
|
|
789
778
|
}
|
|
790
|
-
// Direct commands
|
|
791
779
|
try {
|
|
792
780
|
switch (sub) {
|
|
793
781
|
case 'plans':
|
|
794
782
|
return await directPlans(cfg);
|
|
795
783
|
case 'tasks':
|
|
796
|
-
return await directTasks(cfg
|
|
784
|
+
return await directTasks(cfg);
|
|
797
785
|
case 'complete':
|
|
798
786
|
if (!rest[0]) {
|
|
799
|
-
// Interactive complete
|
|
800
787
|
const { post } = api(cfg);
|
|
801
788
|
return await menuComplete(cfg, post);
|
|
802
789
|
}
|
|
@@ -809,24 +796,9 @@ async function main() {
|
|
|
809
796
|
const { post } = api(cfg);
|
|
810
797
|
return await menuSearch(cfg, post);
|
|
811
798
|
}
|
|
812
|
-
|
|
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;
|
|
799
|
+
return await directSearch(cfg, rest.join(' '));
|
|
827
800
|
case 'switch':
|
|
828
801
|
return await menuSwitch(cfg, api(cfg).post);
|
|
829
|
-
// Power user commands
|
|
830
802
|
case 'cmd':
|
|
831
803
|
case 'command':
|
|
832
804
|
return await directRaw(cfg, rest[0], rest.slice(1));
|
|
@@ -836,7 +808,6 @@ async function main() {
|
|
|
836
808
|
case 'organizations':
|
|
837
809
|
return await directQuery(cfg, 'list_my_organizations', []);
|
|
838
810
|
case undefined:
|
|
839
|
-
// No args → interactive menu
|
|
840
811
|
return await interactiveMenu(cfg);
|
|
841
812
|
default:
|
|
842
813
|
p.log.error(`Unknown command: ${sub}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "checkbox-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
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",
|