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