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