open-wadah 1.0.6 → 1.2.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/README.md +55 -50
- package/cli.js +833 -25
- package/package.json +4 -2
- package/test/cli.test.js +67 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# open-wadah
|
|
2
2
|
|
|
3
|
-
Open Wadah CLI for
|
|
3
|
+
Open Wadah CLI — shared task board for humans and agents. Use it from the terminal or from Cursor, Claude Code, and Kimi.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,22 +8,13 @@ Open Wadah CLI for managing tasks in a shared workspace.
|
|
|
8
8
|
npm install -g open-wadah
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Commands
|
|
11
|
+
Commands: `wadah`, `tm`, `ow` (same CLI).
|
|
12
12
|
|
|
13
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
|
-
1.
|
|
16
|
-
2. Sign
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
wadah signup
|
|
20
|
-
wadah login
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`wadah login` uses browser sign-in by default.
|
|
24
|
-
Use `wadah login --password` for terminal email/password.
|
|
25
|
-
|
|
26
|
-
3. Use the CLI:
|
|
15
|
+
1. API must be reachable (default: `https://api.openwadah.com`). Override: `TASK_MANAGER_API_URL` or `--api <url>`.
|
|
16
|
+
2. Sign in: `wadah login` (browser) or `wadah login --password` (terminal).
|
|
17
|
+
3. Use the board:
|
|
27
18
|
|
|
28
19
|
```bash
|
|
29
20
|
wadah open
|
|
@@ -31,55 +22,69 @@ wadah add "Fix login issue"
|
|
|
31
22
|
wadah complete <task-id>
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
##
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Description |
|
|
28
|
+
|--------|-------------|
|
|
29
|
+
| **Auth** | `login`, `signup`, `logout`, `whoami` |
|
|
30
|
+
| **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
|
|
31
|
+
| **Relationships** | `add --blocks <id>`, `add --blocked-by <id>`, `update --blocks <id>` |
|
|
32
|
+
| **Board** | `boards --json`, `buckets --json`, `assignees --json`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
33
|
+
| **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
|
|
34
|
+
| **Calendar** | `calendar` (list; `--assignee`, `--task`, `--from`, `--to`), `calendar add`, `calendar update <id>`, `calendar delete <id>` |
|
|
35
|
+
| **Docs** | `docs` (list), `doc create [title]`, `doc show <id>`, `doc update <id>`, `doc delete <id>` |
|
|
36
|
+
| **Agent tokens** | `agent-tokens` (list), `agent-token create [name]`, `agent-token delete <id>` |
|
|
37
|
+
| **Workspace** | `state`, `members`, `invite`, `config` |
|
|
38
|
+
| **Other** | `do "<sentence>"` (natural language, needs `OPENAI_API_KEY`), `doctor`, `completion [bash\|zsh]` |
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
- `wadah whoami`
|
|
38
|
-
- `wadah doctor`
|
|
39
|
-
- `wadah list --open`
|
|
40
|
-
- `wadah move <task-id> <bucket>`
|
|
41
|
-
- `wadah update <task-id> --title "New title"`
|
|
42
|
-
- `wadah state`
|
|
40
|
+
Run `wadah --help` for full list and options.
|
|
43
41
|
|
|
44
|
-
##
|
|
42
|
+
## Shell completion
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
- `--token <token>`: use a token for this command only
|
|
48
|
-
- `--json`: force machine-readable JSON output
|
|
49
|
-
- `--quiet`: silence non-error output (also machine-friendly)
|
|
44
|
+
**Bash:**
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
```bash
|
|
47
|
+
wadah completion bash >> ~/.bashrc
|
|
48
|
+
source ~/.bashrc
|
|
49
|
+
```
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
**Zsh:**
|
|
54
52
|
|
|
55
|
-
```
|
|
56
|
-
|
|
53
|
+
```bash
|
|
54
|
+
wadah completion zsh >> ~/.zshrc
|
|
55
|
+
source ~/.zshrc
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
## Agent / AI use
|
|
60
59
|
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- `authorization_pending`
|
|
67
|
-
- `invalid_device_code`
|
|
68
|
-
- `token_expired`
|
|
69
|
-
- `login_timeout`
|
|
60
|
+
- Set `TASK_MANAGER_TOKEN` (create a token: `wadah agent-token create "My Agent"`).
|
|
61
|
+
- Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`, `wadah buckets --json`.
|
|
62
|
+
- Natural language: `wadah do "add a task to fix the bug"` (requires `OPENAI_API_KEY`).
|
|
63
|
+
- In Cursor/Claude/Kimi: see **WADAH_CLI.md** in the repo root for a short reference.
|
|
64
|
+
- For autonomous 24/7 agents with GitHub Actions: see **AGENTS.md** in the repo root.
|
|
70
65
|
|
|
71
|
-
##
|
|
66
|
+
## Global flags
|
|
72
67
|
|
|
73
|
-
|
|
68
|
+
- `--api <url>` — API base URL
|
|
69
|
+
- `--profile <name>` — config profile
|
|
70
|
+
- `--token <token>` — auth token for this run
|
|
71
|
+
- `--json` — JSON output
|
|
72
|
+
- `--quiet` — minimal output
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
TASK_MANAGER_API_URL=https://api.openwadah.com wadah open
|
|
77
|
-
```
|
|
74
|
+
## Develop & test
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
From the repo root:
|
|
80
77
|
|
|
81
78
|
```bash
|
|
82
|
-
|
|
79
|
+
npm run install:all
|
|
80
|
+
npm run tm -- --help
|
|
81
|
+
cd task-manager-cli && npm test
|
|
83
82
|
```
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
## Error codes (with `--json`)
|
|
85
|
+
|
|
86
|
+
- `auth_error` — not authenticated
|
|
87
|
+
- `validation_error` — bad input
|
|
88
|
+
- `network_error` — request failed
|
|
89
|
+
- `api_error` — API returned an error
|
|
90
|
+
- `config` — missing config (e.g. OPENAI_API_KEY for `do`)
|
package/cli.js
CHANGED
|
@@ -6,8 +6,9 @@ import Conf from 'conf'
|
|
|
6
6
|
import { input, password as promptPassword } from '@inquirer/prompts'
|
|
7
7
|
import { webcrypto } from 'node:crypto'
|
|
8
8
|
import { spawn } from 'node:child_process'
|
|
9
|
-
import { readFileSync } from 'node:fs'
|
|
10
|
-
import
|
|
9
|
+
import { readFileSync, existsSync, createReadStream } from 'node:fs'
|
|
10
|
+
import FormData from 'form-data'
|
|
11
|
+
import { dirname, resolve, basename } from 'node:path'
|
|
11
12
|
import { fileURLToPath } from 'node:url'
|
|
12
13
|
|
|
13
14
|
const randomUUID = () => webcrypto.randomUUID()
|
|
@@ -180,8 +181,8 @@ function printTaskList(tasks, state) {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
function handleError(err) {
|
|
183
|
-
const msg
|
|
184
|
-
const code = err.code
|
|
184
|
+
const msg = err.message ?? 'Unexpected error'
|
|
185
|
+
const code = err.code ?? 'unknown_error'
|
|
185
186
|
const authLike =
|
|
186
187
|
code === 'auth_error' ||
|
|
187
188
|
msg.toLowerCase().includes('missing token') ||
|
|
@@ -194,6 +195,17 @@ function handleError(err) {
|
|
|
194
195
|
console.error(chalk.red('\n✗ Not authenticated.'))
|
|
195
196
|
console.error(chalk.gray(' Humans: wadah login'))
|
|
196
197
|
console.error(chalk.gray(' Agents: TASK_MANAGER_TOKEN=<token> wadah open\n'))
|
|
198
|
+
} else if (code === 'network_error') {
|
|
199
|
+
console.error(chalk.red('\n✗ Network error — could not reach the API.'))
|
|
200
|
+
console.error(chalk.gray(` Check your connection or API URL: ${getApiBase()}`))
|
|
201
|
+
console.error(chalk.gray(' Run: wadah doctor\n'))
|
|
202
|
+
} else if (code === 'server_error') {
|
|
203
|
+
console.error(chalk.red('\n✗ Server error — something went wrong on the API side.'))
|
|
204
|
+
console.error(chalk.gray(' Try again in a moment. If it persists, check api.openwadah.com status.\n'))
|
|
205
|
+
} else if (code === 'api_error' && msg.toLowerCase().includes('not found')) {
|
|
206
|
+
console.error(chalk.red('\n✗ Not found.'))
|
|
207
|
+
console.error(chalk.gray(' The resource may not exist, or this endpoint may not be available for agent tokens.'))
|
|
208
|
+
console.error(chalk.gray(' Run: wadah doctor\n'))
|
|
197
209
|
} else {
|
|
198
210
|
console.error(chalk.red(`\n✗ ${msg}\n`))
|
|
199
211
|
}
|
|
@@ -203,12 +215,11 @@ function handleError(err) {
|
|
|
203
215
|
async function resolveMyAssigneeId(state) {
|
|
204
216
|
try {
|
|
205
217
|
const me = await api('/api/auth/me')
|
|
218
|
+
if (me.assigneeId) return me.assigneeId
|
|
206
219
|
const userId = me.user?.id
|
|
207
|
-
// Match by user_id field on assignee (if backend returns it)
|
|
208
220
|
const byUserId = state.assignees.find((a) => a.user_id === userId)
|
|
209
221
|
if (byUserId) return byUserId.id
|
|
210
222
|
} catch {}
|
|
211
|
-
// Fallback: 'Me' assignee
|
|
212
223
|
return state.assignees.find((a) => a.name === 'Me')?.id ?? null
|
|
213
224
|
}
|
|
214
225
|
|
|
@@ -262,10 +273,117 @@ function isMachineOutput() {
|
|
|
262
273
|
return runtimeJsonOutput || runtimeQuietOutput || !process.stdout.isTTY
|
|
263
274
|
}
|
|
264
275
|
|
|
276
|
+
// ── session state cache ───────────────────────────────────────────────────────
|
|
277
|
+
let _cachedState = null
|
|
278
|
+
async function getState() {
|
|
279
|
+
if (!_cachedState) _cachedState = await api('/api/state')
|
|
280
|
+
return _cachedState
|
|
281
|
+
}
|
|
282
|
+
|
|
265
283
|
function sleep(ms) {
|
|
266
284
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
267
285
|
}
|
|
268
286
|
|
|
287
|
+
// ── natural language (do) ─────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
const DO_SYSTEM_PROMPT = `You are a parser for the "wadah" CLI (task board). Given the user's natural language request, output exactly one JSON object with no markdown or extra text:
|
|
290
|
+
{"command": "<command>", "args": [<array of string arguments>]}
|
|
291
|
+
|
|
292
|
+
Available commands and their args (use these exact command names):
|
|
293
|
+
- add: args = [task title string]. Example: add a task "Fix login" -> {"command": "add", "args": ["Fix login"]}
|
|
294
|
+
- folder create (or mkdir): args = [folder name]. Example: create a folder called Reports -> {"command": "folder create", "args": ["Reports"]}
|
|
295
|
+
- upload: args = [file path]. Optional: include "--folder", "<folder-id>" in args if user says to put in a folder. Example: upload report.pdf -> {"command": "upload", "args": ["report.pdf"]}
|
|
296
|
+
- open: args = []. List open tasks.
|
|
297
|
+
- list: args = []. List all tasks.
|
|
298
|
+
- complete: args = [task id]. Mark task done. Example: complete task abc123 -> {"command": "complete", "args": ["abc123"]}
|
|
299
|
+
- requested: args = []. List tasks I requested.
|
|
300
|
+
- view: args = [task id]. Show task details.
|
|
301
|
+
- boards: args = []. List boards.
|
|
302
|
+
- buckets: args = []. List columns.
|
|
303
|
+
- assignees: args = []. List assignees.
|
|
304
|
+
- state: args = []. Full state JSON.
|
|
305
|
+
- move: args = [task id, bucket name or id]. Move task to another column.
|
|
306
|
+
- assign: args = [task id, assignee name]. Assign a task.
|
|
307
|
+
- whoami: args = []. Show current user/workspace.
|
|
308
|
+
- folders: args = []. List folders.
|
|
309
|
+
- files: args = []. List files; optionally include "--folder", "<id>" in args.
|
|
310
|
+
- members: args = []. List workspace members.
|
|
311
|
+
- calendar: args = []. List calendar events.
|
|
312
|
+
- calendar add: args = []; include "--title", "<t>", "--start", "<iso>", "--end", "<iso>" for new event (start/end required).
|
|
313
|
+
- docs: args = []. List workspace docs.
|
|
314
|
+
- doc create: args = [title]. Create a doc. Optional: "--parent", "<id>", "--emoji", "<char>".
|
|
315
|
+
- doc show: args = [doc id]. Show one doc.
|
|
316
|
+
- doc update: args = [doc id]. Optional: "--title", "<t>", "--content", "<json>", "--emoji", "--parent", "<id>".
|
|
317
|
+
- doc delete: args = [doc id]. Delete a doc.
|
|
318
|
+
- agent-tokens: args = []. List agent tokens.
|
|
319
|
+
- agent-token create: args = [name]. Create agent token; optional "--assignee-name", "<text>".
|
|
320
|
+
- agent-token delete: args = [token id]. Revoke token.
|
|
321
|
+
- board create: args = [board name]. Create board.
|
|
322
|
+
- board delete: args = [board id]. Delete board.
|
|
323
|
+
- bucket create: args = [column title]. Optional "--board", "<id>".
|
|
324
|
+
- bucket update: args = [bucket id]. Optional "--title", "<t>", "--order", "<n>".
|
|
325
|
+
- bucket delete: args = [bucket id]. Delete column.
|
|
326
|
+
- assignee create: args = [name]. Optional "--type", "human" or "agent".
|
|
327
|
+
- assignee update: args = [assignee id]. Optional "--name", "--type".
|
|
328
|
+
- assignee delete: args = [assignee id]. Delete assignee.
|
|
329
|
+
|
|
330
|
+
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
331
|
+
|
|
332
|
+
async function parseNaturalLanguage(message) {
|
|
333
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.WADAH_OPENAI_API_KEY
|
|
334
|
+
if (!apiKey || !apiKey.trim()) {
|
|
335
|
+
throw new CliError('config', 'Set OPENAI_API_KEY or WADAH_OPENAI_API_KEY to use natural language (wadah do "...")')
|
|
336
|
+
}
|
|
337
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
headers: {
|
|
340
|
+
'Content-Type': 'application/json',
|
|
341
|
+
Authorization: `Bearer ${apiKey.trim()}`,
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
model: 'gpt-4o-mini',
|
|
345
|
+
messages: [
|
|
346
|
+
{ role: 'system', content: DO_SYSTEM_PROMPT },
|
|
347
|
+
{ role: 'user', content: String(message).trim() },
|
|
348
|
+
],
|
|
349
|
+
max_tokens: 200,
|
|
350
|
+
temperature: 0,
|
|
351
|
+
}),
|
|
352
|
+
})
|
|
353
|
+
const data = await res.json().catch(() => ({}))
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
const err = data.error?.message || data.message || res.statusText
|
|
356
|
+
throw new CliError('api_error', `LLM request failed: ${err}`)
|
|
357
|
+
}
|
|
358
|
+
const text = data.choices?.[0]?.message?.content?.trim() || ''
|
|
359
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
|
360
|
+
const raw = jsonMatch ? jsonMatch[0] : text
|
|
361
|
+
let parsed
|
|
362
|
+
try {
|
|
363
|
+
parsed = JSON.parse(raw)
|
|
364
|
+
} catch (_) {
|
|
365
|
+
throw new CliError('api_error', 'Could not parse LLM response as JSON')
|
|
366
|
+
}
|
|
367
|
+
const cmd = parsed.command && String(parsed.command).trim()
|
|
368
|
+
const args = Array.isArray(parsed.args) ? parsed.args.map(String) : []
|
|
369
|
+
return { command: cmd, args }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function runResolvedCommand(command, args) {
|
|
373
|
+
const cliPath = resolve(__dirname, 'cli.js')
|
|
374
|
+
const childArgs = [cliPath]
|
|
375
|
+
if (runtimeApiBase) childArgs.push('--api', runtimeApiBase)
|
|
376
|
+
if (runtimeToken) childArgs.push('--token', runtimeToken)
|
|
377
|
+
if (runtimeProfile) childArgs.push('--profile', runtimeProfile)
|
|
378
|
+
const cmdParts = String(command).split(/\s+/).filter(Boolean)
|
|
379
|
+
childArgs.push(...cmdParts, ...args)
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const child = spawn(process.execPath, childArgs, { stdio: 'inherit', env: process.env, shell: false })
|
|
382
|
+
child.on('error', reject)
|
|
383
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Exit ${code}`))))
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
269
387
|
function openBrowser(url) {
|
|
270
388
|
let cmd
|
|
271
389
|
let args
|
|
@@ -308,6 +426,27 @@ program.hook('preAction', (_, actionCommand) => {
|
|
|
308
426
|
runtimeQuietOutput = Boolean(opts.quiet)
|
|
309
427
|
})
|
|
310
428
|
|
|
429
|
+
// ── do (natural language) ──────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
program
|
|
432
|
+
.command('do <message>')
|
|
433
|
+
.description('Run a command from natural language (e.g. "add a task to fix the login bug")')
|
|
434
|
+
.action(async (message) => {
|
|
435
|
+
try {
|
|
436
|
+
const { command, args } = await parseNaturalLanguage(message)
|
|
437
|
+
if (!command) {
|
|
438
|
+
console.error(chalk.yellow('\nCould not understand. Try a direct command, e.g:\n wadah add "Fix bug"\n wadah folder create "Reports"\n wadah open\n'))
|
|
439
|
+
process.exit(1)
|
|
440
|
+
}
|
|
441
|
+
if (!runtimeQuietOutput && process.stdout.isTTY) {
|
|
442
|
+
console.log(chalk.gray(`Running: wadah ${command} ${args.join(' ')}\n`))
|
|
443
|
+
}
|
|
444
|
+
await runResolvedCommand(command, args)
|
|
445
|
+
} catch (err) {
|
|
446
|
+
handleError(err)
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
311
450
|
// ── tm login ──────────────────────────────────────────────────────────────────
|
|
312
451
|
|
|
313
452
|
program
|
|
@@ -563,7 +702,7 @@ program
|
|
|
563
702
|
.option('--completed', 'Show completed tasks instead')
|
|
564
703
|
.action(async (opts) => {
|
|
565
704
|
try {
|
|
566
|
-
const state = await
|
|
705
|
+
const state = await getState()
|
|
567
706
|
let tasks = state.tasks.filter((t) => !!t.completed === !!opts.completed)
|
|
568
707
|
|
|
569
708
|
if (!opts.all && !opts.assignee) {
|
|
@@ -602,7 +741,7 @@ program
|
|
|
602
741
|
if (opts.open && opts.completed) {
|
|
603
742
|
return fail('Use either --open or --completed, not both.')
|
|
604
743
|
}
|
|
605
|
-
const state = await
|
|
744
|
+
const state = await getState()
|
|
606
745
|
let tasks = state.tasks
|
|
607
746
|
if (opts.open) tasks = tasks.filter((t) => !t.completed)
|
|
608
747
|
if (opts.completed) tasks = tasks.filter((t) => !!t.completed)
|
|
@@ -627,6 +766,48 @@ program
|
|
|
627
766
|
} catch (err) { handleError(err) }
|
|
628
767
|
})
|
|
629
768
|
|
|
769
|
+
// ── tm search ─────────────────────────────────────────────────────────────────
|
|
770
|
+
|
|
771
|
+
program
|
|
772
|
+
.command('search <query>')
|
|
773
|
+
.description('Search tasks by title, notes or tags')
|
|
774
|
+
.option('--board <name>', 'Limit search to a specific board')
|
|
775
|
+
.option('--assignee <name>', 'Limit search to a specific assignee')
|
|
776
|
+
.option('--open', 'Only open tasks (default)')
|
|
777
|
+
.option('--all', 'Include completed tasks')
|
|
778
|
+
.action(async (query, opts) => {
|
|
779
|
+
try {
|
|
780
|
+
const state = await getState()
|
|
781
|
+
const q = query.toLowerCase()
|
|
782
|
+
let tasks = state.tasks
|
|
783
|
+
|
|
784
|
+
if (!opts.all) tasks = tasks.filter((t) => !t.completed)
|
|
785
|
+
if (opts.board) {
|
|
786
|
+
const b = findBoard(state.boards, opts.board)
|
|
787
|
+
if (!b) return fail(`Board not found: ${opts.board}`)
|
|
788
|
+
tasks = tasks.filter((t) => t.boardId === b.id)
|
|
789
|
+
}
|
|
790
|
+
if (opts.assignee) {
|
|
791
|
+
const a = findAssignee(state.assignees, opts.assignee)
|
|
792
|
+
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
793
|
+
tasks = tasks.filter((t) => t.assigneeId === a.id)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
tasks = tasks.filter((t) =>
|
|
797
|
+
t.title.toLowerCase().includes(q) ||
|
|
798
|
+
(t.notes ?? '').toLowerCase().includes(q) ||
|
|
799
|
+
(t.description ?? '').toLowerCase().includes(q) ||
|
|
800
|
+
(t.tags ?? []).some((tag) => tag.toLowerCase().includes(q))
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
if (!isMachineOutput() && tasks.length === 0) {
|
|
804
|
+
console.log(chalk.gray(`\n No tasks matching "${query}".\n`))
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
printTaskList(tasks, state)
|
|
808
|
+
} catch (err) { handleError(err) }
|
|
809
|
+
})
|
|
810
|
+
|
|
630
811
|
// ── tm requested ──────────────────────────────────────────────────────────────
|
|
631
812
|
|
|
632
813
|
program
|
|
@@ -634,7 +815,7 @@ program
|
|
|
634
815
|
.description('List open tasks you requested (assigned to others / agents)')
|
|
635
816
|
.action(async () => {
|
|
636
817
|
try {
|
|
637
|
-
const state = await
|
|
818
|
+
const state = await getState()
|
|
638
819
|
const myId = await resolveMyAssigneeId(state)
|
|
639
820
|
const tasks = state.tasks.filter((t) => t.requestedById === myId && !t.completed)
|
|
640
821
|
printTaskList(tasks, state)
|
|
@@ -653,9 +834,11 @@ program
|
|
|
653
834
|
.option('--repo <owner/repo>','GitHub repo')
|
|
654
835
|
.option('--url <url>', 'Issue / PR URL')
|
|
655
836
|
.option('--notes <text>', 'Notes')
|
|
837
|
+
.option('--blocks <id>', 'This task blocks another task (provide task id)')
|
|
838
|
+
.option('--blocked-by <id>', 'This task is blocked by another task (provide task id)')
|
|
656
839
|
.action(async (title, opts) => {
|
|
657
840
|
try {
|
|
658
|
-
const state = await
|
|
841
|
+
const state = await getState()
|
|
659
842
|
const myId = await resolveMyAssigneeId(state)
|
|
660
843
|
|
|
661
844
|
let assigneeId = myId
|
|
@@ -668,14 +851,19 @@ program
|
|
|
668
851
|
requestedById = myId
|
|
669
852
|
}
|
|
670
853
|
|
|
671
|
-
const bucket = opts.bucket
|
|
672
|
-
? state.buckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
|
|
673
|
-
: state.buckets[0]
|
|
674
|
-
|
|
675
854
|
const board = opts.board
|
|
676
855
|
? state.boards.find((b) => b.name.toLowerCase().includes(opts.board.toLowerCase()))
|
|
677
856
|
: state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
|
|
678
857
|
|
|
858
|
+
if (!board && opts.board) return fail(`Board not found: ${opts.board}`)
|
|
859
|
+
if (!board) console.warn(chalk.yellow(` ⚠ No board specified — defaulting to "${state.boards[0]?.name}". Set a default with: wadah config --default-board "<name>"`))
|
|
860
|
+
|
|
861
|
+
const boardBuckets = board ? state.buckets.filter((b) => b.boardId === board.id) : state.buckets
|
|
862
|
+
const bucket = opts.bucket
|
|
863
|
+
? boardBuckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
|
|
864
|
+
?? state.buckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
|
|
865
|
+
: boardBuckets[0] ?? state.buckets[0]
|
|
866
|
+
|
|
679
867
|
const task = await api('/api/tasks', {
|
|
680
868
|
method: 'POST',
|
|
681
869
|
body: JSON.stringify({
|
|
@@ -688,9 +876,27 @@ program
|
|
|
688
876
|
repo: opts.repo ?? null,
|
|
689
877
|
contextUrl: opts.url ?? null,
|
|
690
878
|
notes: opts.notes ?? '',
|
|
879
|
+
tags: [
|
|
880
|
+
...(opts.blocks ? [`blocks:${opts.blocks.slice(0, 8)}`] : []),
|
|
881
|
+
...(opts.blockedBy ? [`blocked-by:${opts.blockedBy.slice(0, 8)}`] : []),
|
|
882
|
+
],
|
|
691
883
|
}),
|
|
692
884
|
})
|
|
693
885
|
|
|
886
|
+
// If this task blocks another, tag the other task back
|
|
887
|
+
if (opts.blocks) {
|
|
888
|
+
try {
|
|
889
|
+
const other = await api(`/api/tasks/${opts.blocks}`)
|
|
890
|
+
const existingTags = other.tags ?? []
|
|
891
|
+
if (!existingTags.some((t) => t.startsWith('blocked-by:'))) {
|
|
892
|
+
await api(`/api/tasks/${opts.blocks}`, {
|
|
893
|
+
method: 'PATCH',
|
|
894
|
+
body: JSON.stringify({ tags: [...existingTags, `blocked-by:${task.id.slice(0, 8)}`] }),
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
} catch {}
|
|
898
|
+
}
|
|
899
|
+
|
|
694
900
|
const assigneeName = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
|
|
695
901
|
console.log(chalk.green('\n✓ Created') + ` ${chalk.bold(task.title)}`)
|
|
696
902
|
console.log(chalk.gray(` ID: ${task.id} · Assigned to: ${assigneeName}\n`))
|
|
@@ -735,7 +941,7 @@ program
|
|
|
735
941
|
.requiredOption('--to <name>', 'Assignee name')
|
|
736
942
|
.action(async (id, opts) => {
|
|
737
943
|
try {
|
|
738
|
-
const state = await
|
|
944
|
+
const state = await getState()
|
|
739
945
|
const a = findAssignee(state.assignees, opts.to)
|
|
740
946
|
if (!a) return fail(`Assignee not found: ${opts.to}`)
|
|
741
947
|
|
|
@@ -753,14 +959,18 @@ program
|
|
|
753
959
|
.description('Move a task to another column')
|
|
754
960
|
.action(async (id, bucketQuery) => {
|
|
755
961
|
try {
|
|
756
|
-
const state = await
|
|
962
|
+
const state = await getState()
|
|
757
963
|
const bucket = findBucket(state.buckets, bucketQuery)
|
|
758
964
|
if (!bucket) return fail(`Bucket not found: ${bucketQuery}`)
|
|
965
|
+
const patch = { bucketId: bucket.id }
|
|
966
|
+
if (bucket.boardId) patch.boardId = bucket.boardId
|
|
759
967
|
const task = await api(`/api/tasks/${id}`, {
|
|
760
968
|
method: 'PATCH',
|
|
761
|
-
body: JSON.stringify(
|
|
969
|
+
body: JSON.stringify(patch),
|
|
762
970
|
})
|
|
763
|
-
|
|
971
|
+
const boardName = state.boards.find((b) => b.id === bucket.boardId)?.name
|
|
972
|
+
const dest = boardName ? `${boardName} / ${bucket.title}` : bucket.title
|
|
973
|
+
console.log(chalk.green('\n✓ Moved') + ` ${chalk.bold(task.title)} → ${dest}\n`)
|
|
764
974
|
} catch (err) { handleError(err) }
|
|
765
975
|
})
|
|
766
976
|
|
|
@@ -778,6 +988,8 @@ program
|
|
|
778
988
|
.option('--board <name>', 'Set board')
|
|
779
989
|
.option('--bucket <name>', 'Set bucket')
|
|
780
990
|
.option('--completed <true|false>', 'Set completion state')
|
|
991
|
+
.option('--blocks <id>', 'Mark this task as blocking another task')
|
|
992
|
+
.option('--blocked-by <id>', 'Mark this task as blocked by another task')
|
|
781
993
|
.action(async (id, opts) => {
|
|
782
994
|
try {
|
|
783
995
|
const patch = {}
|
|
@@ -802,7 +1014,8 @@ program
|
|
|
802
1014
|
if (opts.clearDue) patch.dueDate = null
|
|
803
1015
|
|
|
804
1016
|
const needsStateLookup = !!(opts.assignee || opts.board || opts.bucket)
|
|
805
|
-
const state = needsStateLookup ? await
|
|
1017
|
+
const state = needsStateLookup ? await getState() : null
|
|
1018
|
+
|
|
806
1019
|
|
|
807
1020
|
if (opts.assignee) {
|
|
808
1021
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
@@ -815,12 +1028,32 @@ program
|
|
|
815
1028
|
patch.boardId = b.id
|
|
816
1029
|
}
|
|
817
1030
|
if (opts.bucket) {
|
|
818
|
-
const
|
|
1031
|
+
const targetBoardId = patch.boardId ?? null
|
|
1032
|
+
const scopedBuckets = targetBoardId
|
|
1033
|
+
? state.buckets.filter((b) => b.boardId === targetBoardId)
|
|
1034
|
+
: state.buckets
|
|
1035
|
+
const b = scopedBuckets.find((b) => b.id === opts.bucket || b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
|
|
1036
|
+
?? state.buckets.find((b) => b.id === opts.bucket || b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
|
|
819
1037
|
if (!b) return fail(`Bucket not found: ${opts.bucket}`)
|
|
820
1038
|
patch.bucketId = b.id
|
|
1039
|
+
if (!patch.boardId && b.boardId) patch.boardId = b.boardId
|
|
821
1040
|
}
|
|
822
1041
|
if (opts.completed !== undefined) patch.completed = parsedCompleted
|
|
823
1042
|
|
|
1043
|
+
// Handle relationship tags (blocks / blocked-by)
|
|
1044
|
+
if (opts.blocks || opts.blockedBy) {
|
|
1045
|
+
const currentTask = await api(`/api/tasks/${id}`)
|
|
1046
|
+
const currentTags = currentTask.tags ?? []
|
|
1047
|
+
const newTags = [...currentTags]
|
|
1048
|
+
if (opts.blocks && !newTags.some((t) => t.startsWith('blocks:'))) {
|
|
1049
|
+
newTags.push(`blocks:${opts.blocks.slice(0, 8)}`)
|
|
1050
|
+
}
|
|
1051
|
+
if (opts.blockedBy && !newTags.some((t) => t.startsWith('blocked-by:'))) {
|
|
1052
|
+
newTags.push(`blocked-by:${opts.blockedBy.slice(0, 8)}`)
|
|
1053
|
+
}
|
|
1054
|
+
patch.tags = newTags
|
|
1055
|
+
}
|
|
1056
|
+
|
|
824
1057
|
if (Object.keys(patch).length === 0) {
|
|
825
1058
|
return fail('Nothing to update. Run wadah update --help')
|
|
826
1059
|
}
|
|
@@ -868,7 +1101,17 @@ program
|
|
|
868
1101
|
if (task.dueDate) console.log(chalk.gray(` Due: ${formatDate(task.dueDate)}`))
|
|
869
1102
|
if (task.repo) console.log(chalk.gray(` Repo: ${task.repo}`))
|
|
870
1103
|
if (task.contextUrl) console.log(chalk.gray(` Link: ${task.contextUrl}`))
|
|
871
|
-
if (task.tags?.length)
|
|
1104
|
+
if (task.tags?.length) {
|
|
1105
|
+
const relTags = task.tags.filter((t) => t.startsWith('blocks:') || t.startsWith('blocked-by:'))
|
|
1106
|
+
const plainTags = task.tags.filter((t) => !t.startsWith('blocks:') && !t.startsWith('blocked-by:'))
|
|
1107
|
+
if (plainTags.length) console.log(chalk.gray(` Tags: #${plainTags.join(' #')}`))
|
|
1108
|
+
if (relTags.length) {
|
|
1109
|
+
const blocking = relTags.filter((t) => t.startsWith('blocks:')).map((t) => t.replace('blocks:', ''))
|
|
1110
|
+
const blockedBy = relTags.filter((t) => t.startsWith('blocked-by:')).map((t) => t.replace('blocked-by:', ''))
|
|
1111
|
+
if (blocking.length) console.log(chalk.red(` Blocks: ${blocking.join(', ')}`))
|
|
1112
|
+
if (blockedBy.length) console.log(chalk.yellow(` Blocked by: ${blockedBy.join(', ')}`))
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
872
1115
|
if (task.description) { console.log(); console.log(` ${task.description}`) }
|
|
873
1116
|
if (task.notes) { console.log(chalk.gray('\n Notes:')); console.log(` ${task.notes}`) }
|
|
874
1117
|
if (task.subtasks?.length) {
|
|
@@ -911,7 +1154,17 @@ program
|
|
|
911
1154
|
.description('List all boards')
|
|
912
1155
|
.action(async () => {
|
|
913
1156
|
try {
|
|
914
|
-
const state = await
|
|
1157
|
+
const state = await getState()
|
|
1158
|
+
if (isMachineOutput()) {
|
|
1159
|
+
const result = state.boards.map((b) => ({
|
|
1160
|
+
id: b.id,
|
|
1161
|
+
name: b.name,
|
|
1162
|
+
openCount: state.tasks.filter((t) => t.boardId === b.id && !t.completed).length,
|
|
1163
|
+
doneCount: state.tasks.filter((t) => t.boardId === b.id && t.completed).length,
|
|
1164
|
+
}))
|
|
1165
|
+
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
915
1168
|
console.log()
|
|
916
1169
|
state.boards.forEach((b) => {
|
|
917
1170
|
const open = state.tasks.filter((t) => t.boardId === b.id && !t.completed).length
|
|
@@ -927,8 +1180,20 @@ program
|
|
|
927
1180
|
.description('List all columns (buckets)')
|
|
928
1181
|
.action(async () => {
|
|
929
1182
|
try {
|
|
930
|
-
const state = await
|
|
1183
|
+
const state = await getState()
|
|
931
1184
|
const boardMap = Object.fromEntries(state.boards.map((b) => [b.id, b.name]))
|
|
1185
|
+
if (isMachineOutput()) {
|
|
1186
|
+
const result = state.buckets.map((b) => ({
|
|
1187
|
+
id: b.id,
|
|
1188
|
+
title: b.title,
|
|
1189
|
+
boardId: b.boardId,
|
|
1190
|
+
boardName: boardMap[b.boardId] ?? '—',
|
|
1191
|
+
openCount: state.tasks.filter((t) => t.bucketId === b.id && !t.completed).length,
|
|
1192
|
+
doneCount: state.tasks.filter((t) => t.bucketId === b.id && t.completed).length,
|
|
1193
|
+
}))
|
|
1194
|
+
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1195
|
+
return
|
|
1196
|
+
}
|
|
932
1197
|
console.log()
|
|
933
1198
|
state.buckets.forEach((b) => {
|
|
934
1199
|
const open = state.tasks.filter((t) => t.bucketId === b.id && !t.completed).length
|
|
@@ -944,7 +1209,18 @@ program
|
|
|
944
1209
|
.description('List all assignees')
|
|
945
1210
|
.action(async () => {
|
|
946
1211
|
try {
|
|
947
|
-
const state = await
|
|
1212
|
+
const state = await getState()
|
|
1213
|
+
if (isMachineOutput()) {
|
|
1214
|
+
const result = state.assignees.map((a) => ({
|
|
1215
|
+
id: a.id,
|
|
1216
|
+
name: a.name,
|
|
1217
|
+
type: a.type ?? 'human',
|
|
1218
|
+
openCount: state.tasks.filter((t) => t.assigneeId === a.id && !t.completed).length,
|
|
1219
|
+
doneCount: state.tasks.filter((t) => t.assigneeId === a.id && t.completed).length,
|
|
1220
|
+
}))
|
|
1221
|
+
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
948
1224
|
console.log()
|
|
949
1225
|
state.assignees.forEach((a) => {
|
|
950
1226
|
const open = state.tasks.filter((t) => t.assigneeId === a.id && !t.completed).length
|
|
@@ -955,12 +1231,509 @@ program
|
|
|
955
1231
|
} catch (err) { handleError(err) }
|
|
956
1232
|
})
|
|
957
1233
|
|
|
1234
|
+
// ── board CRUD ────────────────────────────────────────────────────────────────
|
|
1235
|
+
|
|
1236
|
+
const boardCmd = program.command('board').description('Create or delete a board (list: wadah boards)')
|
|
1237
|
+
|
|
1238
|
+
boardCmd
|
|
1239
|
+
.command('create <name>')
|
|
1240
|
+
.description('Create a new board')
|
|
1241
|
+
.action(async (name) => {
|
|
1242
|
+
try {
|
|
1243
|
+
const board = await api('/api/boards', { method: 'POST', body: JSON.stringify({ name: String(name).trim() }) })
|
|
1244
|
+
if (isMachineOutput()) {
|
|
1245
|
+
console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
|
|
1246
|
+
} else {
|
|
1247
|
+
console.log(chalk.green('\n✓ Board created') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1248
|
+
}
|
|
1249
|
+
} catch (err) { handleError(err) }
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
boardCmd
|
|
1253
|
+
.command('delete <id>')
|
|
1254
|
+
.description('Delete a board (cannot delete the last one)')
|
|
1255
|
+
.action(async (id) => {
|
|
1256
|
+
try {
|
|
1257
|
+
await api(`/api/boards/${id}`, { method: 'DELETE' })
|
|
1258
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Board deleted\n'))
|
|
1259
|
+
} catch (err) { handleError(err) }
|
|
1260
|
+
})
|
|
1261
|
+
|
|
1262
|
+
// ── bucket CRUD ──────────────────────────────────────────────────────────────
|
|
1263
|
+
|
|
1264
|
+
const bucketCmd = program.command('bucket').description('Create, update, or delete a column/bucket (list: wadah buckets)')
|
|
1265
|
+
|
|
1266
|
+
bucketCmd
|
|
1267
|
+
.command('create <title>')
|
|
1268
|
+
.description('Create a new bucket (column)')
|
|
1269
|
+
.option('--board <id>', 'Board id (optional)')
|
|
1270
|
+
.action(async function (title) {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = { title: String(title).trim() }
|
|
1273
|
+
if (this.opts().board) body.boardId = this.opts().board
|
|
1274
|
+
const bucket = await api('/api/buckets', { method: 'POST', body: JSON.stringify(body) })
|
|
1275
|
+
if (isMachineOutput()) {
|
|
1276
|
+
console.log(JSON.stringify(bucket, null, runtimeJsonOutput ? 2 : 0))
|
|
1277
|
+
} else {
|
|
1278
|
+
console.log(chalk.green('\n✓ Bucket created') + ` ${chalk.bold(bucket.title)} ${chalk.gray(bucket.id?.slice(0, 8))}\n`)
|
|
1279
|
+
}
|
|
1280
|
+
} catch (err) { handleError(err) }
|
|
1281
|
+
})
|
|
1282
|
+
|
|
1283
|
+
bucketCmd
|
|
1284
|
+
.command('update <id>')
|
|
1285
|
+
.description('Update a bucket')
|
|
1286
|
+
.option('--title <text>', 'New title')
|
|
1287
|
+
.option('--order <n>', 'Order (number)', parseInt)
|
|
1288
|
+
.action(async function (id) {
|
|
1289
|
+
const opts = this.opts()
|
|
1290
|
+
try {
|
|
1291
|
+
const patch = {}
|
|
1292
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1293
|
+
if (opts.order !== undefined && !Number.isNaN(opts.order)) patch.order = opts.order
|
|
1294
|
+
if (Object.keys(patch).length === 0) return fail('Provide --title or --order to update')
|
|
1295
|
+
await api(`/api/buckets/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1296
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Bucket updated\n'))
|
|
1297
|
+
} catch (err) { handleError(err) }
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
bucketCmd
|
|
1301
|
+
.command('delete <id>')
|
|
1302
|
+
.description('Delete a bucket (tasks move to another column)')
|
|
1303
|
+
.action(async (id) => {
|
|
1304
|
+
try {
|
|
1305
|
+
await api(`/api/buckets/${id}`, { method: 'DELETE' })
|
|
1306
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Bucket deleted\n'))
|
|
1307
|
+
} catch (err) { handleError(err) }
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
// ── assignee CRUD ────────────────────────────────────────────────────────────
|
|
1311
|
+
|
|
1312
|
+
const assigneeCmd = program.command('assignee').description('Create, update, or delete an assignee (list: wadah assignees)')
|
|
1313
|
+
|
|
1314
|
+
assigneeCmd
|
|
1315
|
+
.command('create <name>')
|
|
1316
|
+
.description('Create a new assignee')
|
|
1317
|
+
.option('--type <type>', 'Type: human or agent', 'human')
|
|
1318
|
+
.action(async function (name) {
|
|
1319
|
+
try {
|
|
1320
|
+
const body = { name: String(name).trim(), type: this.opts().type || 'human' }
|
|
1321
|
+
const assignee = await api('/api/assignees', { method: 'POST', body: JSON.stringify(body) })
|
|
1322
|
+
if (isMachineOutput()) {
|
|
1323
|
+
console.log(JSON.stringify(assignee, null, runtimeJsonOutput ? 2 : 0))
|
|
1324
|
+
} else {
|
|
1325
|
+
console.log(chalk.green('\n✓ Assignee created') + ` ${chalk.bold(assignee.name)} ${chalk.gray(assignee.id?.slice(0, 8))}\n`)
|
|
1326
|
+
}
|
|
1327
|
+
} catch (err) { handleError(err) }
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
assigneeCmd
|
|
1331
|
+
.command('update <id>')
|
|
1332
|
+
.description('Update an assignee')
|
|
1333
|
+
.option('--name <text>', 'New name')
|
|
1334
|
+
.option('--type <type>', 'Type: human or agent')
|
|
1335
|
+
.action(async function (id) {
|
|
1336
|
+
const opts = this.opts()
|
|
1337
|
+
try {
|
|
1338
|
+
const patch = {}
|
|
1339
|
+
if (opts.name !== undefined) patch.name = opts.name
|
|
1340
|
+
if (opts.type !== undefined) patch.type = opts.type
|
|
1341
|
+
if (Object.keys(patch).length === 0) return fail('Provide --name or --type to update')
|
|
1342
|
+
await api(`/api/assignees/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1343
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Assignee updated\n'))
|
|
1344
|
+
} catch (err) { handleError(err) }
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
assigneeCmd
|
|
1348
|
+
.command('delete <id>')
|
|
1349
|
+
.description('Delete an assignee (tasks unassigned)')
|
|
1350
|
+
.action(async (id) => {
|
|
1351
|
+
try {
|
|
1352
|
+
await api(`/api/assignees/${id}`, { method: 'DELETE' })
|
|
1353
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Assignee deleted\n'))
|
|
1354
|
+
} catch (err) { handleError(err) }
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
program
|
|
1358
|
+
.command('folders')
|
|
1359
|
+
.description('List all folders (Files tab)')
|
|
1360
|
+
.action(async () => {
|
|
1361
|
+
try {
|
|
1362
|
+
const folders = await api('/api/folders').catch(() => [])
|
|
1363
|
+
const list = Array.isArray(folders) ? folders : []
|
|
1364
|
+
if (isMachineOutput()) {
|
|
1365
|
+
console.log(JSON.stringify(list, null, runtimeJsonOutput ? 2 : 0))
|
|
1366
|
+
} else if (list.length === 0) {
|
|
1367
|
+
console.log(chalk.gray('\nNo folders. Create one: wadah folder create "Name"\n'))
|
|
1368
|
+
} else {
|
|
1369
|
+
console.log()
|
|
1370
|
+
list.forEach((f) => console.log(` ${chalk.bold(f.name)} ${chalk.gray(`id: ${f.id}`)}`))
|
|
1371
|
+
console.log()
|
|
1372
|
+
}
|
|
1373
|
+
} catch (err) { handleError(err) }
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
program
|
|
1377
|
+
.command('files')
|
|
1378
|
+
.description('List all files in the workspace')
|
|
1379
|
+
.option('--folder <id>', 'Filter by folder id')
|
|
1380
|
+
.action(async function () {
|
|
1381
|
+
const opts = this.opts()
|
|
1382
|
+
try {
|
|
1383
|
+
const state = await getState()
|
|
1384
|
+
let files = state.files ?? []
|
|
1385
|
+
if (opts.folder) files = files.filter((f) => (f.folderId || f.folder_id) === opts.folder)
|
|
1386
|
+
if (isMachineOutput()) {
|
|
1387
|
+
console.log(JSON.stringify(files, null, runtimeJsonOutput ? 2 : 0))
|
|
1388
|
+
} else if (files.length === 0) {
|
|
1389
|
+
console.log(chalk.gray('\nNo files. Upload one: wadah upload <path>\n'))
|
|
1390
|
+
} else {
|
|
1391
|
+
console.log()
|
|
1392
|
+
files.forEach((f) => {
|
|
1393
|
+
const name = f.name || f.id
|
|
1394
|
+
const url = f.storageUrl || f.storage_url
|
|
1395
|
+
console.log(` ${chalk.bold(name)} ${chalk.gray(url ? url.slice(0, 50) + '…' : f.id)}`)
|
|
1396
|
+
})
|
|
1397
|
+
console.log()
|
|
1398
|
+
}
|
|
1399
|
+
} catch (err) { handleError(err) }
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
program
|
|
1403
|
+
.command('members')
|
|
1404
|
+
.description('List workspace members')
|
|
1405
|
+
.action(async () => {
|
|
1406
|
+
try {
|
|
1407
|
+
const members = await api('/api/workspace/members')
|
|
1408
|
+
const list = Array.isArray(members) ? members : []
|
|
1409
|
+
if (isMachineOutput()) {
|
|
1410
|
+
console.log(JSON.stringify(list, null, runtimeJsonOutput ? 2 : 0))
|
|
1411
|
+
} else if (list.length === 0) {
|
|
1412
|
+
console.log(chalk.gray('\nNo members.\n'))
|
|
1413
|
+
} else {
|
|
1414
|
+
console.log()
|
|
1415
|
+
list.forEach((m) => console.log(` ${chalk.bold(m.role)} ${chalk.gray(m.user_id?.slice(0, 8) ?? '—')}`))
|
|
1416
|
+
console.log()
|
|
1417
|
+
}
|
|
1418
|
+
} catch (err) { handleError(err) }
|
|
1419
|
+
})
|
|
1420
|
+
|
|
1421
|
+
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1422
|
+
|
|
1423
|
+
program
|
|
1424
|
+
.command('agent-tokens')
|
|
1425
|
+
.description('List agent tokens for this workspace')
|
|
1426
|
+
.action(async () => {
|
|
1427
|
+
try {
|
|
1428
|
+
const list = await api('/api/agent-tokens').catch(() => [])
|
|
1429
|
+
const arr = Array.isArray(list) ? list : []
|
|
1430
|
+
if (isMachineOutput()) {
|
|
1431
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1432
|
+
} else if (arr.length === 0) {
|
|
1433
|
+
console.log(chalk.gray('\nNo agent tokens. Create one: wadah agent-token create "Bot name"\n'))
|
|
1434
|
+
} else {
|
|
1435
|
+
console.log()
|
|
1436
|
+
arr.forEach((t) => {
|
|
1437
|
+
const as = t.assigneeName ? chalk.gray(` · acts as ${t.assigneeName}`) : ''
|
|
1438
|
+
console.log(` ${chalk.bold(t.name)} ${chalk.gray(`id: ${t.id?.slice(0, 8)}${as}`)}`)
|
|
1439
|
+
})
|
|
1440
|
+
console.log()
|
|
1441
|
+
}
|
|
1442
|
+
} catch (err) { handleError(err) }
|
|
1443
|
+
})
|
|
1444
|
+
|
|
1445
|
+
const agentTokenCmd = program.command('agent-token').description('Create or delete an agent token (for CLI/CI)')
|
|
1446
|
+
|
|
1447
|
+
agentTokenCmd
|
|
1448
|
+
.command('create [name]')
|
|
1449
|
+
.description('Create an agent token; token is shown once — store it as TASK_MANAGER_TOKEN')
|
|
1450
|
+
.option('--name <text>', 'Token name (if not passed as argument)')
|
|
1451
|
+
.option('--assignee-name <text>', 'Display name for the agent (default: same as name)')
|
|
1452
|
+
.action(async function (nameArg) {
|
|
1453
|
+
const opts = this.opts()
|
|
1454
|
+
const name = (nameArg || opts.name || 'CLI Agent').trim()
|
|
1455
|
+
try {
|
|
1456
|
+
const body = { name }
|
|
1457
|
+
if (opts.assigneeName) body.assigneeName = opts.assigneeName
|
|
1458
|
+
const result = await api('/api/agent-tokens', { method: 'POST', body: JSON.stringify(body) })
|
|
1459
|
+
if (isMachineOutput()) {
|
|
1460
|
+
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1461
|
+
} else {
|
|
1462
|
+
console.log(chalk.green('\n✓ Agent token created'))
|
|
1463
|
+
console.log(chalk.gray(` Name: ${result.name} Id: ${result.id?.slice(0, 8)} Acts as: ${result.assigneeName ?? result.name}`))
|
|
1464
|
+
console.log(chalk.yellow('\n Token (save it — shown once):'))
|
|
1465
|
+
console.log(` ${result.token}`)
|
|
1466
|
+
console.log(chalk.gray('\n Use: export TASK_MANAGER_TOKEN="<token>"\n'))
|
|
1467
|
+
}
|
|
1468
|
+
} catch (err) { handleError(err) }
|
|
1469
|
+
})
|
|
1470
|
+
|
|
1471
|
+
agentTokenCmd
|
|
1472
|
+
.command('delete <id>')
|
|
1473
|
+
.description('Revoke an agent token')
|
|
1474
|
+
.action(async (id) => {
|
|
1475
|
+
try {
|
|
1476
|
+
await api(`/api/agent-tokens/${id}`, { method: 'DELETE' })
|
|
1477
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Agent token revoked\n'))
|
|
1478
|
+
} catch (err) { handleError(err) }
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1481
|
+
// ── calendar events ──────────────────────────────────────────────────────────
|
|
1482
|
+
|
|
1483
|
+
const calendarCmd = program
|
|
1484
|
+
.command('calendar')
|
|
1485
|
+
.description('List calendar events (optional: --assignee, --task, --from, --to)')
|
|
1486
|
+
.option('--assignee <id>', 'Filter by assignee id')
|
|
1487
|
+
.option('--task <id>', 'Filter by task id')
|
|
1488
|
+
.option('--from <iso>', 'From date (ISO)')
|
|
1489
|
+
.option('--to <iso>', 'To date (ISO)')
|
|
1490
|
+
.action(async function () {
|
|
1491
|
+
const opts = this.opts()
|
|
1492
|
+
try {
|
|
1493
|
+
let path = '/api/calendar-events'
|
|
1494
|
+
const q = []
|
|
1495
|
+
if (opts.assignee) q.push(`assigneeId=${encodeURIComponent(opts.assignee)}`)
|
|
1496
|
+
if (opts.task) q.push(`taskId=${encodeURIComponent(opts.task)}`)
|
|
1497
|
+
if (opts.from) q.push(`from=${encodeURIComponent(opts.from)}`)
|
|
1498
|
+
if (opts.to) q.push(`to=${encodeURIComponent(opts.to)}`)
|
|
1499
|
+
if (q.length) path += '?' + q.join('&')
|
|
1500
|
+
const list = await api(path).catch(() => [])
|
|
1501
|
+
const arr = Array.isArray(list) ? list : []
|
|
1502
|
+
if (isMachineOutput()) {
|
|
1503
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1504
|
+
} else if (arr.length === 0) {
|
|
1505
|
+
console.log(chalk.gray('\nNo calendar events.\n'))
|
|
1506
|
+
} else {
|
|
1507
|
+
console.log()
|
|
1508
|
+
arr.forEach((e) => {
|
|
1509
|
+
const start = e.startTime ? new Date(e.startTime).toLocaleString() : '—'
|
|
1510
|
+
const end = e.endTime ? new Date(e.endTime).toLocaleString() : '—'
|
|
1511
|
+
console.log(` ${chalk.bold(e.title || 'Untitled')} ${chalk.gray(`${start} → ${end}`)} ${chalk.gray(`id: ${e.id?.slice(0, 8)}`)}`)
|
|
1512
|
+
})
|
|
1513
|
+
console.log()
|
|
1514
|
+
}
|
|
1515
|
+
} catch (err) { handleError(err) }
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
calendarCmd
|
|
1519
|
+
.command('add')
|
|
1520
|
+
.description('Create a calendar event (--start and --end required, ISO or YYYY-MM-DD)')
|
|
1521
|
+
.option('--title <text>', 'Event title', 'Work block')
|
|
1522
|
+
.option('--start <iso>', 'Start time (ISO or YYYY-MM-DD)')
|
|
1523
|
+
.option('--end <iso>', 'End time (ISO or YYYY-MM-DD)')
|
|
1524
|
+
.option('--task <id>', 'Task id')
|
|
1525
|
+
.option('--assignee <id>', 'Assignee id')
|
|
1526
|
+
.option('--color <hex>', 'Color')
|
|
1527
|
+
.action(async function () {
|
|
1528
|
+
const opts = this.opts()
|
|
1529
|
+
try {
|
|
1530
|
+
if (!opts.start || !opts.end) return fail('--start and --end are required')
|
|
1531
|
+
const startTime = new Date(opts.start).toISOString()
|
|
1532
|
+
const endTime = new Date(opts.end).toISOString()
|
|
1533
|
+
const body = { title: opts.title, startTime, endTime }
|
|
1534
|
+
if (opts.task) body.taskId = opts.task
|
|
1535
|
+
if (opts.assignee) body.assigneeId = opts.assignee
|
|
1536
|
+
if (opts.color) body.color = opts.color
|
|
1537
|
+
const event = await api('/api/calendar-events', { method: 'POST', body: JSON.stringify(body) })
|
|
1538
|
+
if (isMachineOutput()) {
|
|
1539
|
+
console.log(JSON.stringify(event, null, runtimeJsonOutput ? 2 : 0))
|
|
1540
|
+
} else {
|
|
1541
|
+
console.log(chalk.green('\n✓ Event created') + ` ${chalk.bold(event.title)} ${chalk.gray(event.id?.slice(0, 8))}\n`)
|
|
1542
|
+
}
|
|
1543
|
+
} catch (err) { handleError(err) }
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
calendarCmd
|
|
1547
|
+
.command('update <id>')
|
|
1548
|
+
.description('Update a calendar event')
|
|
1549
|
+
.option('--title <text>', 'Event title')
|
|
1550
|
+
.option('--start <iso>', 'Start time')
|
|
1551
|
+
.option('--end <iso>', 'End time')
|
|
1552
|
+
.option('--task <id>', 'Task id')
|
|
1553
|
+
.option('--assignee <id>', 'Assignee id')
|
|
1554
|
+
.option('--color <hex>', 'Color')
|
|
1555
|
+
.action(async function (id) {
|
|
1556
|
+
const opts = this.opts()
|
|
1557
|
+
try {
|
|
1558
|
+
const patch = {}
|
|
1559
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1560
|
+
if (opts.start !== undefined) patch.startTime = new Date(opts.start).toISOString()
|
|
1561
|
+
if (opts.end !== undefined) patch.endTime = new Date(opts.end).toISOString()
|
|
1562
|
+
if (opts.task !== undefined) patch.taskId = opts.task
|
|
1563
|
+
if (opts.assignee !== undefined) patch.assigneeId = opts.assignee
|
|
1564
|
+
if (opts.color !== undefined) patch.color = opts.color
|
|
1565
|
+
if (Object.keys(patch).length === 0) return fail('Provide at least one option to update')
|
|
1566
|
+
await api(`/api/calendar-events/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1567
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Event updated\n'))
|
|
1568
|
+
} catch (err) { handleError(err) }
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
calendarCmd
|
|
1572
|
+
.command('delete <id>')
|
|
1573
|
+
.description('Delete a calendar event')
|
|
1574
|
+
.action(async (id) => {
|
|
1575
|
+
try {
|
|
1576
|
+
await api(`/api/calendar-events/${id}`, { method: 'DELETE' })
|
|
1577
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Event deleted\n'))
|
|
1578
|
+
} catch (err) { handleError(err) }
|
|
1579
|
+
})
|
|
1580
|
+
|
|
1581
|
+
// ── workspace docs ─────────────────────────────────────────────────────────────
|
|
1582
|
+
|
|
1583
|
+
program
|
|
1584
|
+
.command('docs')
|
|
1585
|
+
.description('List workspace docs')
|
|
1586
|
+
.action(async () => {
|
|
1587
|
+
try {
|
|
1588
|
+
const list = await api('/api/docs').catch(() => [])
|
|
1589
|
+
const arr = Array.isArray(list) ? list : []
|
|
1590
|
+
if (isMachineOutput()) {
|
|
1591
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1592
|
+
} else if (arr.length === 0) {
|
|
1593
|
+
console.log(chalk.gray('\nNo docs.\n'))
|
|
1594
|
+
} else {
|
|
1595
|
+
console.log()
|
|
1596
|
+
arr.forEach((d) => console.log(` ${chalk.bold(d.title || 'Untitled')} ${chalk.gray(`id: ${d.id?.slice(0, 8)}`)}`))
|
|
1597
|
+
console.log()
|
|
1598
|
+
}
|
|
1599
|
+
} catch (err) { handleError(err) }
|
|
1600
|
+
})
|
|
1601
|
+
|
|
1602
|
+
const docCmd = program.command('doc').description('Workspace doc (create, show, update, delete)')
|
|
1603
|
+
|
|
1604
|
+
docCmd
|
|
1605
|
+
.command('create [title]')
|
|
1606
|
+
.description('Create a workspace doc')
|
|
1607
|
+
.option('--title <text>', 'Doc title (if not passed as argument)')
|
|
1608
|
+
.option('--parent <id>', 'Parent doc id')
|
|
1609
|
+
.option('--emoji <char>', 'Emoji')
|
|
1610
|
+
.action(async function (titleArg) {
|
|
1611
|
+
const opts = this.opts()
|
|
1612
|
+
const title = (titleArg || opts.title || 'Untitled').trim()
|
|
1613
|
+
try {
|
|
1614
|
+
const body = { title }
|
|
1615
|
+
if (opts.parent) body.parentId = opts.parent
|
|
1616
|
+
if (opts.emoji) body.emoji = opts.emoji
|
|
1617
|
+
const doc = await api('/api/docs', { method: 'POST', body: JSON.stringify(body) })
|
|
1618
|
+
if (isMachineOutput()) {
|
|
1619
|
+
console.log(JSON.stringify(doc, null, runtimeJsonOutput ? 2 : 0))
|
|
1620
|
+
} else {
|
|
1621
|
+
console.log(chalk.green('\n✓ Doc created') + ` ${chalk.bold(doc.title)} ${chalk.gray(doc.id?.slice(0, 8))}\n`)
|
|
1622
|
+
}
|
|
1623
|
+
} catch (err) { handleError(err) }
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
docCmd
|
|
1627
|
+
.command('show <id>')
|
|
1628
|
+
.description('Show a workspace doc')
|
|
1629
|
+
.action(async (id) => {
|
|
1630
|
+
try {
|
|
1631
|
+
const doc = await api(`/api/docs/${id}`)
|
|
1632
|
+
if (isMachineOutput()) {
|
|
1633
|
+
console.log(JSON.stringify(doc, null, runtimeJsonOutput ? 2 : 0))
|
|
1634
|
+
} else {
|
|
1635
|
+
console.log()
|
|
1636
|
+
console.log(chalk.bold(doc.title || 'Untitled'))
|
|
1637
|
+
console.log(chalk.gray(`id: ${doc.id}`))
|
|
1638
|
+
if (doc.content && typeof doc.content === 'object') {
|
|
1639
|
+
const text = JSON.stringify(doc.content)
|
|
1640
|
+
console.log(chalk.gray(text.slice(0, 200) + (text.length > 200 ? '…' : '')))
|
|
1641
|
+
}
|
|
1642
|
+
console.log()
|
|
1643
|
+
}
|
|
1644
|
+
} catch (err) { handleError(err) }
|
|
1645
|
+
})
|
|
1646
|
+
|
|
1647
|
+
docCmd
|
|
1648
|
+
.command('update <id>')
|
|
1649
|
+
.description('Update a workspace doc')
|
|
1650
|
+
.option('--title <text>', 'Doc title')
|
|
1651
|
+
.option('--content <json>', 'Content as JSON string')
|
|
1652
|
+
.option('--emoji <char>', 'Emoji')
|
|
1653
|
+
.option('--parent <id>', 'Parent doc id')
|
|
1654
|
+
.action(async function (id) {
|
|
1655
|
+
const opts = this.opts()
|
|
1656
|
+
try {
|
|
1657
|
+
const patch = {}
|
|
1658
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1659
|
+
if (opts.content !== undefined) {
|
|
1660
|
+
try {
|
|
1661
|
+
patch.content = typeof opts.content === 'string' ? JSON.parse(opts.content) : opts.content
|
|
1662
|
+
} catch (_) {
|
|
1663
|
+
return fail('--content must be valid JSON')
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (opts.emoji !== undefined) patch.emoji = opts.emoji
|
|
1667
|
+
if (opts.parent !== undefined) patch.parentId = opts.parent
|
|
1668
|
+
if (Object.keys(patch).length === 0) return fail('Provide at least one option to update')
|
|
1669
|
+
await api(`/api/docs/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1670
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Doc updated\n'))
|
|
1671
|
+
} catch (err) { handleError(err) }
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
docCmd
|
|
1675
|
+
.command('delete <id>')
|
|
1676
|
+
.description('Delete a workspace doc')
|
|
1677
|
+
.action(async (id) => {
|
|
1678
|
+
try {
|
|
1679
|
+
await api(`/api/docs/${id}`, { method: 'DELETE' })
|
|
1680
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Doc deleted\n'))
|
|
1681
|
+
} catch (err) { handleError(err) }
|
|
1682
|
+
})
|
|
1683
|
+
|
|
1684
|
+
program
|
|
1685
|
+
.command('folder create <name>')
|
|
1686
|
+
.alias('mkdir')
|
|
1687
|
+
.description('Create a folder in the workspace (Files tab)')
|
|
1688
|
+
.action(async (name) => {
|
|
1689
|
+
try {
|
|
1690
|
+
const folder = await api('/api/folders', {
|
|
1691
|
+
method: 'POST',
|
|
1692
|
+
body: JSON.stringify({ name: String(name).trim() }),
|
|
1693
|
+
})
|
|
1694
|
+
if (isMachineOutput()) {
|
|
1695
|
+
console.log(JSON.stringify(folder, null, runtimeJsonOutput ? 2 : 0))
|
|
1696
|
+
} else {
|
|
1697
|
+
console.log(chalk.green('\n✓ Folder created') + ` ${chalk.bold(folder.name)} ${chalk.gray(`(id: ${folder.id})\n`)}`)
|
|
1698
|
+
}
|
|
1699
|
+
} catch (err) { handleError(err) }
|
|
1700
|
+
})
|
|
1701
|
+
|
|
1702
|
+
program
|
|
1703
|
+
.command('upload <path>')
|
|
1704
|
+
.description('Upload a file to the workspace (optional: --folder <id>)')
|
|
1705
|
+
.option('--folder <id>', 'Put the file in this folder id')
|
|
1706
|
+
.action(async (pathArg, opts) => {
|
|
1707
|
+
try {
|
|
1708
|
+
await ensureAuth()
|
|
1709
|
+
const filePath = resolve(pathArg)
|
|
1710
|
+
if (!existsSync(filePath)) return fail(`File not found: ${filePath}`)
|
|
1711
|
+
const form = new FormData()
|
|
1712
|
+
form.append('file', createReadStream(filePath), { filename: basename(filePath) })
|
|
1713
|
+
if (opts.folder) form.append('folderId', String(opts.folder).trim())
|
|
1714
|
+
const headers = { ...authHeaders() }
|
|
1715
|
+
delete headers['Content-Type']
|
|
1716
|
+
const res = await fetch(`${getApiBase()}/api/files/upload`, {
|
|
1717
|
+
method: 'POST',
|
|
1718
|
+
headers: { ...headers, ...form.getHeaders() },
|
|
1719
|
+
body: form,
|
|
1720
|
+
})
|
|
1721
|
+
const data = await res.json().catch(() => ({}))
|
|
1722
|
+
if (!res.ok) throw new CliError(data.error ?? 'Upload failed', data.error ?? res.statusText)
|
|
1723
|
+
if (isMachineOutput()) {
|
|
1724
|
+
console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
|
|
1725
|
+
} else {
|
|
1726
|
+
console.log(chalk.green('\n✓ Uploaded') + ` ${chalk.bold(data.name)} ${chalk.gray(`→ ${data.storageUrl ?? data.id}\n`)}`)
|
|
1727
|
+
}
|
|
1728
|
+
} catch (err) { handleError(err) }
|
|
1729
|
+
})
|
|
1730
|
+
|
|
958
1731
|
program
|
|
959
1732
|
.command('state')
|
|
960
1733
|
.description('Print full workspace state as JSON')
|
|
961
1734
|
.action(async () => {
|
|
962
1735
|
try {
|
|
963
|
-
const state = await
|
|
1736
|
+
const state = await getState()
|
|
964
1737
|
console.log(JSON.stringify(state, null, 2))
|
|
965
1738
|
} catch (err) { handleError(err) }
|
|
966
1739
|
})
|
|
@@ -1115,4 +1888,39 @@ program
|
|
|
1115
1888
|
console.log()
|
|
1116
1889
|
})
|
|
1117
1890
|
|
|
1891
|
+
// ── shell completion ──────────────────────────────────────────────────────────
|
|
1892
|
+
|
|
1893
|
+
const CLI_COMMANDS = [
|
|
1894
|
+
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'boards',
|
|
1895
|
+
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
1896
|
+
'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
|
|
1897
|
+
'move', 'open', 'reopen', 'requested', 'search', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
1898
|
+
]
|
|
1899
|
+
|
|
1900
|
+
program
|
|
1901
|
+
.command('completion [shell]')
|
|
1902
|
+
.description('Print shell completion script (bash or zsh). Add to your profile: wadah completion bash >> ~/.bashrc')
|
|
1903
|
+
.action((shell) => {
|
|
1904
|
+
const sh = (shell || process.env.SHELL || 'bash').toLowerCase()
|
|
1905
|
+
if (sh.includes('zsh')) {
|
|
1906
|
+
console.log(`# zsh completion for wadah
|
|
1907
|
+
_wadah() {
|
|
1908
|
+
local cur
|
|
1909
|
+
cur=\${words[CURRENT]}
|
|
1910
|
+
reply=(${CLI_COMMANDS.join(' ')})
|
|
1911
|
+
}
|
|
1912
|
+
compctl -K _wadah wadah tm ow
|
|
1913
|
+
`)
|
|
1914
|
+
} else {
|
|
1915
|
+
console.log(`# bash completion for wadah
|
|
1916
|
+
_wadah_completion() {
|
|
1917
|
+
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
1918
|
+
local cmds="${CLI_COMMANDS.join(' ')}"
|
|
1919
|
+
COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
|
|
1920
|
+
}
|
|
1921
|
+
complete -F _wadah_completion wadah tm ow
|
|
1922
|
+
`)
|
|
1923
|
+
}
|
|
1924
|
+
})
|
|
1925
|
+
|
|
1118
1926
|
program.parse()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-wadah",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Open Wadah CLI — shared task board for humans and agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
"tm": "cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"start": "node cli.js"
|
|
12
|
+
"start": "node cli.js",
|
|
13
|
+
"test": "node --test test/cli.test.js"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@inquirer/prompts": "^8.3.0",
|
|
16
17
|
"chalk": "^5.4.1",
|
|
17
18
|
"commander": "^13.1.0",
|
|
18
19
|
"conf": "^13.1.0",
|
|
20
|
+
"form-data": "^4.0.0",
|
|
19
21
|
"node-fetch": "^3.3.2"
|
|
20
22
|
}
|
|
21
23
|
}
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { strict as assert } from 'node:assert'
|
|
4
|
+
import { resolve, dirname } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const cliPath = resolve(__dirname, '../cli.js')
|
|
9
|
+
|
|
10
|
+
function runCli(args) {
|
|
11
|
+
return new Promise((resolvePromise, reject) => {
|
|
12
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
env: { ...process.env, TASK_MANAGER_TOKEN: '' },
|
|
15
|
+
})
|
|
16
|
+
let stdout = ''
|
|
17
|
+
let stderr = ''
|
|
18
|
+
child.stdout?.on('data', (d) => { stdout += d })
|
|
19
|
+
child.stderr?.on('data', (d) => { stderr += d })
|
|
20
|
+
child.on('close', (code) => resolvePromise({ code, stdout, stderr }))
|
|
21
|
+
child.on('error', reject)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('wadah --help exits 0 and prints usage', async () => {
|
|
26
|
+
const { code, stdout } = await runCli(['--help'])
|
|
27
|
+
assert.equal(code, 0)
|
|
28
|
+
assert.match(stdout, /Open Wadah|Usage|Commands/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('wadah --version exits 0 and prints version', async () => {
|
|
32
|
+
const { code, stdout } = await runCli(['--version'])
|
|
33
|
+
assert.equal(code, 0)
|
|
34
|
+
assert.match(stdout, /\d+\.\d+\.\d+/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('wadah (no args) prints help or exits', async () => {
|
|
38
|
+
const { code, stdout, stderr } = await runCli([])
|
|
39
|
+
assert(code === 0 || code === 1)
|
|
40
|
+
const out = stdout + stderr
|
|
41
|
+
assert.match(out, /Usage|Commands|Open Wadah/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('wadah open --help shows open command help', async () => {
|
|
45
|
+
const { code, stdout } = await runCli(['open', '--help'])
|
|
46
|
+
assert.equal(code, 0)
|
|
47
|
+
assert.match(stdout, /open|List/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('wadah doctor runs without crashing', async () => {
|
|
51
|
+
const { code } = await runCli(['doctor'])
|
|
52
|
+
// Doctor may exit 0 or 1 depending on API/token; we only assert it doesn't throw
|
|
53
|
+
assert(code === 0 || code === 1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('wadah completion bash prints script', async () => {
|
|
57
|
+
const { code, stdout } = await runCli(['completion', 'bash'])
|
|
58
|
+
assert.equal(code, 0)
|
|
59
|
+
assert.match(stdout, /complete -F _wadah_completion/)
|
|
60
|
+
assert.match(stdout, /wadah tm ow/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('wadah completion zsh prints script', async () => {
|
|
64
|
+
const { code, stdout } = await runCli(['completion', 'zsh'])
|
|
65
|
+
assert.equal(code, 0)
|
|
66
|
+
assert.match(stdout, /compctl|_wadah/)
|
|
67
|
+
})
|