checkbox-cli 1.1.1 → 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.
- package/dist/checkbox.d.ts +9 -19
- package/dist/checkbox.js +824 -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,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
|
-
*
|
|
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.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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
// ──
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
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
|
-
|
|
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
|
-
`);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "CLI for
|
|
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
|
-
"
|
|
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
|
}
|