checkbox-cli 2.0.1 → 3.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 +4 -8
- package/dist/checkbox.js +1994 -401
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Checkbox CLI —
|
|
3
|
+
* Checkbox CLI v3.0.0 — Full-featured compliance management from your terminal.
|
|
4
|
+
*
|
|
5
|
+
* Every capability available in the Checkbox web app is accessible here:
|
|
6
|
+
* tasks, plans, tables, documents, dashboards, forms, team management, and more.
|
|
4
7
|
*
|
|
5
8
|
* First-time users: just run `checkbox` and follow the guided setup.
|
|
6
9
|
* Returning users: `checkbox` opens the interactive menu.
|
|
7
|
-
*
|
|
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
|
|
14
10
|
*/
|
|
15
11
|
import * as p from '@clack/prompts';
|
|
16
12
|
import pc from 'picocolors';
|
|
@@ -20,7 +16,7 @@ import { join } from 'path';
|
|
|
20
16
|
// ── Config ──────────────────────────────────────────────────────────
|
|
21
17
|
const CONFIG_DIR = join(homedir(), '.checkbox');
|
|
22
18
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
23
|
-
const VERSION = '
|
|
19
|
+
const VERSION = '3.0.0';
|
|
24
20
|
function loadConfig() {
|
|
25
21
|
try {
|
|
26
22
|
if (!existsSync(CONFIG_FILE))
|
|
@@ -58,7 +54,6 @@ function resolveConfig() {
|
|
|
58
54
|
return fileCfg;
|
|
59
55
|
}
|
|
60
56
|
// ── API ─────────────────────────────────────────────────────────────
|
|
61
|
-
/** Extract a human-readable error message from any API error shape */
|
|
62
57
|
function extractError(obj) {
|
|
63
58
|
if (!obj)
|
|
64
59
|
return 'Unknown error';
|
|
@@ -84,43 +79,33 @@ function api(cfg) {
|
|
|
84
79
|
body: JSON.stringify(body),
|
|
85
80
|
});
|
|
86
81
|
const json = await res.json().catch(() => null);
|
|
87
|
-
if (!res.ok)
|
|
82
|
+
if (!res.ok)
|
|
88
83
|
throw new Error(extractError(json) || `Request failed (${res.status})`);
|
|
89
|
-
|
|
90
|
-
if (json && json.success === false) {
|
|
84
|
+
if (json && json.success === false)
|
|
91
85
|
throw new Error(extractError(json) || 'Request failed');
|
|
92
|
-
}
|
|
93
86
|
return json;
|
|
94
87
|
}
|
|
95
88
|
async function get(path) {
|
|
96
89
|
const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
|
|
97
90
|
const json = await res.json().catch(() => null);
|
|
98
|
-
if (!res.ok)
|
|
91
|
+
if (!res.ok)
|
|
99
92
|
throw new Error(extractError(json) || `Request failed (${res.status})`);
|
|
100
|
-
|
|
101
|
-
if (json && json.success === false) {
|
|
93
|
+
if (json && json.success === false)
|
|
102
94
|
throw new Error(extractError(json) || 'Request failed');
|
|
103
|
-
}
|
|
104
95
|
return json;
|
|
105
96
|
}
|
|
106
97
|
return { post, get };
|
|
107
98
|
}
|
|
108
|
-
// ──
|
|
109
|
-
|
|
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) {
|
|
99
|
+
// ── Data Helpers ────────────────────────────────────────────────────
|
|
100
|
+
async function listEntities(post, orgId, entityType, filters) {
|
|
114
101
|
const res = await post('/api/v2/query', {
|
|
115
102
|
type: 'list_entities',
|
|
116
103
|
organization_id: orgId,
|
|
117
|
-
payload: { entity_type: entityType, organization_id: orgId },
|
|
104
|
+
payload: { entity_type: entityType, organization_id: orgId, ...filters },
|
|
118
105
|
});
|
|
119
|
-
// list_entities returns { success, data: [...], meta }
|
|
120
106
|
return Array.isArray(res.data) ? res.data : [];
|
|
121
107
|
}
|
|
122
108
|
async function searchEntities(post, orgId, search, limit = 20) {
|
|
123
|
-
// entity_discovery returns { success, data: { organization_id, entities: [...] } }
|
|
124
109
|
const res = await post('/api/v2/introspection/entities', {
|
|
125
110
|
organization_id: orgId,
|
|
126
111
|
search,
|
|
@@ -129,7 +114,6 @@ async function searchEntities(post, orgId, search, limit = 20) {
|
|
|
129
114
|
return res.data?.entities || [];
|
|
130
115
|
}
|
|
131
116
|
async function getWorkspaceNodes(post, orgId) {
|
|
132
|
-
// workspace returns { success, data: { organization_id, nodes: [...], edges: [...] } }
|
|
133
117
|
const res = await post('/api/v2/introspection/workspace', {
|
|
134
118
|
organization_id: orgId,
|
|
135
119
|
});
|
|
@@ -145,6 +129,106 @@ function formatDate(iso) {
|
|
|
145
129
|
function pad(s, n) {
|
|
146
130
|
return s.padEnd(n);
|
|
147
131
|
}
|
|
132
|
+
// ── UI Helpers ──────────────────────────────────────────────────────
|
|
133
|
+
async function pickEntity(post, orgId, entityType, label, filters) {
|
|
134
|
+
const s = p.spinner();
|
|
135
|
+
s.start(`Loading ${label}...`);
|
|
136
|
+
try {
|
|
137
|
+
const items = await listEntities(post, orgId, entityType, filters);
|
|
138
|
+
s.stop(`${items.length} ${label}`);
|
|
139
|
+
if (items.length === 0) {
|
|
140
|
+
p.log.info(`No ${label} found.`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const selected = await p.select({
|
|
144
|
+
message: `Choose a ${label.replace(/s$/, '')}`,
|
|
145
|
+
options: [
|
|
146
|
+
...items.slice(0, 50).map((e) => ({
|
|
147
|
+
value: e.id,
|
|
148
|
+
label: truncate(e.name || 'Untitled', 60),
|
|
149
|
+
hint: e.metadata?.frequency || '',
|
|
150
|
+
})),
|
|
151
|
+
...(items.length > 50 ? [{ value: '__more', label: pc.dim(`\u2026${items.length - 50} more (use search)`) }] : []),
|
|
152
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
if (p.isCancel(selected) || selected === '__back' || selected === '__more')
|
|
156
|
+
return null;
|
|
157
|
+
return items.find((e) => e.id === selected) || null;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
s.stop(pc.red('Failed'));
|
|
161
|
+
p.log.error(err.message || `Could not load ${label}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function txt(message, required = false, placeholder) {
|
|
166
|
+
const val = await p.text({
|
|
167
|
+
message,
|
|
168
|
+
placeholder,
|
|
169
|
+
validate: required ? (v) => { if (!v)
|
|
170
|
+
return 'This field is required'; } : undefined,
|
|
171
|
+
});
|
|
172
|
+
if (p.isCancel(val))
|
|
173
|
+
return null;
|
|
174
|
+
return val || null;
|
|
175
|
+
}
|
|
176
|
+
async function sel(message, options) {
|
|
177
|
+
const val = await p.select({ message, options });
|
|
178
|
+
if (p.isCancel(val))
|
|
179
|
+
return null;
|
|
180
|
+
return val;
|
|
181
|
+
}
|
|
182
|
+
async function yn(message, initial = false) {
|
|
183
|
+
const val = await p.confirm({ message, initialValue: initial });
|
|
184
|
+
if (p.isCancel(val))
|
|
185
|
+
return null;
|
|
186
|
+
return val;
|
|
187
|
+
}
|
|
188
|
+
async function cmd(post, orgId, type, payload, label = 'Working...') {
|
|
189
|
+
const s = p.spinner();
|
|
190
|
+
s.start(label);
|
|
191
|
+
try {
|
|
192
|
+
const body = { type, payload };
|
|
193
|
+
if (orgId)
|
|
194
|
+
body.organization_id = orgId;
|
|
195
|
+
const res = await post('/api/v2/commands', body);
|
|
196
|
+
s.stop(pc.green('Done!'));
|
|
197
|
+
return res;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
s.stop(pc.red('Failed'));
|
|
201
|
+
p.log.error(err.message || 'Something went wrong');
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function cmdNoOrg(post, type, payload, label = 'Working...') {
|
|
206
|
+
const s = p.spinner();
|
|
207
|
+
s.start(label);
|
|
208
|
+
try {
|
|
209
|
+
const res = await post('/api/v2/commands', { type, payload });
|
|
210
|
+
s.stop(pc.green('Done!'));
|
|
211
|
+
return res;
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
s.stop(pc.red('Failed'));
|
|
215
|
+
p.log.error(err.message || 'Something went wrong');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function qry(post, orgId, type, payload = {}) {
|
|
220
|
+
try {
|
|
221
|
+
return await post('/api/v2/query', {
|
|
222
|
+
type,
|
|
223
|
+
organization_id: orgId,
|
|
224
|
+
payload: { ...payload, organization_id: orgId },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
p.log.error(err.message || 'Query failed');
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
148
232
|
// ── Setup Wizard ────────────────────────────────────────────────────
|
|
149
233
|
async function runSetup(existingConfig) {
|
|
150
234
|
p.intro(pc.bgCyan(pc.black(' Checkbox Setup ')));
|
|
@@ -236,7 +320,9 @@ async function runSetup(existingConfig) {
|
|
|
236
320
|
p.outro(pc.green('Setup complete! Run `checkbox` to get started.'));
|
|
237
321
|
return cfg;
|
|
238
322
|
}
|
|
239
|
-
//
|
|
323
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
324
|
+
// MAIN MENU
|
|
325
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
240
326
|
async function interactiveMenu(cfg) {
|
|
241
327
|
const { post } = api(cfg);
|
|
242
328
|
console.log();
|
|
@@ -247,14 +333,19 @@ async function interactiveMenu(cfg) {
|
|
|
247
333
|
const action = await p.select({
|
|
248
334
|
message: 'What would you like to do?',
|
|
249
335
|
options: [
|
|
250
|
-
{ value: '
|
|
251
|
-
{ value: '
|
|
252
|
-
{ value: '
|
|
253
|
-
{ value: '
|
|
254
|
-
{ value: '
|
|
255
|
-
{ value: '
|
|
256
|
-
{ value: '
|
|
257
|
-
{ value: '
|
|
336
|
+
{ value: 'tasks', label: 'My Tasks', hint: 'view, complete, create, and manage tasks' },
|
|
337
|
+
{ value: 'plans', label: 'Plans', hint: 'compliance plans, goals, rules, requirements' },
|
|
338
|
+
{ value: 'tables', label: 'Data Tables', hint: 'manage tables and records' },
|
|
339
|
+
{ value: 'documents', label: 'Documents', hint: 'files, folders, and attachments' },
|
|
340
|
+
{ value: 'assets', label: 'Assets', hint: 'photos, locations, checklists' },
|
|
341
|
+
{ value: 'automation', label: 'Rules & Automation', hint: 'validation rules, triggers' },
|
|
342
|
+
{ value: 'dashboards', label: 'Dashboards & Sharing', hint: 'dashboards, shares, reports' },
|
|
343
|
+
{ value: 'forms', label: 'Forms', hint: 'public data collection forms' },
|
|
344
|
+
{ value: 'approvals', label: 'Approvals', hint: 'review and manage approvals' },
|
|
345
|
+
{ value: 'team', label: 'Team', hint: 'invite members, manage roles' },
|
|
346
|
+
{ value: 'organization', label: 'Organization', hint: 'settings, branding, membership' },
|
|
347
|
+
{ value: 'overview', label: 'Workspace Overview', hint: 'search and status' },
|
|
348
|
+
{ value: 'settings', label: 'Settings', hint: 'API key, switch workspace' },
|
|
258
349
|
],
|
|
259
350
|
});
|
|
260
351
|
if (p.isCancel(action)) {
|
|
@@ -262,371 +353,1898 @@ async function interactiveMenu(cfg) {
|
|
|
262
353
|
return;
|
|
263
354
|
}
|
|
264
355
|
switch (action) {
|
|
265
|
-
case '
|
|
266
|
-
case '
|
|
267
|
-
case '
|
|
268
|
-
case '
|
|
269
|
-
case '
|
|
270
|
-
case '
|
|
271
|
-
case '
|
|
272
|
-
case '
|
|
356
|
+
case 'tasks': return mTasks(cfg, post);
|
|
357
|
+
case 'plans': return mPlans(cfg, post);
|
|
358
|
+
case 'tables': return mTables(cfg, post);
|
|
359
|
+
case 'documents': return mDocs(cfg, post);
|
|
360
|
+
case 'assets': return mAssets(cfg, post);
|
|
361
|
+
case 'automation': return mAutomation(cfg, post);
|
|
362
|
+
case 'dashboards': return mDashboards(cfg, post);
|
|
363
|
+
case 'forms': return mForms(cfg, post);
|
|
364
|
+
case 'approvals': return mApprovals(cfg, post);
|
|
365
|
+
case 'team': return mTeam(cfg, post);
|
|
366
|
+
case 'organization': return mOrg(cfg, post);
|
|
367
|
+
case 'overview': return mOverview(cfg, post);
|
|
368
|
+
case 'settings':
|
|
273
369
|
await runSetup(cfg);
|
|
274
370
|
return;
|
|
275
371
|
}
|
|
276
372
|
}
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
373
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
374
|
+
// TASKS — 9 commands + send_nudge + create_corrective_action + queries
|
|
375
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
376
|
+
async function mTasks(cfg, post) {
|
|
377
|
+
const a = await sel('My Tasks', [
|
|
378
|
+
{ value: 'today', label: "Today's tasks", hint: 'see what needs to be done' },
|
|
379
|
+
{ value: 'all', label: 'All tasks', hint: 'browse all tasks' },
|
|
380
|
+
{ value: 'complete', label: 'Complete a task' },
|
|
381
|
+
{ value: 'reopen', label: 'Reopen a completed task' },
|
|
382
|
+
{ value: 'create', label: 'Create a new task' },
|
|
383
|
+
{ value: 'edit', label: 'Edit a task' },
|
|
384
|
+
{ value: 'delete', label: 'Delete a task' },
|
|
385
|
+
{ value: 'validate', label: 'Submit proof', hint: 'photo, document, location, checklist' },
|
|
386
|
+
{ value: 'approval', label: 'Request approval for a task' },
|
|
387
|
+
{ value: 'answer', label: 'Answer a conditional question' },
|
|
388
|
+
{ value: 'nudge', label: 'Send a reminder', hint: 'nudge someone about a task' },
|
|
389
|
+
{ value: 'corrective', label: 'Corrective action', hint: 'for a missed task' },
|
|
390
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
391
|
+
]);
|
|
392
|
+
if (!a || a === '__back')
|
|
393
|
+
return interactiveMenu(cfg);
|
|
394
|
+
switch (a) {
|
|
395
|
+
case 'today': {
|
|
396
|
+
const s = p.spinner();
|
|
397
|
+
s.start('Loading...');
|
|
398
|
+
const today = new Date().toISOString().split('T')[0];
|
|
399
|
+
const res = await qry(post, cfg.orgId, 'get_task_states', { date: today });
|
|
400
|
+
const states = Array.isArray(res?.data) ? res.data : [];
|
|
401
|
+
s.stop(`${states.length} tasks today`);
|
|
402
|
+
if (states.length > 0) {
|
|
403
|
+
const lines = states.map((t) => {
|
|
404
|
+
const icon = t.status === 'completed' ? pc.green('\u2713') : t.status === 'missed' ? pc.red('\u2717') : pc.yellow('\u25CB');
|
|
405
|
+
return ` ${icon} ${pc.cyan(truncate(t.task_name || t.name || 'Untitled', 50))} ${pc.dim(t.frequency || '')}`;
|
|
406
|
+
});
|
|
407
|
+
p.note(lines.join('\n'), `Today (${today})`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
p.log.info('No tasks scheduled for today.');
|
|
411
|
+
}
|
|
412
|
+
return mTasks(cfg, post);
|
|
413
|
+
}
|
|
414
|
+
case 'all': {
|
|
415
|
+
const s = p.spinner();
|
|
416
|
+
s.start('Loading tasks...');
|
|
417
|
+
const tasks = await listEntities(post, cfg.orgId, 'task');
|
|
418
|
+
s.stop(`${tasks.length} tasks`);
|
|
419
|
+
if (tasks.length > 0) {
|
|
420
|
+
const lines = tasks.map((t) => ` ${pc.cyan(pad(truncate(t.name || 'Untitled', 50), 52))} ${pc.dim(t.metadata?.frequency || '')}`);
|
|
421
|
+
p.note(lines.join('\n'), 'All Tasks');
|
|
422
|
+
}
|
|
423
|
+
return mTasks(cfg, post);
|
|
424
|
+
}
|
|
425
|
+
case 'complete': {
|
|
426
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
427
|
+
if (t)
|
|
428
|
+
await cmd(post, cfg.orgId, 'complete_task', { task_id: t.id }, `Completing "${t.name}"...`);
|
|
429
|
+
return mTasks(cfg, post);
|
|
430
|
+
}
|
|
431
|
+
case 'reopen': {
|
|
432
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
433
|
+
if (t)
|
|
434
|
+
await cmd(post, cfg.orgId, 'uncomplete_task', { task_id: t.id }, `Reopening "${t.name}"...`);
|
|
435
|
+
return mTasks(cfg, post);
|
|
436
|
+
}
|
|
437
|
+
case 'create': {
|
|
438
|
+
const name = await txt('Task name', true, 'e.g. Daily safety walkthrough');
|
|
439
|
+
if (!name)
|
|
440
|
+
return mTasks(cfg, post);
|
|
441
|
+
const frequency = await sel('How often?', [
|
|
442
|
+
{ value: 'daily', label: 'Daily' },
|
|
443
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
444
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
445
|
+
{ value: 'yearly', label: 'Yearly' },
|
|
446
|
+
{ value: 'one-time', label: 'One time' },
|
|
447
|
+
]);
|
|
448
|
+
if (!frequency)
|
|
449
|
+
return mTasks(cfg, post);
|
|
450
|
+
const payload = { name, frequency };
|
|
451
|
+
const addPlan = await yn('Add to a plan?');
|
|
452
|
+
if (addPlan) {
|
|
453
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
454
|
+
if (plan)
|
|
455
|
+
payload.plan_id = plan.id;
|
|
456
|
+
}
|
|
457
|
+
const more = await yn('Add more details? (description, time, reminders, etc.)');
|
|
458
|
+
if (more) {
|
|
459
|
+
const desc = await txt('Description');
|
|
460
|
+
if (desc)
|
|
461
|
+
payload.description = desc;
|
|
462
|
+
const priv = await yn('Private task?');
|
|
463
|
+
if (priv)
|
|
464
|
+
payload.is_private = true;
|
|
465
|
+
const time = await txt('Scheduled time (HH:MM)', false, '09:00');
|
|
466
|
+
if (time)
|
|
467
|
+
payload.time = time;
|
|
468
|
+
const reminder = await sel('Reminder', [
|
|
469
|
+
{ value: 'none', label: 'No reminder' },
|
|
470
|
+
{ value: 'notification', label: 'Push notification' },
|
|
471
|
+
{ value: 'email', label: 'Email' },
|
|
472
|
+
]);
|
|
473
|
+
if (reminder && reminder !== 'none') {
|
|
474
|
+
payload.reminder_mode = reminder;
|
|
475
|
+
const mins = await txt('Minutes before', false, '30');
|
|
476
|
+
if (mins)
|
|
477
|
+
payload.reminder_minutes = parseInt(mins, 10);
|
|
478
|
+
}
|
|
479
|
+
const approval = await yn('Requires approval?');
|
|
480
|
+
if (approval)
|
|
481
|
+
payload.approval_required = true;
|
|
482
|
+
const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
|
|
483
|
+
if (cond)
|
|
484
|
+
payload.conditional_question = cond;
|
|
485
|
+
}
|
|
486
|
+
await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
|
|
487
|
+
return mTasks(cfg, post);
|
|
488
|
+
}
|
|
489
|
+
case 'edit': {
|
|
490
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
491
|
+
if (!t)
|
|
492
|
+
return mTasks(cfg, post);
|
|
493
|
+
p.log.info(`Editing: ${pc.cyan(t.name)} ${pc.dim('(leave blank to skip)')}`);
|
|
494
|
+
const updates = {};
|
|
495
|
+
const n = await txt('New name');
|
|
496
|
+
if (n === null)
|
|
497
|
+
return mTasks(cfg, post);
|
|
498
|
+
if (n)
|
|
499
|
+
updates.name = n;
|
|
500
|
+
const f = await sel('New frequency', [
|
|
501
|
+
{ value: '__skip', label: pc.dim('Keep current') },
|
|
502
|
+
{ value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
|
|
503
|
+
{ value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
|
|
504
|
+
{ value: 'one-time', label: 'One time' },
|
|
505
|
+
]);
|
|
506
|
+
if (f && f !== '__skip')
|
|
507
|
+
updates.frequency = f;
|
|
508
|
+
const d = await txt('New description');
|
|
509
|
+
if (d)
|
|
510
|
+
updates.description = d;
|
|
511
|
+
if (Object.keys(updates).length > 0) {
|
|
512
|
+
await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
|
|
513
|
+
}
|
|
514
|
+
else
|
|
515
|
+
p.log.info('No changes.');
|
|
516
|
+
return mTasks(cfg, post);
|
|
517
|
+
}
|
|
518
|
+
case 'delete': {
|
|
519
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
520
|
+
if (!t)
|
|
521
|
+
return mTasks(cfg, post);
|
|
522
|
+
const sure = await yn(`Delete "${t.name}"? This cannot be undone.`);
|
|
523
|
+
if (sure)
|
|
524
|
+
await cmd(post, cfg.orgId, 'delete_task', { task_id: t.id }, 'Deleting...');
|
|
525
|
+
return mTasks(cfg, post);
|
|
526
|
+
}
|
|
527
|
+
case 'validate': {
|
|
528
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
529
|
+
if (!t)
|
|
530
|
+
return mTasks(cfg, post);
|
|
531
|
+
const vtype = await sel('Proof type', [
|
|
532
|
+
{ value: 'photo', label: 'Photo' },
|
|
533
|
+
{ value: 'document', label: 'Document' },
|
|
534
|
+
{ value: 'geolocation', label: 'Location check-in' },
|
|
535
|
+
{ value: 'checklist', label: 'Checklist' },
|
|
536
|
+
{ value: 'table', label: 'Table record' },
|
|
537
|
+
]);
|
|
538
|
+
if (!vtype)
|
|
539
|
+
return mTasks(cfg, post);
|
|
540
|
+
let vid = null;
|
|
541
|
+
let data = {};
|
|
542
|
+
if (vtype === 'photo') {
|
|
543
|
+
const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
544
|
+
if (!ph)
|
|
545
|
+
return mTasks(cfg, post);
|
|
546
|
+
vid = ph.id;
|
|
547
|
+
const url = await txt('Photo URL', true);
|
|
548
|
+
if (!url)
|
|
549
|
+
return mTasks(cfg, post);
|
|
550
|
+
data = { url };
|
|
551
|
+
}
|
|
552
|
+
else if (vtype === 'document') {
|
|
553
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
|
|
554
|
+
if (!doc)
|
|
555
|
+
return mTasks(cfg, post);
|
|
556
|
+
vid = doc.id;
|
|
557
|
+
const url = await txt('Document URL', true);
|
|
558
|
+
if (!url)
|
|
559
|
+
return mTasks(cfg, post);
|
|
560
|
+
data = { url };
|
|
561
|
+
}
|
|
562
|
+
else if (vtype === 'geolocation') {
|
|
563
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
564
|
+
if (!area)
|
|
565
|
+
return mTasks(cfg, post);
|
|
566
|
+
vid = area.id;
|
|
567
|
+
const lat = await txt('Your latitude', true);
|
|
568
|
+
const lng = await txt('Your longitude', true);
|
|
569
|
+
if (!lat || !lng)
|
|
570
|
+
return mTasks(cfg, post);
|
|
571
|
+
data = { latitude: parseFloat(lat), longitude: parseFloat(lng) };
|
|
572
|
+
}
|
|
573
|
+
else if (vtype === 'checklist') {
|
|
574
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
575
|
+
if (!cl)
|
|
576
|
+
return mTasks(cfg, post);
|
|
577
|
+
vid = cl.id;
|
|
578
|
+
data = { completed: true };
|
|
579
|
+
}
|
|
580
|
+
else if (vtype === 'table') {
|
|
581
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
582
|
+
if (!tbl)
|
|
583
|
+
return mTasks(cfg, post);
|
|
584
|
+
vid = tbl.id;
|
|
585
|
+
const rid = await txt('Record ID', true);
|
|
586
|
+
if (!rid)
|
|
587
|
+
return mTasks(cfg, post);
|
|
588
|
+
data = { record_id: rid };
|
|
589
|
+
}
|
|
590
|
+
if (vid) {
|
|
591
|
+
await cmd(post, cfg.orgId, 'submit_validation', {
|
|
592
|
+
task_id: t.id, validation_type: vtype, validation_id: vid, data,
|
|
593
|
+
}, 'Submitting proof...');
|
|
594
|
+
}
|
|
595
|
+
return mTasks(cfg, post);
|
|
596
|
+
}
|
|
597
|
+
case 'approval': {
|
|
598
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
599
|
+
if (!t)
|
|
600
|
+
return mTasks(cfg, post);
|
|
601
|
+
const payload = { task_id: t.id };
|
|
602
|
+
const specify = await yn('Specify an approver?');
|
|
603
|
+
if (specify) {
|
|
604
|
+
const aid = await txt('Approver user ID', true);
|
|
605
|
+
if (aid)
|
|
606
|
+
payload.approver_id = aid;
|
|
607
|
+
}
|
|
608
|
+
await cmd(post, cfg.orgId, 'request_approval', payload, 'Requesting approval...');
|
|
609
|
+
return mTasks(cfg, post);
|
|
610
|
+
}
|
|
611
|
+
case 'answer': {
|
|
612
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
613
|
+
if (!t)
|
|
614
|
+
return mTasks(cfg, post);
|
|
615
|
+
const answer = await sel(`Answer for "${t.name}"`, [
|
|
616
|
+
{ value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' },
|
|
617
|
+
]);
|
|
618
|
+
if (answer)
|
|
619
|
+
await cmd(post, cfg.orgId, 'answer_conditional', { task_id: t.id, answer }, 'Submitting answer...');
|
|
620
|
+
return mTasks(cfg, post);
|
|
621
|
+
}
|
|
622
|
+
case 'nudge': {
|
|
623
|
+
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
624
|
+
if (!t)
|
|
625
|
+
return mTasks(cfg, post);
|
|
626
|
+
const rid = await txt('Who should get the reminder? (user ID)', true);
|
|
627
|
+
if (rid)
|
|
628
|
+
await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id: rid }, 'Sending reminder...');
|
|
629
|
+
return mTasks(cfg, post);
|
|
630
|
+
}
|
|
631
|
+
case 'corrective': {
|
|
632
|
+
const hid = await txt('Task history ID (from a missed task)', true);
|
|
633
|
+
if (!hid)
|
|
634
|
+
return mTasks(cfg, post);
|
|
635
|
+
const desc = await txt('What happened?', true);
|
|
636
|
+
if (!desc)
|
|
637
|
+
return mTasks(cfg, post);
|
|
638
|
+
const action = await txt('What was done to fix it?', true);
|
|
639
|
+
if (!action)
|
|
640
|
+
return mTasks(cfg, post);
|
|
641
|
+
await cmd(post, cfg.orgId, 'create_corrective_action', {
|
|
642
|
+
task_history_id: hid, description: desc, action_taken: action,
|
|
643
|
+
}, 'Recording corrective action...');
|
|
644
|
+
return mTasks(cfg, post);
|
|
307
645
|
}
|
|
308
|
-
return menuPlanTasks(cfg, post, selected, plans.find((pl) => pl.id === selected)?.name || '');
|
|
309
646
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
647
|
+
return mTasks(cfg, post);
|
|
648
|
+
}
|
|
649
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
650
|
+
// PLANS — plan + goal + rule + requirement CRUD (12 commands)
|
|
651
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
652
|
+
async function mPlans(cfg, post) {
|
|
653
|
+
const a = await sel('Plans', [
|
|
654
|
+
{ value: 'list', label: 'View plans' },
|
|
655
|
+
{ value: 'create', label: 'Create a plan' },
|
|
656
|
+
{ value: 'edit', label: 'Edit a plan' },
|
|
657
|
+
{ value: 'delete', label: 'Delete a plan' },
|
|
658
|
+
{ value: 'goals', label: 'Manage goals', hint: 'within a plan' },
|
|
659
|
+
{ value: 'rules', label: 'Manage rules', hint: 'within a goal' },
|
|
660
|
+
{ value: 'reqs', label: 'Manage requirements', hint: 'within a rule' },
|
|
661
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
662
|
+
]);
|
|
663
|
+
if (!a || a === '__back')
|
|
313
664
|
return interactiveMenu(cfg);
|
|
665
|
+
switch (a) {
|
|
666
|
+
case 'list': {
|
|
667
|
+
const s = p.spinner();
|
|
668
|
+
s.start('Loading plans...');
|
|
669
|
+
const plans = await listEntities(post, cfg.orgId, 'plan');
|
|
670
|
+
s.stop(`${plans.length} plans`);
|
|
671
|
+
if (plans.length > 0) {
|
|
672
|
+
const lines = plans.map((pl) => {
|
|
673
|
+
const progress = pl.metadata?.progress != null ? `${pl.metadata.progress}%` : '';
|
|
674
|
+
const status = pl.metadata?.status || '';
|
|
675
|
+
return ` ${pc.cyan(pad(truncate(pl.name || 'Untitled', 40), 42))} ${pc.dim(pad(status, 10))} ${pc.dim(progress)}`;
|
|
676
|
+
});
|
|
677
|
+
p.note(lines.join('\n'), 'Plans');
|
|
678
|
+
}
|
|
679
|
+
return mPlans(cfg, post);
|
|
680
|
+
}
|
|
681
|
+
case 'create': {
|
|
682
|
+
const name = await txt('Plan name', true);
|
|
683
|
+
if (!name)
|
|
684
|
+
return mPlans(cfg, post);
|
|
685
|
+
const payload = { name };
|
|
686
|
+
const desc = await txt('Description');
|
|
687
|
+
if (desc)
|
|
688
|
+
payload.description = desc;
|
|
689
|
+
const priv = await yn('Private plan?');
|
|
690
|
+
if (priv)
|
|
691
|
+
payload.is_private = true;
|
|
692
|
+
const addGoals = await yn('Add goals now?');
|
|
693
|
+
if (addGoals) {
|
|
694
|
+
const goals = [];
|
|
695
|
+
let more = true;
|
|
696
|
+
while (more) {
|
|
697
|
+
const gn = await txt('Goal name', true);
|
|
698
|
+
if (!gn)
|
|
699
|
+
break;
|
|
700
|
+
goals.push({ name: gn });
|
|
701
|
+
more = !!(await yn('Add another goal?'));
|
|
702
|
+
}
|
|
703
|
+
if (goals.length > 0)
|
|
704
|
+
payload.goals = goals;
|
|
705
|
+
}
|
|
706
|
+
await cmd(post, cfg.orgId, 'create_plan', payload, 'Creating plan...');
|
|
707
|
+
return mPlans(cfg, post);
|
|
708
|
+
}
|
|
709
|
+
case 'edit': {
|
|
710
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
711
|
+
if (!plan)
|
|
712
|
+
return mPlans(cfg, post);
|
|
713
|
+
p.log.info(`Editing: ${pc.cyan(plan.name)}`);
|
|
714
|
+
const updates = {};
|
|
715
|
+
const n = await txt('New name (blank to skip)');
|
|
716
|
+
if (n === null)
|
|
717
|
+
return mPlans(cfg, post);
|
|
718
|
+
if (n)
|
|
719
|
+
updates.name = n;
|
|
720
|
+
const d = await txt('New description (blank to skip)');
|
|
721
|
+
if (d)
|
|
722
|
+
updates.description = d;
|
|
723
|
+
const st = await sel('Status', [
|
|
724
|
+
{ value: '__skip', label: pc.dim('Keep current') },
|
|
725
|
+
{ value: 'active', label: 'Active' }, { value: 'draft', label: 'Draft' }, { value: 'archived', label: 'Archived' },
|
|
726
|
+
]);
|
|
727
|
+
if (st && st !== '__skip')
|
|
728
|
+
updates.status = st;
|
|
729
|
+
if (Object.keys(updates).length > 0) {
|
|
730
|
+
await cmd(post, cfg.orgId, 'update_plan', { plan_id: plan.id, updates }, 'Updating plan...');
|
|
731
|
+
}
|
|
732
|
+
else
|
|
733
|
+
p.log.info('No changes.');
|
|
734
|
+
return mPlans(cfg, post);
|
|
735
|
+
}
|
|
736
|
+
case 'delete': {
|
|
737
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
738
|
+
if (!plan)
|
|
739
|
+
return mPlans(cfg, post);
|
|
740
|
+
const sure = await yn(`Delete "${plan.name}" and everything inside it? This cannot be undone.`);
|
|
741
|
+
if (sure)
|
|
742
|
+
await cmd(post, cfg.orgId, 'delete_plan', { plan_id: plan.id }, 'Deleting plan...');
|
|
743
|
+
return mPlans(cfg, post);
|
|
744
|
+
}
|
|
745
|
+
case 'goals': return mGoals(cfg, post);
|
|
746
|
+
case 'rules': return mPlanRules(cfg, post);
|
|
747
|
+
case 'reqs': return mReqs(cfg, post);
|
|
314
748
|
}
|
|
749
|
+
return mPlans(cfg, post);
|
|
315
750
|
}
|
|
316
|
-
async function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
751
|
+
async function mGoals(cfg, post) {
|
|
752
|
+
const a = await sel('Goals', [
|
|
753
|
+
{ value: 'list', label: 'View goals in a plan' },
|
|
754
|
+
{ value: 'create', label: 'Add a goal to a plan' },
|
|
755
|
+
{ value: 'edit', label: 'Edit a goal' },
|
|
756
|
+
{ value: 'delete', label: 'Delete a goal' },
|
|
757
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
758
|
+
]);
|
|
759
|
+
if (!a || a === '__back')
|
|
760
|
+
return mPlans(cfg, post);
|
|
761
|
+
switch (a) {
|
|
762
|
+
case 'list': {
|
|
763
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
764
|
+
if (!plan)
|
|
765
|
+
return mGoals(cfg, post);
|
|
766
|
+
const s = p.spinner();
|
|
767
|
+
s.start('Loading goals...');
|
|
768
|
+
const goals = await listEntities(post, cfg.orgId, 'goal', { plan_id: plan.id });
|
|
769
|
+
s.stop(`${goals.length} goals in "${plan.name}"`);
|
|
770
|
+
if (goals.length > 0) {
|
|
771
|
+
const lines = goals.map((g) => ` ${pc.cyan(g.name || 'Untitled')}`);
|
|
772
|
+
p.note(lines.join('\n'), plan.name);
|
|
773
|
+
}
|
|
774
|
+
return mGoals(cfg, post);
|
|
775
|
+
}
|
|
776
|
+
case 'create': {
|
|
777
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
778
|
+
if (!plan)
|
|
779
|
+
return mGoals(cfg, post);
|
|
780
|
+
const name = await txt('Goal name', true);
|
|
781
|
+
if (!name)
|
|
782
|
+
return mGoals(cfg, post);
|
|
783
|
+
await cmd(post, cfg.orgId, 'create_goal', { plan_id: plan.id, name }, 'Creating goal...');
|
|
784
|
+
return mGoals(cfg, post);
|
|
785
|
+
}
|
|
786
|
+
case 'edit': {
|
|
787
|
+
const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals');
|
|
788
|
+
if (!goal)
|
|
789
|
+
return mGoals(cfg, post);
|
|
790
|
+
const name = await txt('New name', true);
|
|
791
|
+
if (!name)
|
|
792
|
+
return mGoals(cfg, post);
|
|
793
|
+
await cmd(post, cfg.orgId, 'update_goal', { goal_id: goal.id, updates: { name } }, 'Updating goal...');
|
|
794
|
+
return mGoals(cfg, post);
|
|
795
|
+
}
|
|
796
|
+
case 'delete': {
|
|
797
|
+
const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals');
|
|
798
|
+
if (!goal)
|
|
799
|
+
return mGoals(cfg, post);
|
|
800
|
+
const sure = await yn(`Delete "${goal.name}"?`);
|
|
801
|
+
if (sure)
|
|
802
|
+
await cmd(post, cfg.orgId, 'delete_goal', { goal_id: goal.id }, 'Deleting goal...');
|
|
803
|
+
return mGoals(cfg, post);
|
|
804
|
+
}
|
|
342
805
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
806
|
+
return mGoals(cfg, post);
|
|
807
|
+
}
|
|
808
|
+
async function mPlanRules(cfg, post) {
|
|
809
|
+
const a = await sel('Rules (within goals)', [
|
|
810
|
+
{ value: 'list', label: 'View rules in a goal' },
|
|
811
|
+
{ value: 'create', label: 'Add a rule to a goal' },
|
|
812
|
+
{ value: 'edit', label: 'Edit a rule' },
|
|
813
|
+
{ value: 'delete', label: 'Delete a rule' },
|
|
814
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
815
|
+
]);
|
|
816
|
+
if (!a || a === '__back')
|
|
817
|
+
return mPlans(cfg, post);
|
|
818
|
+
switch (a) {
|
|
819
|
+
case 'list': {
|
|
820
|
+
const s = p.spinner();
|
|
821
|
+
s.start('Loading rules...');
|
|
822
|
+
const rules = await listEntities(post, cfg.orgId, 'rule');
|
|
823
|
+
s.stop(`${rules.length} rules`);
|
|
824
|
+
if (rules.length > 0) {
|
|
825
|
+
const lines = rules.slice(0, 50).map((r) => ` ${pc.cyan(truncate(r.name || 'Untitled', 60))}`);
|
|
826
|
+
p.note(lines.join('\n'), 'Rules');
|
|
827
|
+
}
|
|
828
|
+
return mPlanRules(cfg, post);
|
|
829
|
+
}
|
|
830
|
+
case 'create': {
|
|
831
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
832
|
+
if (!plan)
|
|
833
|
+
return mPlanRules(cfg, post);
|
|
834
|
+
const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals', { plan_id: plan.id });
|
|
835
|
+
if (!goal)
|
|
836
|
+
return mPlanRules(cfg, post);
|
|
837
|
+
const name = await txt('Rule name', true);
|
|
838
|
+
if (!name)
|
|
839
|
+
return mPlanRules(cfg, post);
|
|
840
|
+
await cmd(post, cfg.orgId, 'create_rule', { plan_id: plan.id, goal_id: goal.id, name }, 'Creating rule...');
|
|
841
|
+
return mPlanRules(cfg, post);
|
|
842
|
+
}
|
|
843
|
+
case 'edit': {
|
|
844
|
+
const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules');
|
|
845
|
+
if (!rule)
|
|
846
|
+
return mPlanRules(cfg, post);
|
|
847
|
+
const name = await txt('New name', true);
|
|
848
|
+
if (!name)
|
|
849
|
+
return mPlanRules(cfg, post);
|
|
850
|
+
await cmd(post, cfg.orgId, 'update_rule', { rule_id: rule.id, updates: { name } }, 'Updating rule...');
|
|
851
|
+
return mPlanRules(cfg, post);
|
|
852
|
+
}
|
|
853
|
+
case 'delete': {
|
|
854
|
+
const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules');
|
|
855
|
+
if (!rule)
|
|
856
|
+
return mPlanRules(cfg, post);
|
|
857
|
+
const sure = await yn(`Delete "${rule.name}"?`);
|
|
858
|
+
if (sure)
|
|
859
|
+
await cmd(post, cfg.orgId, 'delete_rule', { rule_id: rule.id }, 'Deleting rule...');
|
|
860
|
+
return mPlanRules(cfg, post);
|
|
861
|
+
}
|
|
347
862
|
}
|
|
863
|
+
return mPlanRules(cfg, post);
|
|
348
864
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
865
|
+
async function mReqs(cfg, post) {
|
|
866
|
+
const a = await sel('Requirements', [
|
|
867
|
+
{ value: 'list', label: 'View requirements' },
|
|
868
|
+
{ value: 'create', label: 'Add a requirement' },
|
|
869
|
+
{ value: 'edit', label: 'Edit a requirement' },
|
|
870
|
+
{ value: 'delete', label: 'Delete a requirement' },
|
|
871
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
872
|
+
]);
|
|
873
|
+
if (!a || a === '__back')
|
|
874
|
+
return mPlans(cfg, post);
|
|
875
|
+
switch (a) {
|
|
876
|
+
case 'list': {
|
|
877
|
+
const s = p.spinner();
|
|
878
|
+
s.start('Loading requirements...');
|
|
879
|
+
const reqs = await listEntities(post, cfg.orgId, 'requirement');
|
|
880
|
+
s.stop(`${reqs.length} requirements`);
|
|
881
|
+
if (reqs.length > 0) {
|
|
882
|
+
const lines = reqs.slice(0, 50).map((r) => ` ${pc.cyan(truncate(r.name || 'Untitled', 60))}`);
|
|
883
|
+
p.note(lines.join('\n'), 'Requirements');
|
|
884
|
+
}
|
|
885
|
+
return mReqs(cfg, post);
|
|
359
886
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
887
|
+
case 'create': {
|
|
888
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
889
|
+
if (!plan)
|
|
890
|
+
return mReqs(cfg, post);
|
|
891
|
+
const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules', { plan_id: plan.id });
|
|
892
|
+
if (!rule)
|
|
893
|
+
return mReqs(cfg, post);
|
|
894
|
+
const name = await txt('Requirement name', true);
|
|
895
|
+
if (!name)
|
|
896
|
+
return mReqs(cfg, post);
|
|
897
|
+
const payload = { plan_id: plan.id, rule_id: rule.id, name };
|
|
898
|
+
const desc = await txt('Description');
|
|
899
|
+
if (desc)
|
|
900
|
+
payload.description = desc;
|
|
901
|
+
await cmd(post, cfg.orgId, 'create_requirement', payload, 'Creating requirement...');
|
|
902
|
+
return mReqs(cfg, post);
|
|
374
903
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
],
|
|
391
|
-
});
|
|
392
|
-
if (action === 'complete' && !p.isCancel(action)) {
|
|
393
|
-
await doComplete(cfg, post, task.id, task.name);
|
|
904
|
+
case 'edit': {
|
|
905
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
906
|
+
if (!req)
|
|
907
|
+
return mReqs(cfg, post);
|
|
908
|
+
const updates = {};
|
|
909
|
+
const n = await txt('New name (blank to skip)');
|
|
910
|
+
if (n === null)
|
|
911
|
+
return mReqs(cfg, post);
|
|
912
|
+
if (n)
|
|
913
|
+
updates.name = n;
|
|
914
|
+
const d = await txt('New description (blank to skip)');
|
|
915
|
+
if (d)
|
|
916
|
+
updates.description = d;
|
|
917
|
+
if (Object.keys(updates).length > 0) {
|
|
918
|
+
await cmd(post, cfg.orgId, 'update_requirement', { requirement_id: req.id, updates }, 'Updating...');
|
|
394
919
|
}
|
|
920
|
+
return mReqs(cfg, post);
|
|
921
|
+
}
|
|
922
|
+
case 'delete': {
|
|
923
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
924
|
+
if (!req)
|
|
925
|
+
return mReqs(cfg, post);
|
|
926
|
+
const sure = await yn(`Delete "${req.name}"?`);
|
|
927
|
+
if (sure)
|
|
928
|
+
await cmd(post, cfg.orgId, 'delete_requirement', { requirement_id: req.id }, 'Deleting...');
|
|
929
|
+
return mReqs(cfg, post);
|
|
395
930
|
}
|
|
396
|
-
return interactiveMenu(cfg);
|
|
397
931
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
932
|
+
return mReqs(cfg, post);
|
|
933
|
+
}
|
|
934
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
935
|
+
// DATA TABLES — 7 commands
|
|
936
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
937
|
+
async function mTables(cfg, post) {
|
|
938
|
+
const a = await sel('Data Tables', [
|
|
939
|
+
{ value: 'list', label: 'View tables' },
|
|
940
|
+
{ value: 'create', label: 'Create a table' },
|
|
941
|
+
{ value: 'edit', label: 'Edit a table' },
|
|
942
|
+
{ value: 'delete', label: 'Delete a table' },
|
|
943
|
+
{ value: 'add-record', label: 'Add a record', hint: 'add data to a table' },
|
|
944
|
+
{ value: 'edit-record', label: 'Edit a record' },
|
|
945
|
+
{ value: 'delete-record', label: 'Delete a record' },
|
|
946
|
+
{ value: 'bulk-delete', label: 'Bulk delete records' },
|
|
947
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
948
|
+
]);
|
|
949
|
+
if (!a || a === '__back')
|
|
401
950
|
return interactiveMenu(cfg);
|
|
951
|
+
switch (a) {
|
|
952
|
+
case 'list': {
|
|
953
|
+
const s = p.spinner();
|
|
954
|
+
s.start('Loading tables...');
|
|
955
|
+
const tables = await listEntities(post, cfg.orgId, 'table');
|
|
956
|
+
s.stop(`${tables.length} tables`);
|
|
957
|
+
if (tables.length > 0) {
|
|
958
|
+
const lines = tables.map((t) => ` ${pc.cyan(truncate(t.name || 'Untitled', 50))} ${pc.dim(t.id)}`);
|
|
959
|
+
p.note(lines.join('\n'), 'Tables');
|
|
960
|
+
}
|
|
961
|
+
return mTables(cfg, post);
|
|
962
|
+
}
|
|
963
|
+
case 'create': {
|
|
964
|
+
const name = await txt('Table name', true);
|
|
965
|
+
if (!name)
|
|
966
|
+
return mTables(cfg, post);
|
|
967
|
+
const desc = await txt('Description');
|
|
968
|
+
const payload = { name };
|
|
969
|
+
if (desc)
|
|
970
|
+
payload.description = desc;
|
|
971
|
+
// Build schema interactively
|
|
972
|
+
const fields = [];
|
|
973
|
+
p.log.info('Define your table columns:');
|
|
974
|
+
let addMore = true;
|
|
975
|
+
while (addMore) {
|
|
976
|
+
const fn = await txt('Column name', true);
|
|
977
|
+
if (!fn)
|
|
978
|
+
break;
|
|
979
|
+
const ft = await sel('Column type', [
|
|
980
|
+
{ value: 'text', label: 'Text' }, { value: 'number', label: 'Number' },
|
|
981
|
+
{ value: 'date', label: 'Date' }, { value: 'select', label: 'Dropdown' },
|
|
982
|
+
{ value: 'checkbox', label: 'Checkbox' }, { value: 'file', label: 'File' },
|
|
983
|
+
]);
|
|
984
|
+
if (!ft)
|
|
985
|
+
break;
|
|
986
|
+
const req = await yn('Required?');
|
|
987
|
+
fields.push({ name: fn, type: ft, ...(req ? { required: true } : {}) });
|
|
988
|
+
addMore = !!(await yn('Add another column?'));
|
|
989
|
+
}
|
|
990
|
+
if (fields.length > 0)
|
|
991
|
+
payload.schema = fields;
|
|
992
|
+
await cmd(post, cfg.orgId, 'create_table', payload, 'Creating table...');
|
|
993
|
+
return mTables(cfg, post);
|
|
994
|
+
}
|
|
995
|
+
case 'edit': {
|
|
996
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
997
|
+
if (!table)
|
|
998
|
+
return mTables(cfg, post);
|
|
999
|
+
const updates = {};
|
|
1000
|
+
const n = await txt('New name (blank to skip)');
|
|
1001
|
+
if (n === null)
|
|
1002
|
+
return mTables(cfg, post);
|
|
1003
|
+
if (n)
|
|
1004
|
+
updates.name = n;
|
|
1005
|
+
const d = await txt('New description (blank to skip)');
|
|
1006
|
+
if (d)
|
|
1007
|
+
updates.description = d;
|
|
1008
|
+
if (Object.keys(updates).length > 0) {
|
|
1009
|
+
await cmd(post, cfg.orgId, 'update_table', { table_id: table.id, updates }, 'Updating table...');
|
|
1010
|
+
}
|
|
1011
|
+
return mTables(cfg, post);
|
|
1012
|
+
}
|
|
1013
|
+
case 'delete': {
|
|
1014
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1015
|
+
if (!table)
|
|
1016
|
+
return mTables(cfg, post);
|
|
1017
|
+
const sure = await yn(`Delete "${table.name}" and all its data? This cannot be undone.`);
|
|
1018
|
+
if (sure)
|
|
1019
|
+
await cmd(post, cfg.orgId, 'delete_table', { table_id: table.id }, 'Deleting table...');
|
|
1020
|
+
return mTables(cfg, post);
|
|
1021
|
+
}
|
|
1022
|
+
case 'add-record': {
|
|
1023
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1024
|
+
if (!table)
|
|
1025
|
+
return mTables(cfg, post);
|
|
1026
|
+
p.log.info(`Adding record to: ${pc.cyan(table.name)}`);
|
|
1027
|
+
p.log.info(pc.dim('Enter field values. Leave blank to skip optional fields.'));
|
|
1028
|
+
const data = {};
|
|
1029
|
+
// Get schema fields from metadata
|
|
1030
|
+
const schema = table.metadata?.schema || [];
|
|
1031
|
+
if (Array.isArray(schema) && schema.length > 0) {
|
|
1032
|
+
for (const field of schema) {
|
|
1033
|
+
const val = await txt(`${field.name || field.label || 'Field'}`, !!field.required);
|
|
1034
|
+
if (val === null)
|
|
1035
|
+
return mTables(cfg, post);
|
|
1036
|
+
if (val)
|
|
1037
|
+
data[field.name || field.label] = val;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
p.log.info('Could not load table schema. Enter field name/value pairs:');
|
|
1042
|
+
let addMore = true;
|
|
1043
|
+
while (addMore) {
|
|
1044
|
+
const fn = await txt('Field name', true);
|
|
1045
|
+
if (!fn)
|
|
1046
|
+
break;
|
|
1047
|
+
const fv = await txt('Value', true);
|
|
1048
|
+
if (!fv)
|
|
1049
|
+
break;
|
|
1050
|
+
data[fn] = fv;
|
|
1051
|
+
addMore = !!(await yn('Add another field?'));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (Object.keys(data).length > 0) {
|
|
1055
|
+
await cmd(post, cfg.orgId, 'create_table_record', { table_id: table.id, data }, 'Adding record...');
|
|
1056
|
+
}
|
|
1057
|
+
return mTables(cfg, post);
|
|
1058
|
+
}
|
|
1059
|
+
case 'edit-record': {
|
|
1060
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1061
|
+
if (!table)
|
|
1062
|
+
return mTables(cfg, post);
|
|
1063
|
+
const record = await pickEntity(post, cfg.orgId, 'table_record', 'records', { table_id: table.id });
|
|
1064
|
+
if (!record)
|
|
1065
|
+
return mTables(cfg, post);
|
|
1066
|
+
const updates = {};
|
|
1067
|
+
let addMore = true;
|
|
1068
|
+
while (addMore) {
|
|
1069
|
+
const fn = await txt('Field to update');
|
|
1070
|
+
if (!fn)
|
|
1071
|
+
break;
|
|
1072
|
+
const fv = await txt('New value', true);
|
|
1073
|
+
if (!fv)
|
|
1074
|
+
break;
|
|
1075
|
+
updates[fn] = fv;
|
|
1076
|
+
addMore = !!(await yn('Update another field?'));
|
|
1077
|
+
}
|
|
1078
|
+
if (Object.keys(updates).length > 0) {
|
|
1079
|
+
await cmd(post, cfg.orgId, 'update_table_record', { record_id: record.id, updates }, 'Updating record...');
|
|
1080
|
+
}
|
|
1081
|
+
return mTables(cfg, post);
|
|
1082
|
+
}
|
|
1083
|
+
case 'delete-record': {
|
|
1084
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1085
|
+
if (!table)
|
|
1086
|
+
return mTables(cfg, post);
|
|
1087
|
+
const record = await pickEntity(post, cfg.orgId, 'table_record', 'records', { table_id: table.id });
|
|
1088
|
+
if (!record)
|
|
1089
|
+
return mTables(cfg, post);
|
|
1090
|
+
const sure = await yn('Delete this record?');
|
|
1091
|
+
if (sure)
|
|
1092
|
+
await cmd(post, cfg.orgId, 'delete_table_record', { record_id: record.id }, 'Deleting record...');
|
|
1093
|
+
return mTables(cfg, post);
|
|
1094
|
+
}
|
|
1095
|
+
case 'bulk-delete': {
|
|
1096
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1097
|
+
if (!table)
|
|
1098
|
+
return mTables(cfg, post);
|
|
1099
|
+
const ids = await txt('Record IDs to delete (comma-separated)', true);
|
|
1100
|
+
if (!ids)
|
|
1101
|
+
return mTables(cfg, post);
|
|
1102
|
+
const recordIds = ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1103
|
+
const sure = await yn(`Delete ${recordIds.length} records? This cannot be undone.`);
|
|
1104
|
+
if (sure) {
|
|
1105
|
+
await cmd(post, cfg.orgId, 'bulk_delete_table_records', { table_id: table.id, record_ids: recordIds }, 'Deleting records...');
|
|
1106
|
+
}
|
|
1107
|
+
return mTables(cfg, post);
|
|
1108
|
+
}
|
|
402
1109
|
}
|
|
1110
|
+
return mTables(cfg, post);
|
|
403
1111
|
}
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
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);
|
|
435
|
-
}
|
|
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))
|
|
1112
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1113
|
+
// DOCUMENTS — 6 commands
|
|
1114
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1115
|
+
async function mDocs(cfg, post) {
|
|
1116
|
+
const a = await sel('Documents', [
|
|
1117
|
+
{ value: 'list', label: 'View documents' },
|
|
1118
|
+
{ value: 'create', label: 'Create a document or folder' },
|
|
1119
|
+
{ value: 'edit', label: 'Edit a document' },
|
|
1120
|
+
{ value: 'delete', label: 'Delete a document' },
|
|
1121
|
+
{ value: 'attach', label: 'Attach files', hint: 'upload files to a document' },
|
|
1122
|
+
{ value: 'edit-file', label: 'Edit a file attachment' },
|
|
1123
|
+
{ value: 'delete-file', label: 'Remove a file attachment' },
|
|
1124
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1125
|
+
]);
|
|
1126
|
+
if (!a || a === '__back')
|
|
447
1127
|
return interactiveMenu(cfg);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1128
|
+
switch (a) {
|
|
1129
|
+
case 'list': {
|
|
1130
|
+
const s = p.spinner();
|
|
1131
|
+
s.start('Loading documents...');
|
|
1132
|
+
const docs = await listEntities(post, cfg.orgId, 'document');
|
|
1133
|
+
s.stop(`${docs.length} documents`);
|
|
1134
|
+
if (docs.length > 0) {
|
|
1135
|
+
const lines = docs.map((d) => ` ${pc.cyan(truncate(d.name || 'Untitled', 50))} ${pc.dim(d.id)}`);
|
|
1136
|
+
p.note(lines.join('\n'), 'Documents');
|
|
1137
|
+
}
|
|
1138
|
+
return mDocs(cfg, post);
|
|
1139
|
+
}
|
|
1140
|
+
case 'create': {
|
|
1141
|
+
const name = await txt('Name', true);
|
|
1142
|
+
if (!name)
|
|
1143
|
+
return mDocs(cfg, post);
|
|
1144
|
+
const isFolder = await yn('Is this a folder?');
|
|
1145
|
+
const payload = { name };
|
|
1146
|
+
if (isFolder)
|
|
1147
|
+
payload.is_folder = true;
|
|
1148
|
+
const url = isFolder ? null : await txt('File URL (blank to skip)');
|
|
1149
|
+
if (url)
|
|
1150
|
+
payload.file_url = url;
|
|
1151
|
+
await cmd(post, cfg.orgId, 'create_document', payload, 'Creating...');
|
|
1152
|
+
return mDocs(cfg, post);
|
|
1153
|
+
}
|
|
1154
|
+
case 'edit': {
|
|
1155
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
|
|
1156
|
+
if (!doc)
|
|
1157
|
+
return mDocs(cfg, post);
|
|
1158
|
+
const updates = {};
|
|
1159
|
+
const n = await txt('New name (blank to skip)');
|
|
1160
|
+
if (n === null)
|
|
1161
|
+
return mDocs(cfg, post);
|
|
1162
|
+
if (n)
|
|
1163
|
+
updates.name = n;
|
|
1164
|
+
const url = await txt('New file URL (blank to skip)');
|
|
1165
|
+
if (url)
|
|
1166
|
+
updates.file_url = url;
|
|
1167
|
+
if (Object.keys(updates).length > 0) {
|
|
1168
|
+
await cmd(post, cfg.orgId, 'update_document', { document_id: doc.id, updates }, 'Updating...');
|
|
1169
|
+
}
|
|
1170
|
+
return mDocs(cfg, post);
|
|
1171
|
+
}
|
|
1172
|
+
case 'delete': {
|
|
1173
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
|
|
1174
|
+
if (!doc)
|
|
1175
|
+
return mDocs(cfg, post);
|
|
1176
|
+
const sure = await yn(`Delete "${doc.name}"?`);
|
|
1177
|
+
if (sure)
|
|
1178
|
+
await cmd(post, cfg.orgId, 'delete_document', { document_id: doc.id }, 'Deleting...');
|
|
1179
|
+
return mDocs(cfg, post);
|
|
1180
|
+
}
|
|
1181
|
+
case 'attach': {
|
|
1182
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
|
|
1183
|
+
if (!doc)
|
|
1184
|
+
return mDocs(cfg, post);
|
|
1185
|
+
const entries = [];
|
|
1186
|
+
let addMore = true;
|
|
1187
|
+
while (addMore) {
|
|
1188
|
+
const url = await txt('File URL', true);
|
|
1189
|
+
if (!url)
|
|
1190
|
+
break;
|
|
1191
|
+
const name = await txt('File name (blank for auto)');
|
|
1192
|
+
entries.push({ document_url: url, ...(name ? { name } : {}) });
|
|
1193
|
+
addMore = !!(await yn('Attach another file?'));
|
|
1194
|
+
}
|
|
1195
|
+
if (entries.length > 0) {
|
|
1196
|
+
await cmd(post, cfg.orgId, 'create_document_completion', { document_id: doc.id, entries }, 'Attaching files...');
|
|
1197
|
+
}
|
|
1198
|
+
return mDocs(cfg, post);
|
|
1199
|
+
}
|
|
1200
|
+
case 'edit-file': {
|
|
1201
|
+
const comp = await pickEntity(post, cfg.orgId, 'document_completion', 'file attachments');
|
|
1202
|
+
if (!comp)
|
|
1203
|
+
return mDocs(cfg, post);
|
|
1204
|
+
const updates = {};
|
|
1205
|
+
const n = await txt('New file name (blank to skip)');
|
|
1206
|
+
if (n)
|
|
1207
|
+
updates.name = n;
|
|
1208
|
+
if (Object.keys(updates).length > 0) {
|
|
1209
|
+
await cmd(post, cfg.orgId, 'update_document_completion', { completion_id: comp.id, updates }, 'Updating...');
|
|
1210
|
+
}
|
|
1211
|
+
return mDocs(cfg, post);
|
|
455
1212
|
}
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1213
|
+
case 'delete-file': {
|
|
1214
|
+
const comp = await pickEntity(post, cfg.orgId, 'document_completion', 'file attachments');
|
|
1215
|
+
if (!comp)
|
|
1216
|
+
return mDocs(cfg, post);
|
|
1217
|
+
const sure = await yn('Remove this file attachment?');
|
|
1218
|
+
if (sure)
|
|
1219
|
+
await cmd(post, cfg.orgId, 'delete_document_completion', { completion_id: comp.id }, 'Removing...');
|
|
1220
|
+
return mDocs(cfg, post);
|
|
463
1221
|
}
|
|
464
1222
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
1223
|
+
return mDocs(cfg, post);
|
|
1224
|
+
}
|
|
1225
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1226
|
+
// ASSETS — photos (3) + geolocation (3) + checklists (3) = 9 commands
|
|
1227
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1228
|
+
async function mAssets(cfg, post) {
|
|
1229
|
+
const a = await sel('Assets', [
|
|
1230
|
+
{ value: 'photos', label: 'Photos', hint: 'photo validation assets' },
|
|
1231
|
+
{ value: 'locations', label: 'Locations', hint: 'geofenced areas for check-ins' },
|
|
1232
|
+
{ value: 'checklists', label: 'Checklists', hint: 'checklist templates' },
|
|
1233
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1234
|
+
]);
|
|
1235
|
+
if (!a || a === '__back')
|
|
1236
|
+
return interactiveMenu(cfg);
|
|
1237
|
+
switch (a) {
|
|
1238
|
+
case 'photos': return mPhotos(cfg, post);
|
|
1239
|
+
case 'locations': return mGeo(cfg, post);
|
|
1240
|
+
case 'checklists': return mChecklists(cfg, post);
|
|
468
1241
|
}
|
|
469
|
-
return
|
|
1242
|
+
return mAssets(cfg, post);
|
|
470
1243
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1244
|
+
async function mPhotos(cfg, post) {
|
|
1245
|
+
const a = await sel('Photos', [
|
|
1246
|
+
{ value: 'list', label: 'View photo assets' },
|
|
1247
|
+
{ value: 'create', label: 'Create a photo asset' },
|
|
1248
|
+
{ value: 'edit', label: 'Edit a photo asset' },
|
|
1249
|
+
{ value: 'delete', label: 'Delete a photo asset' },
|
|
1250
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
1251
|
+
]);
|
|
1252
|
+
if (!a || a === '__back')
|
|
1253
|
+
return mAssets(cfg, post);
|
|
1254
|
+
switch (a) {
|
|
1255
|
+
case 'list': {
|
|
1256
|
+
const s = p.spinner();
|
|
1257
|
+
s.start('Loading...');
|
|
1258
|
+
const items = await listEntities(post, cfg.orgId, 'photo');
|
|
1259
|
+
s.stop(`${items.length} photo assets`);
|
|
1260
|
+
if (items.length > 0) {
|
|
1261
|
+
const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
|
|
1262
|
+
p.note(lines.join('\n'), 'Photos');
|
|
1263
|
+
}
|
|
1264
|
+
return mPhotos(cfg, post);
|
|
481
1265
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
1266
|
+
case 'create': {
|
|
1267
|
+
const name = await txt('Photo asset name', true);
|
|
1268
|
+
if (!name)
|
|
1269
|
+
return mPhotos(cfg, post);
|
|
1270
|
+
const desc = await txt('Description');
|
|
1271
|
+
const payload = { name };
|
|
1272
|
+
if (desc)
|
|
1273
|
+
payload.description = desc;
|
|
1274
|
+
await cmd(post, cfg.orgId, 'create_photo', payload, 'Creating...');
|
|
1275
|
+
return mPhotos(cfg, post);
|
|
1276
|
+
}
|
|
1277
|
+
case 'edit': {
|
|
1278
|
+
const photo = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
1279
|
+
if (!photo)
|
|
1280
|
+
return mPhotos(cfg, post);
|
|
1281
|
+
const updates = {};
|
|
1282
|
+
const n = await txt('New name (blank to skip)');
|
|
1283
|
+
if (n)
|
|
1284
|
+
updates.name = n;
|
|
1285
|
+
const d = await txt('New description (blank to skip)');
|
|
1286
|
+
if (d)
|
|
1287
|
+
updates.description = d;
|
|
1288
|
+
if (Object.keys(updates).length > 0) {
|
|
1289
|
+
await cmd(post, cfg.orgId, 'update_photo', { photo_id: photo.id, updates }, 'Updating...');
|
|
1290
|
+
}
|
|
1291
|
+
return mPhotos(cfg, post);
|
|
1292
|
+
}
|
|
1293
|
+
case 'delete': {
|
|
1294
|
+
const photo = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
1295
|
+
if (!photo)
|
|
1296
|
+
return mPhotos(cfg, post);
|
|
1297
|
+
const sure = await yn(`Delete "${photo.name}"?`);
|
|
1298
|
+
if (sure)
|
|
1299
|
+
await cmd(post, cfg.orgId, 'delete_photo', { photo_id: photo.id }, 'Deleting...');
|
|
1300
|
+
return mPhotos(cfg, post);
|
|
495
1301
|
}
|
|
496
|
-
const task = tasks.find((t) => t.id === selected);
|
|
497
|
-
await doComplete(cfg, post, selected, task?.name || '');
|
|
498
1302
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1303
|
+
return mPhotos(cfg, post);
|
|
1304
|
+
}
|
|
1305
|
+
async function mGeo(cfg, post) {
|
|
1306
|
+
const a = await sel('Locations', [
|
|
1307
|
+
{ value: 'list', label: 'View location areas' },
|
|
1308
|
+
{ value: 'create', label: 'Create a location area' },
|
|
1309
|
+
{ value: 'edit', label: 'Edit a location area' },
|
|
1310
|
+
{ value: 'delete', label: 'Delete a location area' },
|
|
1311
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
1312
|
+
]);
|
|
1313
|
+
if (!a || a === '__back')
|
|
1314
|
+
return mAssets(cfg, post);
|
|
1315
|
+
switch (a) {
|
|
1316
|
+
case 'list': {
|
|
1317
|
+
const s = p.spinner();
|
|
1318
|
+
s.start('Loading...');
|
|
1319
|
+
const items = await listEntities(post, cfg.orgId, 'geolocation_area');
|
|
1320
|
+
s.stop(`${items.length} location areas`);
|
|
1321
|
+
if (items.length > 0) {
|
|
1322
|
+
const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
|
|
1323
|
+
p.note(lines.join('\n'), 'Location Areas');
|
|
1324
|
+
}
|
|
1325
|
+
return mGeo(cfg, post);
|
|
1326
|
+
}
|
|
1327
|
+
case 'create': {
|
|
1328
|
+
const label = await txt('Area name', true, 'e.g. Main warehouse');
|
|
1329
|
+
if (!label)
|
|
1330
|
+
return mGeo(cfg, post);
|
|
1331
|
+
const lat = await txt('Latitude', true, 'e.g. 25.7617');
|
|
1332
|
+
if (!lat)
|
|
1333
|
+
return mGeo(cfg, post);
|
|
1334
|
+
const lng = await txt('Longitude', true, 'e.g. -80.1918');
|
|
1335
|
+
if (!lng)
|
|
1336
|
+
return mGeo(cfg, post);
|
|
1337
|
+
const radius = await txt('Radius in meters', true, '100');
|
|
1338
|
+
if (!radius)
|
|
1339
|
+
return mGeo(cfg, post);
|
|
1340
|
+
await cmd(post, cfg.orgId, 'create_geolocation_area', {
|
|
1341
|
+
label, latitude: parseFloat(lat), longitude: parseFloat(lng), radius: parseFloat(radius),
|
|
1342
|
+
}, 'Creating location area...');
|
|
1343
|
+
return mGeo(cfg, post);
|
|
1344
|
+
}
|
|
1345
|
+
case 'edit': {
|
|
1346
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
1347
|
+
if (!area)
|
|
1348
|
+
return mGeo(cfg, post);
|
|
1349
|
+
const updates = {};
|
|
1350
|
+
const n = await txt('New name (blank to skip)');
|
|
1351
|
+
if (n)
|
|
1352
|
+
updates.label = n;
|
|
1353
|
+
const lat = await txt('New latitude (blank to skip)');
|
|
1354
|
+
if (lat)
|
|
1355
|
+
updates.latitude = parseFloat(lat);
|
|
1356
|
+
const lng = await txt('New longitude (blank to skip)');
|
|
1357
|
+
if (lng)
|
|
1358
|
+
updates.longitude = parseFloat(lng);
|
|
1359
|
+
const r = await txt('New radius (blank to skip)');
|
|
1360
|
+
if (r)
|
|
1361
|
+
updates.radius = parseFloat(r);
|
|
1362
|
+
if (Object.keys(updates).length > 0) {
|
|
1363
|
+
await cmd(post, cfg.orgId, 'update_geolocation_area', { area_id: area.id, updates }, 'Updating...');
|
|
1364
|
+
}
|
|
1365
|
+
return mGeo(cfg, post);
|
|
1366
|
+
}
|
|
1367
|
+
case 'delete': {
|
|
1368
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
1369
|
+
if (!area)
|
|
1370
|
+
return mGeo(cfg, post);
|
|
1371
|
+
const sure = await yn(`Delete "${area.name}"?`);
|
|
1372
|
+
if (sure)
|
|
1373
|
+
await cmd(post, cfg.orgId, 'delete_geolocation_area', { area_id: area.id }, 'Deleting...');
|
|
1374
|
+
return mGeo(cfg, post);
|
|
1375
|
+
}
|
|
502
1376
|
}
|
|
503
|
-
return
|
|
1377
|
+
return mGeo(cfg, post);
|
|
504
1378
|
}
|
|
505
|
-
async function
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1379
|
+
async function mChecklists(cfg, post) {
|
|
1380
|
+
const a = await sel('Checklists', [
|
|
1381
|
+
{ value: 'list', label: 'View checklists' },
|
|
1382
|
+
{ value: 'create', label: 'Create a checklist' },
|
|
1383
|
+
{ value: 'edit', label: 'Edit a checklist' },
|
|
1384
|
+
{ value: 'delete', label: 'Delete a checklist' },
|
|
1385
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
1386
|
+
]);
|
|
1387
|
+
if (!a || a === '__back')
|
|
1388
|
+
return mAssets(cfg, post);
|
|
1389
|
+
switch (a) {
|
|
1390
|
+
case 'list': {
|
|
1391
|
+
const s = p.spinner();
|
|
1392
|
+
s.start('Loading...');
|
|
1393
|
+
const items = await listEntities(post, cfg.orgId, 'checklist');
|
|
1394
|
+
s.stop(`${items.length} checklists`);
|
|
1395
|
+
if (items.length > 0) {
|
|
1396
|
+
const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
|
|
1397
|
+
p.note(lines.join('\n'), 'Checklists');
|
|
1398
|
+
}
|
|
1399
|
+
return mChecklists(cfg, post);
|
|
1400
|
+
}
|
|
1401
|
+
case 'create': {
|
|
1402
|
+
const name = await txt('Checklist name', true);
|
|
1403
|
+
if (!name)
|
|
1404
|
+
return mChecklists(cfg, post);
|
|
1405
|
+
const desc = await txt('Description');
|
|
1406
|
+
const items = [];
|
|
1407
|
+
p.log.info('Add checklist items:');
|
|
1408
|
+
let addMore = true;
|
|
1409
|
+
while (addMore) {
|
|
1410
|
+
const item = await txt('Item', true);
|
|
1411
|
+
if (!item)
|
|
1412
|
+
break;
|
|
1413
|
+
items.push(item);
|
|
1414
|
+
addMore = !!(await yn('Add another item?'));
|
|
1415
|
+
}
|
|
1416
|
+
const payload = { name, items };
|
|
1417
|
+
if (desc)
|
|
1418
|
+
payload.description = desc;
|
|
1419
|
+
await cmd(post, cfg.orgId, 'create_checklist', payload, 'Creating checklist...');
|
|
1420
|
+
return mChecklists(cfg, post);
|
|
1421
|
+
}
|
|
1422
|
+
case 'edit': {
|
|
1423
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
1424
|
+
if (!cl)
|
|
1425
|
+
return mChecklists(cfg, post);
|
|
1426
|
+
const updates = {};
|
|
1427
|
+
const n = await txt('New name (blank to skip)');
|
|
1428
|
+
if (n)
|
|
1429
|
+
updates.name = n;
|
|
1430
|
+
const d = await txt('New description (blank to skip)');
|
|
1431
|
+
if (d)
|
|
1432
|
+
updates.description = d;
|
|
1433
|
+
if (Object.keys(updates).length > 0) {
|
|
1434
|
+
await cmd(post, cfg.orgId, 'update_checklist', { checklist_id: cl.id, updates }, 'Updating...');
|
|
1435
|
+
}
|
|
1436
|
+
return mChecklists(cfg, post);
|
|
1437
|
+
}
|
|
1438
|
+
case 'delete': {
|
|
1439
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
1440
|
+
if (!cl)
|
|
1441
|
+
return mChecklists(cfg, post);
|
|
1442
|
+
const sure = await yn(`Delete "${cl.name}"?`);
|
|
1443
|
+
if (sure)
|
|
1444
|
+
await cmd(post, cfg.orgId, 'delete_checklist', { checklist_id: cl.id }, 'Deleting...');
|
|
1445
|
+
return mChecklists(cfg, post);
|
|
1446
|
+
}
|
|
515
1447
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1448
|
+
return mChecklists(cfg, post);
|
|
1449
|
+
}
|
|
1450
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1451
|
+
// RULES & AUTOMATION — validation rules (4) + trigger rules (3)
|
|
1452
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1453
|
+
async function mAutomation(cfg, post) {
|
|
1454
|
+
const a = await sel('Rules & Automation', [
|
|
1455
|
+
{ value: 'val-list', label: 'View validation rules' },
|
|
1456
|
+
{ value: 'val-create', label: 'Create a validation rule', hint: 'require proof for a task' },
|
|
1457
|
+
{ value: 'val-edit', label: 'Edit a validation rule' },
|
|
1458
|
+
{ value: 'val-batch', label: 'Reorder validation rules' },
|
|
1459
|
+
{ value: 'val-delete', label: 'Delete a validation rule' },
|
|
1460
|
+
{ value: 'trig-list', label: 'View trigger rules' },
|
|
1461
|
+
{ value: 'trig-create', label: 'Create a trigger rule', hint: 'automate task activation' },
|
|
1462
|
+
{ value: 'trig-edit', label: 'Edit a trigger rule' },
|
|
1463
|
+
{ value: 'trig-delete', label: 'Delete a trigger rule' },
|
|
1464
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1465
|
+
]);
|
|
1466
|
+
if (!a || a === '__back')
|
|
1467
|
+
return interactiveMenu(cfg);
|
|
1468
|
+
switch (a) {
|
|
1469
|
+
case 'val-list': {
|
|
1470
|
+
const s = p.spinner();
|
|
1471
|
+
s.start('Loading...');
|
|
1472
|
+
const items = await listEntities(post, cfg.orgId, 'validation_rule');
|
|
1473
|
+
s.stop(`${items.length} validation rules`);
|
|
1474
|
+
if (items.length > 0) {
|
|
1475
|
+
const lines = items.map((i) => ` ${pc.cyan(truncate(i.name || 'Untitled', 50))} ${pc.dim(i.metadata?.rule_type || '')}`);
|
|
1476
|
+
p.note(lines.join('\n'), 'Validation Rules');
|
|
1477
|
+
}
|
|
1478
|
+
return mAutomation(cfg, post);
|
|
1479
|
+
}
|
|
1480
|
+
case 'val-create': {
|
|
1481
|
+
const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
1482
|
+
if (!task)
|
|
1483
|
+
return mAutomation(cfg, post);
|
|
1484
|
+
const name = await txt('Rule name', true, 'e.g. Require photo proof');
|
|
1485
|
+
if (!name)
|
|
1486
|
+
return mAutomation(cfg, post);
|
|
1487
|
+
const ruleType = await sel('Rule type', [
|
|
1488
|
+
{ value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, etc.' },
|
|
1489
|
+
{ value: 'field_constraint', label: 'Field constraint', hint: 'validate a data field' },
|
|
1490
|
+
{ value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
|
|
1491
|
+
{ value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
|
|
1492
|
+
{ value: 'record_exists', label: 'Record exists', hint: 'verify data was entered' },
|
|
1493
|
+
{ value: 'cross_field', label: 'Cross-field validation' },
|
|
1494
|
+
{ value: 'aggregate', label: 'Aggregate check' },
|
|
1495
|
+
]);
|
|
1496
|
+
if (!ruleType)
|
|
1497
|
+
return mAutomation(cfg, post);
|
|
1498
|
+
const condition = {};
|
|
1499
|
+
if (ruleType === 'proof_required') {
|
|
1500
|
+
const proofType = await sel('What kind of proof?', [
|
|
1501
|
+
{ value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
|
|
1502
|
+
{ value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
|
|
1503
|
+
]);
|
|
1504
|
+
if (proofType)
|
|
1505
|
+
condition.proof_type = proofType;
|
|
1506
|
+
}
|
|
1507
|
+
else if (ruleType === 'time_window') {
|
|
1508
|
+
const start = await txt('Start time (HH:MM)', false, '08:00');
|
|
1509
|
+
const end = await txt('End time (HH:MM)', false, '17:00');
|
|
1510
|
+
if (start)
|
|
1511
|
+
condition.start_time = start;
|
|
1512
|
+
if (end)
|
|
1513
|
+
condition.end_time = end;
|
|
1514
|
+
}
|
|
1515
|
+
await cmd(post, cfg.orgId, 'create_validation_rule', {
|
|
1516
|
+
task_id: task.id, name, rule_type: ruleType, condition,
|
|
1517
|
+
}, 'Creating validation rule...');
|
|
1518
|
+
return mAutomation(cfg, post);
|
|
1519
|
+
}
|
|
1520
|
+
case 'val-edit': {
|
|
1521
|
+
const rule = await pickEntity(post, cfg.orgId, 'validation_rule', 'validation rules');
|
|
1522
|
+
if (!rule)
|
|
1523
|
+
return mAutomation(cfg, post);
|
|
1524
|
+
const updates = {};
|
|
1525
|
+
const n = await txt('New name (blank to skip)');
|
|
1526
|
+
if (n)
|
|
1527
|
+
updates.name = n;
|
|
1528
|
+
const enabled = await sel('Status', [
|
|
1529
|
+
{ value: '__skip', label: pc.dim('Keep current') },
|
|
1530
|
+
{ value: 'true', label: 'Enabled' }, { value: 'false', label: 'Disabled' },
|
|
1531
|
+
]);
|
|
1532
|
+
if (enabled && enabled !== '__skip')
|
|
1533
|
+
updates.enabled = enabled === 'true';
|
|
1534
|
+
if (Object.keys(updates).length > 0) {
|
|
1535
|
+
await cmd(post, cfg.orgId, 'update_validation_rule', { id: rule.id, updates }, 'Updating...');
|
|
1536
|
+
}
|
|
1537
|
+
return mAutomation(cfg, post);
|
|
1538
|
+
}
|
|
1539
|
+
case 'val-batch': {
|
|
1540
|
+
const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
1541
|
+
if (!task)
|
|
1542
|
+
return mAutomation(cfg, post);
|
|
1543
|
+
const s = p.spinner();
|
|
1544
|
+
s.start('Loading rules...');
|
|
1545
|
+
const rules = await listEntities(post, cfg.orgId, 'validation_rule', { task_id: task.id });
|
|
1546
|
+
s.stop(`${rules.length} rules`);
|
|
1547
|
+
if (rules.length === 0) {
|
|
1548
|
+
p.log.info('No validation rules for this task.');
|
|
1549
|
+
return mAutomation(cfg, post);
|
|
1550
|
+
}
|
|
1551
|
+
const batch = [];
|
|
1552
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1553
|
+
const r = rules[i];
|
|
1554
|
+
p.log.info(`${i + 1}. ${r.name || 'Untitled'}`);
|
|
1555
|
+
const pos = await txt(`New position for "${r.name}" (blank to keep ${i + 1})`);
|
|
1556
|
+
if (pos)
|
|
1557
|
+
batch.push({ id: r.id, updates: { position: parseInt(pos, 10) } });
|
|
1558
|
+
}
|
|
1559
|
+
if (batch.length > 0) {
|
|
1560
|
+
await cmd(post, cfg.orgId, 'batch_update_validation_rules', { batch }, 'Updating order...');
|
|
1561
|
+
}
|
|
1562
|
+
return mAutomation(cfg, post);
|
|
1563
|
+
}
|
|
1564
|
+
case 'val-delete': {
|
|
1565
|
+
const rule = await pickEntity(post, cfg.orgId, 'validation_rule', 'validation rules');
|
|
1566
|
+
if (!rule)
|
|
1567
|
+
return mAutomation(cfg, post);
|
|
1568
|
+
const sure = await yn(`Delete "${rule.name}"?`);
|
|
1569
|
+
if (sure)
|
|
1570
|
+
await cmd(post, cfg.orgId, 'delete_validation_rule', { id: rule.id }, 'Deleting...');
|
|
1571
|
+
return mAutomation(cfg, post);
|
|
1572
|
+
}
|
|
1573
|
+
case 'trig-list': {
|
|
1574
|
+
const s = p.spinner();
|
|
1575
|
+
s.start('Loading...');
|
|
1576
|
+
const items = await listEntities(post, cfg.orgId, 'trigger_rule');
|
|
1577
|
+
s.stop(`${items.length} trigger rules`);
|
|
1578
|
+
if (items.length > 0) {
|
|
1579
|
+
const lines = items.map((i) => ` ${pc.cyan(truncate(i.name || 'Untitled', 50))} ${pc.dim(i.metadata?.event_type || '')}`);
|
|
1580
|
+
p.note(lines.join('\n'), 'Trigger Rules');
|
|
1581
|
+
}
|
|
1582
|
+
return mAutomation(cfg, post);
|
|
1583
|
+
}
|
|
1584
|
+
case 'trig-create': {
|
|
1585
|
+
const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
1586
|
+
if (!task)
|
|
1587
|
+
return mAutomation(cfg, post);
|
|
1588
|
+
const name = await txt('Trigger name', true, 'e.g. Activate when record added');
|
|
1589
|
+
if (!name)
|
|
1590
|
+
return mAutomation(cfg, post);
|
|
1591
|
+
const eventType = await sel('What triggers this?', [
|
|
1592
|
+
{ value: 'record_created', label: 'New record added to a table' },
|
|
1593
|
+
{ value: 'record_updated', label: 'Record updated in a table' },
|
|
1594
|
+
{ value: 'task_completed', label: 'Another task completed' },
|
|
1595
|
+
{ value: 'task_missed', label: 'A task was missed' },
|
|
1596
|
+
{ value: 'approval_resolved', label: 'An approval was resolved' },
|
|
1597
|
+
]);
|
|
1598
|
+
if (!eventType)
|
|
1599
|
+
return mAutomation(cfg, post);
|
|
1600
|
+
const sourceType = await sel('Source type', [
|
|
1601
|
+
{ value: 'table', label: 'Table' }, { value: 'task', label: 'Task' },
|
|
1602
|
+
{ value: 'approval', label: 'Approval' },
|
|
1603
|
+
]);
|
|
1604
|
+
if (!sourceType)
|
|
1605
|
+
return mAutomation(cfg, post);
|
|
1606
|
+
const payload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
|
|
1607
|
+
const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
|
|
1608
|
+
if (source)
|
|
1609
|
+
payload.source_entity_id = source.id;
|
|
1610
|
+
await cmd(post, cfg.orgId, 'create_trigger_rule', payload, 'Creating trigger rule...');
|
|
1611
|
+
return mAutomation(cfg, post);
|
|
1612
|
+
}
|
|
1613
|
+
case 'trig-edit': {
|
|
1614
|
+
const rule = await pickEntity(post, cfg.orgId, 'trigger_rule', 'trigger rules');
|
|
1615
|
+
if (!rule)
|
|
1616
|
+
return mAutomation(cfg, post);
|
|
1617
|
+
const updates = {};
|
|
1618
|
+
const n = await txt('New name (blank to skip)');
|
|
1619
|
+
if (n)
|
|
1620
|
+
updates.name = n;
|
|
1621
|
+
const enabled = await sel('Status', [
|
|
1622
|
+
{ value: '__skip', label: pc.dim('Keep current') },
|
|
1623
|
+
{ value: 'true', label: 'Enabled' }, { value: 'false', label: 'Disabled' },
|
|
1624
|
+
]);
|
|
1625
|
+
if (enabled && enabled !== '__skip')
|
|
1626
|
+
updates.enabled = enabled === 'true';
|
|
1627
|
+
if (Object.keys(updates).length > 0) {
|
|
1628
|
+
await cmd(post, cfg.orgId, 'update_trigger_rule', { id: rule.id, updates }, 'Updating...');
|
|
1629
|
+
}
|
|
1630
|
+
return mAutomation(cfg, post);
|
|
1631
|
+
}
|
|
1632
|
+
case 'trig-delete': {
|
|
1633
|
+
const rule = await pickEntity(post, cfg.orgId, 'trigger_rule', 'trigger rules');
|
|
1634
|
+
if (!rule)
|
|
1635
|
+
return mAutomation(cfg, post);
|
|
1636
|
+
const sure = await yn(`Delete "${rule.name}"?`);
|
|
1637
|
+
if (sure)
|
|
1638
|
+
await cmd(post, cfg.orgId, 'delete_trigger_rule', { id: rule.id }, 'Deleting...');
|
|
1639
|
+
return mAutomation(cfg, post);
|
|
1640
|
+
}
|
|
519
1641
|
}
|
|
1642
|
+
return mAutomation(cfg, post);
|
|
520
1643
|
}
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1644
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1645
|
+
// DASHBOARDS & SHARING — dashboards (3) + shares (5) = 8 commands
|
|
1646
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1647
|
+
async function mDashboards(cfg, post) {
|
|
1648
|
+
const a = await sel('Dashboards & Sharing', [
|
|
1649
|
+
{ value: 'list', label: 'View dashboards' },
|
|
1650
|
+
{ value: 'create', label: 'Create a dashboard' },
|
|
1651
|
+
{ value: 'edit', label: 'Edit a dashboard' },
|
|
1652
|
+
{ value: 'delete', label: 'Delete a dashboard' },
|
|
1653
|
+
{ value: 'share', label: 'Share a dashboard', hint: 'public link, password, invite' },
|
|
1654
|
+
{ value: 'unshare', label: 'Stop sharing a dashboard' },
|
|
1655
|
+
{ value: 'invite', label: 'Invite people to a shared dashboard' },
|
|
1656
|
+
{ value: 'uninvite', label: 'Remove an invite' },
|
|
1657
|
+
{ value: 'regen', label: 'Regenerate share link' },
|
|
1658
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1659
|
+
]);
|
|
1660
|
+
if (!a || a === '__back')
|
|
1661
|
+
return interactiveMenu(cfg);
|
|
1662
|
+
switch (a) {
|
|
1663
|
+
case 'list': {
|
|
1664
|
+
const s = p.spinner();
|
|
1665
|
+
s.start('Loading dashboards...');
|
|
1666
|
+
const items = await listEntities(post, cfg.orgId, 'dashboard');
|
|
1667
|
+
s.stop(`${items.length} dashboards`);
|
|
1668
|
+
if (items.length > 0) {
|
|
1669
|
+
const lines = items.map((d) => ` ${pc.cyan(truncate(d.name || 'Untitled', 50))} ${pc.dim(d.id)}`);
|
|
1670
|
+
p.note(lines.join('\n'), 'Dashboards');
|
|
1671
|
+
}
|
|
1672
|
+
return mDashboards(cfg, post);
|
|
1673
|
+
}
|
|
1674
|
+
case 'create': {
|
|
1675
|
+
const title = await txt('Dashboard title', true);
|
|
1676
|
+
if (!title)
|
|
1677
|
+
return mDashboards(cfg, post);
|
|
1678
|
+
const desc = await txt('Description');
|
|
1679
|
+
const payload = { title };
|
|
1680
|
+
if (desc)
|
|
1681
|
+
payload.description = desc;
|
|
1682
|
+
await cmd(post, cfg.orgId, 'create_dashboard', payload, 'Creating dashboard...');
|
|
1683
|
+
return mDashboards(cfg, post);
|
|
1684
|
+
}
|
|
1685
|
+
case 'edit': {
|
|
1686
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1687
|
+
if (!dash)
|
|
1688
|
+
return mDashboards(cfg, post);
|
|
1689
|
+
const updates = {};
|
|
1690
|
+
const t = await txt('New title (blank to skip)');
|
|
1691
|
+
if (t)
|
|
1692
|
+
updates.title = t;
|
|
1693
|
+
const d = await txt('New description (blank to skip)');
|
|
1694
|
+
if (d)
|
|
1695
|
+
updates.description = d;
|
|
1696
|
+
if (Object.keys(updates).length > 0) {
|
|
1697
|
+
await cmd(post, cfg.orgId, 'update_dashboard', { dashboard_id: dash.id, updates }, 'Updating...');
|
|
1698
|
+
}
|
|
1699
|
+
return mDashboards(cfg, post);
|
|
1700
|
+
}
|
|
1701
|
+
case 'delete': {
|
|
1702
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1703
|
+
if (!dash)
|
|
1704
|
+
return mDashboards(cfg, post);
|
|
1705
|
+
const sure = await yn(`Delete "${dash.name}"?`);
|
|
1706
|
+
if (sure)
|
|
1707
|
+
await cmd(post, cfg.orgId, 'delete_dashboard', { dashboard_id: dash.id }, 'Deleting...');
|
|
1708
|
+
return mDashboards(cfg, post);
|
|
1709
|
+
}
|
|
1710
|
+
case 'share': {
|
|
1711
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1712
|
+
if (!dash)
|
|
1713
|
+
return mDashboards(cfg, post);
|
|
1714
|
+
const shareType = await sel('How should this be shared?', [
|
|
1715
|
+
{ value: 'public', label: 'Public link', hint: 'anyone with the link can view' },
|
|
1716
|
+
{ value: 'password', label: 'Password protected' },
|
|
1717
|
+
{ value: 'login', label: 'Login required', hint: 'must be signed in' },
|
|
1718
|
+
{ value: 'invite', label: 'Invite only', hint: 'only invited emails' },
|
|
1719
|
+
]);
|
|
1720
|
+
if (!shareType)
|
|
1721
|
+
return mDashboards(cfg, post);
|
|
1722
|
+
const payload = { dashboard_id: dash.id, share_type: shareType };
|
|
1723
|
+
if (shareType === 'password') {
|
|
1724
|
+
const pw = await txt('Set a password', true);
|
|
1725
|
+
if (pw)
|
|
1726
|
+
payload.password = pw;
|
|
1727
|
+
}
|
|
1728
|
+
const branding = await sel('Branding', [
|
|
1729
|
+
{ value: 'branded', label: 'Show your logo and colors' },
|
|
1730
|
+
{ value: 'minimal', label: 'Minimal / clean' },
|
|
1731
|
+
]);
|
|
1732
|
+
if (branding)
|
|
1733
|
+
payload.branding_mode = branding;
|
|
1734
|
+
await cmd(post, cfg.orgId, 'upsert_dashboard_share', payload, 'Setting up sharing...');
|
|
1735
|
+
return mDashboards(cfg, post);
|
|
1736
|
+
}
|
|
1737
|
+
case 'unshare': {
|
|
1738
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1739
|
+
if (!dash)
|
|
1740
|
+
return mDashboards(cfg, post);
|
|
1741
|
+
const sure = await yn(`Stop sharing "${dash.name}"? Anyone with the link will lose access.`);
|
|
1742
|
+
if (sure)
|
|
1743
|
+
await cmd(post, cfg.orgId, 'delete_dashboard_share', { dashboard_id: dash.id }, 'Revoking share...');
|
|
1744
|
+
return mDashboards(cfg, post);
|
|
1745
|
+
}
|
|
1746
|
+
case 'invite': {
|
|
1747
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1748
|
+
if (!dash)
|
|
1749
|
+
return mDashboards(cfg, post);
|
|
1750
|
+
const emailsRaw = await txt('Email addresses (comma-separated)', true);
|
|
1751
|
+
if (!emailsRaw)
|
|
1752
|
+
return mDashboards(cfg, post);
|
|
1753
|
+
const emails = emailsRaw.split(',').map(e => e.trim()).filter(Boolean);
|
|
1754
|
+
await cmd(post, cfg.orgId, 'invite_dashboard_share', { dashboard_id: dash.id, emails }, 'Sending invitations...');
|
|
1755
|
+
return mDashboards(cfg, post);
|
|
1756
|
+
}
|
|
1757
|
+
case 'uninvite': {
|
|
1758
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1759
|
+
if (!dash)
|
|
1760
|
+
return mDashboards(cfg, post);
|
|
1761
|
+
const inviteId = await txt('Invite ID to remove', true);
|
|
1762
|
+
if (!inviteId)
|
|
1763
|
+
return mDashboards(cfg, post);
|
|
1764
|
+
await cmd(post, cfg.orgId, 'delete_dashboard_share_invite', { dashboard_id: dash.id, invite_id: inviteId }, 'Removing invite...');
|
|
1765
|
+
return mDashboards(cfg, post);
|
|
1766
|
+
}
|
|
1767
|
+
case 'regen': {
|
|
1768
|
+
const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
|
|
1769
|
+
if (!dash)
|
|
1770
|
+
return mDashboards(cfg, post);
|
|
1771
|
+
const sure = await yn('Regenerate the share link? The old link will stop working.');
|
|
1772
|
+
if (sure)
|
|
1773
|
+
await cmd(post, cfg.orgId, 'regenerate_dashboard_share', { dashboard_id: dash.id }, 'Regenerating link...');
|
|
1774
|
+
return mDashboards(cfg, post);
|
|
1775
|
+
}
|
|
529
1776
|
}
|
|
530
|
-
|
|
531
|
-
|
|
1777
|
+
return mDashboards(cfg, post);
|
|
1778
|
+
}
|
|
1779
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1780
|
+
// FORMS — 3 commands
|
|
1781
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1782
|
+
async function mForms(cfg, post) {
|
|
1783
|
+
const a = await sel('Public Forms', [
|
|
1784
|
+
{ value: 'list', label: 'View forms' },
|
|
1785
|
+
{ value: 'create', label: 'Create a form' },
|
|
1786
|
+
{ value: 'edit', label: 'Edit a form' },
|
|
1787
|
+
{ value: 'delete', label: 'Delete a form' },
|
|
1788
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1789
|
+
]);
|
|
1790
|
+
if (!a || a === '__back')
|
|
1791
|
+
return interactiveMenu(cfg);
|
|
1792
|
+
switch (a) {
|
|
1793
|
+
case 'list': {
|
|
1794
|
+
const s = p.spinner();
|
|
1795
|
+
s.start('Loading forms...');
|
|
1796
|
+
const items = await listEntities(post, cfg.orgId, 'public_form');
|
|
1797
|
+
s.stop(`${items.length} forms`);
|
|
1798
|
+
if (items.length > 0) {
|
|
1799
|
+
const lines = items.map((f) => ` ${pc.cyan(truncate(f.name || 'Untitled', 50))} ${pc.dim(f.id)}`);
|
|
1800
|
+
p.note(lines.join('\n'), 'Forms');
|
|
1801
|
+
}
|
|
1802
|
+
return mForms(cfg, post);
|
|
1803
|
+
}
|
|
1804
|
+
case 'create': {
|
|
1805
|
+
const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1806
|
+
if (!table)
|
|
1807
|
+
return mForms(cfg, post);
|
|
1808
|
+
const title = await txt('Form title', true);
|
|
1809
|
+
if (!title)
|
|
1810
|
+
return mForms(cfg, post);
|
|
1811
|
+
const payload = { table_id: table.id, title };
|
|
1812
|
+
const desc = await txt('Description');
|
|
1813
|
+
if (desc)
|
|
1814
|
+
payload.description = desc;
|
|
1815
|
+
const access = await sel('Who can see submitted data?', [
|
|
1816
|
+
{ value: 'own', label: 'Only their own submissions' },
|
|
1817
|
+
{ value: 'all', label: 'All submissions' },
|
|
1818
|
+
{ value: 'none', label: 'No one (admin only)' },
|
|
1819
|
+
]);
|
|
1820
|
+
if (access)
|
|
1821
|
+
payload.submission_access = access;
|
|
1822
|
+
const share = await sel('Sharing', [
|
|
1823
|
+
{ value: 'public', label: 'Public link' },
|
|
1824
|
+
{ value: 'password', label: 'Password protected' },
|
|
1825
|
+
{ value: 'invite', label: 'Invite only' },
|
|
1826
|
+
]);
|
|
1827
|
+
if (share)
|
|
1828
|
+
payload.share_type = share;
|
|
1829
|
+
if (share === 'password') {
|
|
1830
|
+
const pw = await txt('Password', true);
|
|
1831
|
+
if (pw)
|
|
1832
|
+
payload.password = pw;
|
|
1833
|
+
}
|
|
1834
|
+
await cmd(post, cfg.orgId, 'create_public_form', payload, 'Creating form...');
|
|
1835
|
+
return mForms(cfg, post);
|
|
1836
|
+
}
|
|
1837
|
+
case 'edit': {
|
|
1838
|
+
const form = await pickEntity(post, cfg.orgId, 'public_form', 'forms');
|
|
1839
|
+
if (!form)
|
|
1840
|
+
return mForms(cfg, post);
|
|
1841
|
+
const updates = {};
|
|
1842
|
+
const t = await txt('New title (blank to skip)');
|
|
1843
|
+
if (t)
|
|
1844
|
+
updates.title = t;
|
|
1845
|
+
const d = await txt('New description (blank to skip)');
|
|
1846
|
+
if (d)
|
|
1847
|
+
updates.description = d;
|
|
1848
|
+
const active = await sel('Active?', [
|
|
1849
|
+
{ value: '__skip', label: pc.dim('Keep current') },
|
|
1850
|
+
{ value: 'true', label: 'Active (accepting submissions)' },
|
|
1851
|
+
{ value: 'false', label: 'Inactive (closed)' },
|
|
1852
|
+
]);
|
|
1853
|
+
if (active && active !== '__skip')
|
|
1854
|
+
updates.is_active = active === 'true';
|
|
1855
|
+
if (Object.keys(updates).length > 0) {
|
|
1856
|
+
await cmd(post, cfg.orgId, 'update_public_form', { form_id: form.id, updates }, 'Updating form...');
|
|
1857
|
+
}
|
|
1858
|
+
return mForms(cfg, post);
|
|
1859
|
+
}
|
|
1860
|
+
case 'delete': {
|
|
1861
|
+
const form = await pickEntity(post, cfg.orgId, 'public_form', 'forms');
|
|
1862
|
+
if (!form)
|
|
1863
|
+
return mForms(cfg, post);
|
|
1864
|
+
const sure = await yn(`Delete "${form.name}"?`);
|
|
1865
|
+
if (sure)
|
|
1866
|
+
await cmd(post, cfg.orgId, 'delete_public_form', { form_id: form.id }, 'Deleting form...');
|
|
1867
|
+
return mForms(cfg, post);
|
|
1868
|
+
}
|
|
532
1869
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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)
|
|
1870
|
+
return mForms(cfg, post);
|
|
1871
|
+
}
|
|
1872
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1873
|
+
// APPROVALS — resolve_approval + list_approvals query
|
|
1874
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1875
|
+
async function mApprovals(cfg, post) {
|
|
1876
|
+
const a = await sel('Approvals', [
|
|
1877
|
+
{ value: 'list', label: 'View pending approvals' },
|
|
1878
|
+
{ value: 'all', label: 'View all approvals', hint: 'including resolved' },
|
|
1879
|
+
{ value: 'resolve', label: 'Approve or reject' },
|
|
1880
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1881
|
+
]);
|
|
1882
|
+
if (!a || a === '__back')
|
|
572
1883
|
return interactiveMenu(cfg);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1884
|
+
switch (a) {
|
|
1885
|
+
case 'list': {
|
|
1886
|
+
const s = p.spinner();
|
|
1887
|
+
s.start('Loading approvals...');
|
|
1888
|
+
const res = await qry(post, cfg.orgId, 'list_approvals', { status: 'pending' });
|
|
1889
|
+
const items = Array.isArray(res?.data) ? res.data : [];
|
|
1890
|
+
s.stop(`${items.length} pending approvals`);
|
|
1891
|
+
if (items.length > 0) {
|
|
1892
|
+
const lines = items.map((ap) => {
|
|
1893
|
+
const target = ap.target_name || ap.target_id || '';
|
|
1894
|
+
return ` ${pc.yellow('\u25CB')} ${pc.cyan(truncate(target, 40))} ${pc.dim(ap.id)}`;
|
|
1895
|
+
});
|
|
1896
|
+
p.note(lines.join('\n'), 'Pending Approvals');
|
|
1897
|
+
}
|
|
1898
|
+
return mApprovals(cfg, post);
|
|
1899
|
+
}
|
|
1900
|
+
case 'all': {
|
|
1901
|
+
const s = p.spinner();
|
|
1902
|
+
s.start('Loading approvals...');
|
|
1903
|
+
const res = await qry(post, cfg.orgId, 'list_approvals', { status: 'all' });
|
|
1904
|
+
const items = Array.isArray(res?.data) ? res.data : [];
|
|
1905
|
+
s.stop(`${items.length} approvals`);
|
|
1906
|
+
if (items.length > 0) {
|
|
1907
|
+
const lines = items.map((ap) => {
|
|
1908
|
+
const icon = ap.status === 'approved' ? pc.green('\u2713') : ap.status === 'rejected' ? pc.red('\u2717') : pc.yellow('\u25CB');
|
|
1909
|
+
const target = ap.target_name || ap.target_id || '';
|
|
1910
|
+
return ` ${icon} ${pc.cyan(truncate(target, 40))} ${pc.dim(ap.status || '')}`;
|
|
1911
|
+
});
|
|
1912
|
+
p.note(lines.join('\n'), 'All Approvals');
|
|
1913
|
+
}
|
|
1914
|
+
return mApprovals(cfg, post);
|
|
1915
|
+
}
|
|
1916
|
+
case 'resolve': {
|
|
1917
|
+
const approval = await pickEntity(post, cfg.orgId, 'approval', 'approvals');
|
|
1918
|
+
if (!approval)
|
|
1919
|
+
return mApprovals(cfg, post);
|
|
1920
|
+
const resolution = await sel('Decision', [
|
|
1921
|
+
{ value: 'approved', label: 'Approve' },
|
|
1922
|
+
{ value: 'rejected', label: 'Reject' },
|
|
1923
|
+
]);
|
|
1924
|
+
if (!resolution)
|
|
1925
|
+
return mApprovals(cfg, post);
|
|
1926
|
+
const note = await txt('Add a note (optional)');
|
|
1927
|
+
const payload = { approval_id: approval.id, resolution };
|
|
1928
|
+
if (note)
|
|
1929
|
+
payload.resolution_note = note;
|
|
1930
|
+
await cmd(post, cfg.orgId, 'resolve_approval', payload, resolution === 'approved' ? 'Approving...' : 'Rejecting...');
|
|
1931
|
+
return mApprovals(cfg, post);
|
|
1932
|
+
}
|
|
589
1933
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1934
|
+
return mApprovals(cfg, post);
|
|
1935
|
+
}
|
|
1936
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1937
|
+
// TEAM — 4 commands
|
|
1938
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1939
|
+
async function mTeam(cfg, post) {
|
|
1940
|
+
const a = await sel('Team', [
|
|
1941
|
+
{ value: 'invite', label: 'Invite a member' },
|
|
1942
|
+
{ value: 'revoke', label: 'Revoke an invitation' },
|
|
1943
|
+
{ value: 'role', label: 'Change a member\u2019s role' },
|
|
1944
|
+
{ value: 'remove', label: 'Remove a member' },
|
|
1945
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
1946
|
+
]);
|
|
1947
|
+
if (!a || a === '__back')
|
|
1948
|
+
return interactiveMenu(cfg);
|
|
1949
|
+
switch (a) {
|
|
1950
|
+
case 'invite': {
|
|
1951
|
+
const email = await txt('Email address', true);
|
|
1952
|
+
if (!email)
|
|
1953
|
+
return mTeam(cfg, post);
|
|
1954
|
+
const role = await sel('Role', [
|
|
1955
|
+
{ value: 'user', label: 'Member', hint: 'can complete tasks and view data' },
|
|
1956
|
+
{ value: 'admin', label: 'Admin', hint: 'can manage plans, tables, and team' },
|
|
1957
|
+
{ value: 'super_admin', label: 'Owner', hint: 'full control' },
|
|
1958
|
+
]);
|
|
1959
|
+
if (!role)
|
|
1960
|
+
return mTeam(cfg, post);
|
|
1961
|
+
await cmd(post, cfg.orgId, 'invite_member', { email, role }, 'Sending invitation...');
|
|
1962
|
+
return mTeam(cfg, post);
|
|
1963
|
+
}
|
|
1964
|
+
case 'revoke': {
|
|
1965
|
+
const invId = await txt('Invitation ID', true);
|
|
1966
|
+
if (!invId)
|
|
1967
|
+
return mTeam(cfg, post);
|
|
1968
|
+
await cmd(post, cfg.orgId, 'revoke_invitation', { invitation_id: invId }, 'Revoking invitation...');
|
|
1969
|
+
return mTeam(cfg, post);
|
|
1970
|
+
}
|
|
1971
|
+
case 'role': {
|
|
1972
|
+
const userId = await txt('Member user ID', true);
|
|
1973
|
+
if (!userId)
|
|
1974
|
+
return mTeam(cfg, post);
|
|
1975
|
+
const role = await sel('New role', [
|
|
1976
|
+
{ value: 'user', label: 'Member' },
|
|
1977
|
+
{ value: 'admin', label: 'Admin' },
|
|
1978
|
+
{ value: 'super_admin', label: 'Owner' },
|
|
1979
|
+
]);
|
|
1980
|
+
if (!role)
|
|
1981
|
+
return mTeam(cfg, post);
|
|
1982
|
+
await cmd(post, cfg.orgId, 'update_member_role', { user_id: userId, role }, 'Updating role...');
|
|
1983
|
+
return mTeam(cfg, post);
|
|
1984
|
+
}
|
|
1985
|
+
case 'remove': {
|
|
1986
|
+
const userId = await txt('Member user ID', true);
|
|
1987
|
+
if (!userId)
|
|
1988
|
+
return mTeam(cfg, post);
|
|
1989
|
+
const sure = await yn('Remove this member? They will lose access to the workspace.');
|
|
1990
|
+
if (sure)
|
|
1991
|
+
await cmd(post, cfg.orgId, 'remove_member', { user_id: userId }, 'Removing member...');
|
|
1992
|
+
return mTeam(cfg, post);
|
|
1993
|
+
}
|
|
593
1994
|
}
|
|
594
|
-
return
|
|
1995
|
+
return mTeam(cfg, post);
|
|
595
1996
|
}
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1997
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1998
|
+
// ORGANIZATION — 6 commands
|
|
1999
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
2000
|
+
async function mOrg(cfg, post) {
|
|
2001
|
+
const a = await sel('Organization', [
|
|
2002
|
+
{ value: 'branding', label: 'Update branding', hint: 'name, logo, accent color' },
|
|
2003
|
+
{ value: 'create', label: 'Create a new organization' },
|
|
2004
|
+
{ value: 'join', label: 'Join an organization' },
|
|
2005
|
+
{ value: 'accept', label: 'Accept an invitation' },
|
|
2006
|
+
{ value: 'leave', label: 'Leave this organization' },
|
|
2007
|
+
{ value: 'delete', label: 'Delete this organization', hint: 'owner only, irreversible' },
|
|
2008
|
+
{ value: 'switch', label: 'Switch workspace' },
|
|
2009
|
+
{ value: 'permissions', label: 'View my permissions' },
|
|
2010
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
2011
|
+
]);
|
|
2012
|
+
if (!a || a === '__back')
|
|
2013
|
+
return interactiveMenu(cfg);
|
|
2014
|
+
switch (a) {
|
|
2015
|
+
case 'branding': {
|
|
2016
|
+
const updates = { organization_id: cfg.orgId };
|
|
2017
|
+
const name = await txt('Organization name (blank to skip)');
|
|
2018
|
+
if (name)
|
|
2019
|
+
updates.name = name;
|
|
2020
|
+
const color = await txt('Accent color (blank to skip)', false, '#6366f1');
|
|
2021
|
+
if (color)
|
|
2022
|
+
updates.accent_color = color;
|
|
2023
|
+
const logo = await txt('Logo URL (blank to skip)');
|
|
2024
|
+
if (logo)
|
|
2025
|
+
updates.logo_url = logo;
|
|
2026
|
+
if (Object.keys(updates).length > 1) {
|
|
2027
|
+
await cmd(post, cfg.orgId, 'update_organization_branding', updates, 'Updating branding...');
|
|
2028
|
+
}
|
|
2029
|
+
return mOrg(cfg, post);
|
|
2030
|
+
}
|
|
2031
|
+
case 'create': {
|
|
2032
|
+
const name = await txt('Organization name', true);
|
|
2033
|
+
if (!name)
|
|
2034
|
+
return mOrg(cfg, post);
|
|
2035
|
+
const payload = { name };
|
|
2036
|
+
const color = await txt('Accent color', false, '#6366f1');
|
|
2037
|
+
if (color)
|
|
2038
|
+
payload.accent_color = color;
|
|
2039
|
+
await cmdNoOrg(post, 'create_organization', payload, 'Creating organization...');
|
|
2040
|
+
return mOrg(cfg, post);
|
|
2041
|
+
}
|
|
2042
|
+
case 'join': {
|
|
2043
|
+
const orgId = await txt('Organization ID', true);
|
|
2044
|
+
if (!orgId)
|
|
2045
|
+
return mOrg(cfg, post);
|
|
2046
|
+
await cmdNoOrg(post, 'join_organization', { organization_id: orgId }, 'Joining...');
|
|
2047
|
+
return mOrg(cfg, post);
|
|
2048
|
+
}
|
|
2049
|
+
case 'accept': {
|
|
2050
|
+
const token = await txt('Invitation token', true);
|
|
2051
|
+
if (!token)
|
|
2052
|
+
return mOrg(cfg, post);
|
|
2053
|
+
await cmdNoOrg(post, 'accept_invitation', { token }, 'Accepting invitation...');
|
|
2054
|
+
return mOrg(cfg, post);
|
|
2055
|
+
}
|
|
2056
|
+
case 'leave': {
|
|
2057
|
+
const sure = await yn(`Leave "${cfg.orgName}"? You will lose access.`);
|
|
2058
|
+
if (sure)
|
|
2059
|
+
await cmd(post, cfg.orgId, 'leave_organization', {}, 'Leaving organization...');
|
|
2060
|
+
return mOrg(cfg, post);
|
|
2061
|
+
}
|
|
2062
|
+
case 'delete': {
|
|
2063
|
+
const sure1 = await yn(`DELETE "${cfg.orgName}"? This permanently destroys ALL data.`);
|
|
2064
|
+
if (!sure1)
|
|
2065
|
+
return mOrg(cfg, post);
|
|
2066
|
+
const sure2 = await yn('Are you absolutely sure? This CANNOT be undone.');
|
|
2067
|
+
if (sure2)
|
|
2068
|
+
await cmd(post, cfg.orgId, 'delete_organization', {}, 'Deleting organization...');
|
|
2069
|
+
return mOrg(cfg, post);
|
|
2070
|
+
}
|
|
2071
|
+
case 'switch': {
|
|
2072
|
+
const s = p.spinner();
|
|
2073
|
+
s.start('Loading workspaces...');
|
|
2074
|
+
const res = await qry(post, cfg.orgId, 'list_my_organizations');
|
|
2075
|
+
const orgs = Array.isArray(res?.data) ? res.data : [];
|
|
2076
|
+
s.stop(`${orgs.length} workspaces`);
|
|
2077
|
+
const selected = await sel('Switch to', orgs.map((o) => ({
|
|
610
2078
|
value: o.id,
|
|
611
2079
|
label: o.name,
|
|
612
2080
|
hint: o.id === cfg.orgId ? pc.green('current') : o.role,
|
|
613
|
-
}))
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
2081
|
+
})));
|
|
2082
|
+
if (selected) {
|
|
2083
|
+
const org = orgs.find((o) => o.id === selected);
|
|
2084
|
+
cfg.orgId = selected;
|
|
2085
|
+
cfg.orgName = org?.name || '';
|
|
2086
|
+
saveConfig(cfg);
|
|
2087
|
+
p.log.success(`Switched to ${pc.cyan(cfg.orgName)}`);
|
|
2088
|
+
}
|
|
2089
|
+
return mOrg(cfg, post);
|
|
2090
|
+
}
|
|
2091
|
+
case 'permissions': {
|
|
2092
|
+
const s = p.spinner();
|
|
2093
|
+
s.start('Checking permissions...');
|
|
2094
|
+
try {
|
|
2095
|
+
const res = await post('/api/v2/introspection/permissions', { organization_id: cfg.orgId });
|
|
2096
|
+
s.stop('');
|
|
2097
|
+
const data = res.data || res;
|
|
2098
|
+
if (data.role)
|
|
2099
|
+
p.log.info(`Your role: ${pc.cyan(data.role)}`);
|
|
2100
|
+
if (data.capabilities) {
|
|
2101
|
+
const allowed = Object.entries(data.capabilities).filter(([, v]) => v).map(([k]) => k);
|
|
2102
|
+
const denied = Object.entries(data.capabilities).filter(([, v]) => !v).map(([k]) => k);
|
|
2103
|
+
if (allowed.length > 0)
|
|
2104
|
+
p.log.info(`${pc.green('Allowed')}: ${allowed.length} capabilities`);
|
|
2105
|
+
if (denied.length > 0)
|
|
2106
|
+
p.log.info(`${pc.red('Denied')}: ${denied.length} capabilities`);
|
|
2107
|
+
}
|
|
2108
|
+
else {
|
|
2109
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
catch (err) {
|
|
2113
|
+
s.stop(pc.red('Failed'));
|
|
2114
|
+
p.log.error(err.message);
|
|
2115
|
+
}
|
|
2116
|
+
return mOrg(cfg, post);
|
|
2117
|
+
}
|
|
622
2118
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
2119
|
+
return mOrg(cfg, post);
|
|
2120
|
+
}
|
|
2121
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
2122
|
+
// WORKSPACE OVERVIEW — search, status, entity details, schema
|
|
2123
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
2124
|
+
async function mOverview(cfg, post) {
|
|
2125
|
+
const a = await sel('Workspace', [
|
|
2126
|
+
{ value: 'status', label: 'Workspace status', hint: 'summary of everything' },
|
|
2127
|
+
{ value: 'search', label: 'Search', hint: 'find anything in your workspace' },
|
|
2128
|
+
{ value: 'details', label: 'View entity details', hint: 'look up any item by type' },
|
|
2129
|
+
{ value: 'schema', label: 'View entity schemas', hint: 'see available fields per type' },
|
|
2130
|
+
{ value: '__back', label: pc.dim('\u2190 Back to main menu') },
|
|
2131
|
+
]);
|
|
2132
|
+
if (!a || a === '__back')
|
|
2133
|
+
return interactiveMenu(cfg);
|
|
2134
|
+
switch (a) {
|
|
2135
|
+
case 'status': {
|
|
2136
|
+
const s = p.spinner();
|
|
2137
|
+
s.start('Loading workspace...');
|
|
2138
|
+
const nodes = await getWorkspaceNodes(post, cfg.orgId);
|
|
2139
|
+
s.stop('Workspace loaded');
|
|
2140
|
+
const counts = {};
|
|
2141
|
+
for (const node of nodes) {
|
|
2142
|
+
const t = node.type || 'unknown';
|
|
2143
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
2144
|
+
}
|
|
2145
|
+
const lines = [
|
|
2146
|
+
`${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
|
|
2147
|
+
'',
|
|
2148
|
+
`${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
|
|
2149
|
+
`${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
|
|
2150
|
+
`${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
|
|
2151
|
+
`${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
|
|
2152
|
+
`${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
|
|
2153
|
+
`${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
|
|
2154
|
+
`${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
|
|
2155
|
+
`${pad('Dashboards', 16)} ${pc.bold(String(counts.dashboard || 0))}`,
|
|
2156
|
+
`${pad('Photos', 16)} ${pc.bold(String(counts.photo || 0))}`,
|
|
2157
|
+
`${pad('Locations', 16)} ${pc.bold(String(counts.geolocation_area || 0))}`,
|
|
2158
|
+
`${pad('Checklists', 16)} ${pc.bold(String(counts.checklist || 0))}`,
|
|
2159
|
+
];
|
|
2160
|
+
p.note(lines.join('\n'), 'Workspace Overview');
|
|
2161
|
+
return mOverview(cfg, post);
|
|
2162
|
+
}
|
|
2163
|
+
case 'search': {
|
|
2164
|
+
const query = await txt('Search for...', true, 'task name, plan, document...');
|
|
2165
|
+
if (!query)
|
|
2166
|
+
return mOverview(cfg, post);
|
|
2167
|
+
const s = p.spinner();
|
|
2168
|
+
s.start('Searching...');
|
|
2169
|
+
const entities = await searchEntities(post, cfg.orgId, query);
|
|
2170
|
+
s.stop(`${entities.length} results`);
|
|
2171
|
+
if (entities.length > 0) {
|
|
2172
|
+
const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 18))} ${pc.cyan(truncate(e.name || 'Untitled', 50))}`);
|
|
2173
|
+
p.note(lines.join('\n'), `Results for "${query}"`);
|
|
2174
|
+
}
|
|
2175
|
+
else {
|
|
2176
|
+
p.log.info('No results found.');
|
|
2177
|
+
}
|
|
2178
|
+
return mOverview(cfg, post);
|
|
2179
|
+
}
|
|
2180
|
+
case 'details': {
|
|
2181
|
+
const entityType = await sel('What type of item?', [
|
|
2182
|
+
{ value: 'plan', label: 'Plan' }, { value: 'task', label: 'Task' },
|
|
2183
|
+
{ value: 'table', label: 'Table' }, { value: 'document', label: 'Document' },
|
|
2184
|
+
{ value: 'dashboard', label: 'Dashboard' }, { value: 'goal', label: 'Goal' },
|
|
2185
|
+
{ value: 'rule', label: 'Rule' }, { value: 'requirement', label: 'Requirement' },
|
|
2186
|
+
{ value: 'photo', label: 'Photo' }, { value: 'geolocation_area', label: 'Location area' },
|
|
2187
|
+
{ value: 'checklist', label: 'Checklist' }, { value: 'public_form', label: 'Form' },
|
|
2188
|
+
]);
|
|
2189
|
+
if (!entityType)
|
|
2190
|
+
return mOverview(cfg, post);
|
|
2191
|
+
const entity = await pickEntity(post, cfg.orgId, entityType, `${entityType}s`);
|
|
2192
|
+
if (!entity)
|
|
2193
|
+
return mOverview(cfg, post);
|
|
2194
|
+
// Fetch full details
|
|
2195
|
+
const s = p.spinner();
|
|
2196
|
+
s.start('Loading details...');
|
|
2197
|
+
const res = await qry(post, cfg.orgId, 'get_entity', { entity_type: entityType, id: entity.id });
|
|
2198
|
+
s.stop('');
|
|
2199
|
+
if (res?.data) {
|
|
2200
|
+
const data = res.data;
|
|
2201
|
+
const lines = Object.entries(data.metadata || data).map(([k, v]) => {
|
|
2202
|
+
if (v === null || v === undefined)
|
|
2203
|
+
return null;
|
|
2204
|
+
const val = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
2205
|
+
return ` ${pc.dim(pad(k, 24))} ${truncate(val, 60)}`;
|
|
2206
|
+
}).filter(Boolean);
|
|
2207
|
+
p.note(lines.join('\n'), entity.name || 'Details');
|
|
2208
|
+
}
|
|
2209
|
+
return mOverview(cfg, post);
|
|
2210
|
+
}
|
|
2211
|
+
case 'schema': {
|
|
2212
|
+
const entityType = await sel('Schema for which type?', [
|
|
2213
|
+
{ value: 'plan', label: 'Plan' }, { value: 'task', label: 'Task' },
|
|
2214
|
+
{ value: 'table', label: 'Table' }, { value: 'document', label: 'Document' },
|
|
2215
|
+
{ value: 'dashboard', label: 'Dashboard' }, { value: 'goal', label: 'Goal' },
|
|
2216
|
+
{ value: 'rule', label: 'Rule' }, { value: 'requirement', label: 'Requirement' },
|
|
2217
|
+
]);
|
|
2218
|
+
if (!entityType)
|
|
2219
|
+
return mOverview(cfg, post);
|
|
2220
|
+
const s = p.spinner();
|
|
2221
|
+
s.start('Loading schema...');
|
|
2222
|
+
try {
|
|
2223
|
+
const res = await post('/api/v2/introspection/schema', {
|
|
2224
|
+
organization_id: cfg.orgId, entity_type: entityType,
|
|
2225
|
+
});
|
|
2226
|
+
s.stop('');
|
|
2227
|
+
const schema = res.data || res;
|
|
2228
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
2229
|
+
}
|
|
2230
|
+
catch (err) {
|
|
2231
|
+
s.stop(pc.red('Failed'));
|
|
2232
|
+
p.log.error(err.message);
|
|
2233
|
+
}
|
|
2234
|
+
return mOverview(cfg, post);
|
|
2235
|
+
}
|
|
626
2236
|
}
|
|
627
|
-
return
|
|
2237
|
+
return mOverview(cfg, post);
|
|
2238
|
+
}
|
|
2239
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
2240
|
+
// DIRECT COMMANDS (non-interactive, for scripts & power users)
|
|
2241
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
2242
|
+
function getFlag(args, flag) {
|
|
2243
|
+
const idx = args.indexOf(flag);
|
|
2244
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
2245
|
+
return undefined;
|
|
2246
|
+
return args[idx + 1];
|
|
628
2247
|
}
|
|
629
|
-
// ── Direct Commands (non-interactive) ───────────────────────────────
|
|
630
2248
|
async function directPlans(cfg) {
|
|
631
2249
|
const { post } = api(cfg);
|
|
632
2250
|
const s = p.spinner();
|
|
@@ -637,11 +2255,8 @@ async function directPlans(cfg) {
|
|
|
637
2255
|
p.log.info('No plans found.');
|
|
638
2256
|
return;
|
|
639
2257
|
}
|
|
640
|
-
const lines = plans.map((
|
|
641
|
-
|
|
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'}`);
|
|
2258
|
+
const lines = plans.map((pl) => ` ${pc.cyan(pad(truncate(pl.name || 'Untitled', 40), 42))} ${pc.dim(pl.id)}`);
|
|
2259
|
+
p.note(lines.join('\n'), `${plans.length} plans`);
|
|
645
2260
|
}
|
|
646
2261
|
async function directTasks(cfg) {
|
|
647
2262
|
const { post } = api(cfg);
|
|
@@ -653,23 +2268,12 @@ async function directTasks(cfg) {
|
|
|
653
2268
|
p.log.info('No tasks found.');
|
|
654
2269
|
return;
|
|
655
2270
|
}
|
|
656
|
-
const lines = tasks.map((t) => {
|
|
657
|
-
|
|
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'}`);
|
|
2271
|
+
const lines = tasks.map((t) => ` ${pc.cyan(pad(truncate(t.name || 'Untitled', 50), 52))} ${pc.dim(t.metadata?.frequency || '')} ${pc.dim(t.id)}`);
|
|
2272
|
+
p.note(lines.join('\n'), `${tasks.length} tasks`);
|
|
662
2273
|
}
|
|
663
2274
|
async function directComplete(cfg, taskId) {
|
|
664
2275
|
const { post } = api(cfg);
|
|
665
|
-
|
|
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!'));
|
|
2276
|
+
await cmd(post, cfg.orgId, 'complete_task', { task_id: taskId }, 'Completing task...');
|
|
673
2277
|
}
|
|
674
2278
|
async function directStatus(cfg) {
|
|
675
2279
|
const { post } = api(cfg);
|
|
@@ -683,8 +2287,7 @@ async function directStatus(cfg) {
|
|
|
683
2287
|
counts[t] = (counts[t] || 0) + 1;
|
|
684
2288
|
}
|
|
685
2289
|
const lines = [
|
|
686
|
-
`${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
|
|
687
|
-
'',
|
|
2290
|
+
`${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`, '',
|
|
688
2291
|
`${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
|
|
689
2292
|
`${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
|
|
690
2293
|
`${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
|
|
@@ -703,13 +2306,11 @@ async function directSearch(cfg, term) {
|
|
|
703
2306
|
s.stop('');
|
|
704
2307
|
if (entities.length === 0) {
|
|
705
2308
|
p.log.info('No results found.');
|
|
2309
|
+
return;
|
|
706
2310
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
p.note(lines.join('\n'), `${entities.length} result${entities.length === 1 ? '' : 's'} for "${term}"`);
|
|
710
|
-
}
|
|
2311
|
+
const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
|
|
2312
|
+
p.note(lines.join('\n'), `${entities.length} results for "${term}"`);
|
|
711
2313
|
}
|
|
712
|
-
// Raw passthrough for power users
|
|
713
2314
|
async function directRaw(cfg, type, args) {
|
|
714
2315
|
const { post } = api(cfg);
|
|
715
2316
|
const payloadRaw = args[0];
|
|
@@ -733,12 +2334,6 @@ async function directQuery(cfg, type, args) {
|
|
|
733
2334
|
});
|
|
734
2335
|
console.log(JSON.stringify(res, null, 2));
|
|
735
2336
|
}
|
|
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];
|
|
741
|
-
}
|
|
742
2337
|
// ── Main ────────────────────────────────────────────────────────────
|
|
743
2338
|
async function main() {
|
|
744
2339
|
const args = process.argv.slice(2);
|
|
@@ -756,19 +2351,19 @@ async function main() {
|
|
|
756
2351
|
console.log();
|
|
757
2352
|
p.intro(pc.bgCyan(pc.black(' Checkbox CLI ')) + pc.dim(` v${VERSION}`));
|
|
758
2353
|
p.note([
|
|
759
|
-
`${pc.cyan('checkbox')} Interactive menu`,
|
|
760
|
-
`${pc.cyan('checkbox setup')} Setup wizard
|
|
2354
|
+
`${pc.cyan('checkbox')} Interactive menu (all features)`,
|
|
2355
|
+
`${pc.cyan('checkbox setup')} Setup wizard`,
|
|
761
2356
|
`${pc.cyan('checkbox plans')} List your plans`,
|
|
762
2357
|
`${pc.cyan('checkbox tasks')} List your tasks`,
|
|
763
2358
|
`${pc.cyan('checkbox complete <id>')} Complete a task`,
|
|
764
2359
|
`${pc.cyan('checkbox status')} Workspace overview`,
|
|
765
2360
|
`${pc.cyan('checkbox search <term>')} Search your workspace`,
|
|
766
2361
|
'',
|
|
767
|
-
pc.dim('
|
|
768
|
-
`${pc.dim('checkbox cmd <type> [json] [--org <id>]')}`,
|
|
769
|
-
`${pc.dim('checkbox query <type> [json] [--org <id>]')}`,
|
|
2362
|
+
pc.dim('Automation & scripting:'),
|
|
2363
|
+
`${pc.dim('checkbox cmd <type> [json] [--org <id>]')} Run any command`,
|
|
2364
|
+
`${pc.dim('checkbox query <type> [json] [--org <id>]')} Run any query`,
|
|
770
2365
|
].join('\n'), 'Commands');
|
|
771
|
-
p.outro(pc.dim('Run `checkbox
|
|
2366
|
+
p.outro(pc.dim('Run `checkbox` with no arguments for the full interactive menu'));
|
|
772
2367
|
return;
|
|
773
2368
|
}
|
|
774
2369
|
const cfg = resolveConfig();
|
|
@@ -778,14 +2373,12 @@ async function main() {
|
|
|
778
2373
|
}
|
|
779
2374
|
try {
|
|
780
2375
|
switch (sub) {
|
|
781
|
-
case 'plans':
|
|
782
|
-
|
|
783
|
-
case 'tasks':
|
|
784
|
-
return await directTasks(cfg);
|
|
2376
|
+
case 'plans': return await directPlans(cfg);
|
|
2377
|
+
case 'tasks': return await directTasks(cfg);
|
|
785
2378
|
case 'complete':
|
|
786
2379
|
if (!rest[0]) {
|
|
787
2380
|
const { post } = api(cfg);
|
|
788
|
-
return await
|
|
2381
|
+
return await mTasks(cfg, post);
|
|
789
2382
|
}
|
|
790
2383
|
return await directComplete(cfg, rest[0]);
|
|
791
2384
|
case 'status':
|
|
@@ -794,11 +2387,11 @@ async function main() {
|
|
|
794
2387
|
case 'search':
|
|
795
2388
|
if (!rest[0]) {
|
|
796
2389
|
const { post } = api(cfg);
|
|
797
|
-
return await
|
|
2390
|
+
return await mOverview(cfg, post);
|
|
798
2391
|
}
|
|
799
2392
|
return await directSearch(cfg, rest.join(' '));
|
|
800
2393
|
case 'switch':
|
|
801
|
-
return await
|
|
2394
|
+
return await mOrg(cfg, api(cfg).post);
|
|
802
2395
|
case 'cmd':
|
|
803
2396
|
case 'command':
|
|
804
2397
|
return await directRaw(cfg, rest[0], rest.slice(1));
|