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