open-wadah 1.2.2 → 1.2.3
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 +10 -1
- package/cli.js +197 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ wadah complete <task-id>
|
|
|
27
27
|
| Command | Description |
|
|
28
28
|
|--------|-------------|
|
|
29
29
|
| **Auth** | `login`, `signup`, `logout`, `whoami` |
|
|
30
|
-
| **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
|
|
30
|
+
| **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `subtask list/add/toggle/delete`, `delete` |
|
|
31
31
|
| **Relationships** | `add --blocks <id>`, `add --blocked-by <id>`, `update --blocks <id>` |
|
|
32
32
|
| **Board** | `boards --json`, `buckets --json`, `assignees --json`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
33
33
|
| **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
|
|
@@ -39,6 +39,15 @@ wadah complete <task-id>
|
|
|
39
39
|
|
|
40
40
|
Run `wadah --help` for full list and options.
|
|
41
41
|
|
|
42
|
+
Subtask examples:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
wadah subtask add <task-id> "Write tests"
|
|
46
|
+
wadah subtask list <task-id>
|
|
47
|
+
wadah subtask toggle <task-id> <subtask-id>
|
|
48
|
+
wadah subtask delete <task-id> <subtask-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
42
51
|
## Shell completion
|
|
43
52
|
|
|
44
53
|
**Bash:**
|
package/cli.js
CHANGED
|
@@ -279,6 +279,23 @@ function findBucket(buckets, query) {
|
|
|
279
279
|
return buckets.find((b) => b.id === query || b.title.toLowerCase().includes(q)) ?? null
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
function findSubtask(subtasks, query) {
|
|
283
|
+
const raw = String(query ?? '').trim()
|
|
284
|
+
if (!raw) return null
|
|
285
|
+
const q = raw.toLowerCase()
|
|
286
|
+
const byId = subtasks.filter((s) => s.id === raw || s.id.startsWith(raw))
|
|
287
|
+
if (byId.length === 1) return byId[0]
|
|
288
|
+
if (byId.length > 1) {
|
|
289
|
+
throw new CliError('validation_error', `Ambiguous subtask id prefix: ${raw}`)
|
|
290
|
+
}
|
|
291
|
+
const byTitle = subtasks.filter((s) => String(s.title ?? '').toLowerCase() === q)
|
|
292
|
+
if (byTitle.length === 1) return byTitle[0]
|
|
293
|
+
if (byTitle.length > 1) {
|
|
294
|
+
throw new CliError('validation_error', `Multiple subtasks have title: ${raw}`)
|
|
295
|
+
}
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
282
299
|
function parseCompletedFlag(value) {
|
|
283
300
|
if (typeof value !== 'string') return null
|
|
284
301
|
const v = value.trim().toLowerCase()
|
|
@@ -410,6 +427,8 @@ Available commands and their args (use these exact command names):
|
|
|
410
427
|
- repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
|
|
411
428
|
- repo list (or repos): args = []. List linked GitHub repositories.
|
|
412
429
|
- repo remove: args = [repo id]. Unlink a repository.
|
|
430
|
+
- pr link: args = [task id, pr url]. Attach a PR URL to a task.
|
|
431
|
+
- pr open: args = [task id]. Open the linked PR URL for a task.
|
|
413
432
|
|
|
414
433
|
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
415
434
|
|
|
@@ -1280,6 +1299,87 @@ program
|
|
|
1280
1299
|
} catch (err) { handleError(err) }
|
|
1281
1300
|
})
|
|
1282
1301
|
|
|
1302
|
+
const subtaskCmd = program.command('subtask').description('Manage task subtasks')
|
|
1303
|
+
|
|
1304
|
+
subtaskCmd
|
|
1305
|
+
.command('list <taskId>')
|
|
1306
|
+
.description('List subtasks on a task')
|
|
1307
|
+
.action(async (taskId) => {
|
|
1308
|
+
try {
|
|
1309
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1310
|
+
const subtasks = task.subtasks ?? []
|
|
1311
|
+
if (isMachineOutput()) {
|
|
1312
|
+
console.log(JSON.stringify({ taskId, subtasks }, null, runtimeJsonOutput ? 2 : 0))
|
|
1313
|
+
return
|
|
1314
|
+
}
|
|
1315
|
+
console.log()
|
|
1316
|
+
if (subtasks.length === 0) {
|
|
1317
|
+
console.log(chalk.gray(' No subtasks.\n'))
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
console.log(chalk.bold(task.title))
|
|
1321
|
+
subtasks.forEach((s) => {
|
|
1322
|
+
const status = s.completed ? chalk.green('✓') : '○'
|
|
1323
|
+
console.log(` ${status} ${s.title} ${chalk.gray(s.id.slice(0, 8))}`)
|
|
1324
|
+
})
|
|
1325
|
+
console.log()
|
|
1326
|
+
} catch (err) { handleError(err) }
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
subtaskCmd
|
|
1330
|
+
.command('add <taskId> <title>')
|
|
1331
|
+
.description('Add a subtask to a task')
|
|
1332
|
+
.action(async (taskId, title) => {
|
|
1333
|
+
try {
|
|
1334
|
+
const trimmedTitle = String(title ?? '').trim()
|
|
1335
|
+
if (!trimmedTitle) return fail('Subtask title is required')
|
|
1336
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1337
|
+
const nextSubtask = { id: randomUUID(), title: trimmedTitle, completed: false }
|
|
1338
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1339
|
+
method: 'PATCH',
|
|
1340
|
+
body: JSON.stringify({ subtasks: [...(task.subtasks ?? []), nextSubtask] }),
|
|
1341
|
+
})
|
|
1342
|
+
console.log(chalk.green('\n✓ Subtask added') + ` ${chalk.bold(nextSubtask.title)} ${chalk.gray(`(${nextSubtask.id.slice(0, 8)})`)}\n`)
|
|
1343
|
+
} catch (err) { handleError(err) }
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
subtaskCmd
|
|
1347
|
+
.command('toggle <taskId> <subtaskId>')
|
|
1348
|
+
.description('Toggle subtask completion')
|
|
1349
|
+
.action(async (taskId, subtaskId) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1352
|
+
const subtasks = task.subtasks ?? []
|
|
1353
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1354
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1355
|
+
const nextSubtasks = subtasks.map((s) => s.id === target.id ? { ...s, completed: !s.completed } : s)
|
|
1356
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1357
|
+
method: 'PATCH',
|
|
1358
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1359
|
+
})
|
|
1360
|
+
const marker = !target.completed ? chalk.green('✓') : '○'
|
|
1361
|
+
console.log(chalk.green('\n✓ Subtask updated') + ` ${marker} ${chalk.bold(target.title)}\n`)
|
|
1362
|
+
} catch (err) { handleError(err) }
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
subtaskCmd
|
|
1366
|
+
.command('delete <taskId> <subtaskId>')
|
|
1367
|
+
.description('Delete a subtask from a task')
|
|
1368
|
+
.action(async (taskId, subtaskId) => {
|
|
1369
|
+
try {
|
|
1370
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1371
|
+
const subtasks = task.subtasks ?? []
|
|
1372
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1373
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1374
|
+
const nextSubtasks = subtasks.filter((s) => s.id !== target.id)
|
|
1375
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1376
|
+
method: 'PATCH',
|
|
1377
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1378
|
+
})
|
|
1379
|
+
console.log(chalk.green('\n✓ Subtask deleted') + ` ${chalk.bold(target.title)}\n`)
|
|
1380
|
+
} catch (err) { handleError(err) }
|
|
1381
|
+
})
|
|
1382
|
+
|
|
1283
1383
|
// ── tm boards ─────────────────────────────────────────────────────────────────
|
|
1284
1384
|
|
|
1285
1385
|
program
|
|
@@ -1382,6 +1482,29 @@ boardCmd
|
|
|
1382
1482
|
} catch (err) { handleError(err) }
|
|
1383
1483
|
})
|
|
1384
1484
|
|
|
1485
|
+
boardCmd
|
|
1486
|
+
.command('rename <id> <name>')
|
|
1487
|
+
.description('Rename a board')
|
|
1488
|
+
.action(async (id, name) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const payload = JSON.stringify({ name: String(name).trim() })
|
|
1491
|
+
let board
|
|
1492
|
+
try {
|
|
1493
|
+
board = await api(`/api/boards/${id}`, { method: 'PATCH', body: payload })
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
// Backward compatibility: some deployments only support PUT for board rename.
|
|
1496
|
+
const notFound = err instanceof CliError && err.code === 'api_error' && String(err.message).toLowerCase().includes('not found')
|
|
1497
|
+
if (!notFound) throw err
|
|
1498
|
+
board = await api(`/api/boards/${id}`, { method: 'PUT', body: payload })
|
|
1499
|
+
}
|
|
1500
|
+
if (isMachineOutput()) {
|
|
1501
|
+
console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
|
|
1502
|
+
} else {
|
|
1503
|
+
console.log(chalk.green('\n✓ Board renamed') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1504
|
+
}
|
|
1505
|
+
} catch (err) { handleError(err) }
|
|
1506
|
+
})
|
|
1507
|
+
|
|
1385
1508
|
boardCmd
|
|
1386
1509
|
.command('delete <id>')
|
|
1387
1510
|
.description('Delete a board (cannot delete the last one)')
|
|
@@ -1630,6 +1753,79 @@ repoCmd
|
|
|
1630
1753
|
} catch (err) { handleError(err) }
|
|
1631
1754
|
})
|
|
1632
1755
|
|
|
1756
|
+
const prCmd = program.command('pr').description('Attach and open pull requests on tasks')
|
|
1757
|
+
|
|
1758
|
+
prCmd
|
|
1759
|
+
.command('link <taskId> <prUrl>')
|
|
1760
|
+
.description('Attach a PR URL to a task')
|
|
1761
|
+
.option('--comment', 'Add a task comment with the PR link')
|
|
1762
|
+
.action(async function (taskId, prUrl) {
|
|
1763
|
+
const opts = this.opts()
|
|
1764
|
+
try {
|
|
1765
|
+
let parsed
|
|
1766
|
+
try {
|
|
1767
|
+
parsed = new URL(String(prUrl).trim())
|
|
1768
|
+
} catch {
|
|
1769
|
+
return fail('Provide a valid PR URL (e.g. https://github.com/owner/repo/pull/123)')
|
|
1770
|
+
}
|
|
1771
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
1772
|
+
return fail('PR URL must start with http:// or https://')
|
|
1773
|
+
}
|
|
1774
|
+
const normalizedUrl = parsed.toString()
|
|
1775
|
+
|
|
1776
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1777
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1778
|
+
method: 'PATCH',
|
|
1779
|
+
body: JSON.stringify({ contextUrl: normalizedUrl }),
|
|
1780
|
+
})
|
|
1781
|
+
if (opts.comment) {
|
|
1782
|
+
const comment = {
|
|
1783
|
+
id: randomUUID(),
|
|
1784
|
+
text: `Linked PR: ${normalizedUrl}`,
|
|
1785
|
+
createdAt: Date.now(),
|
|
1786
|
+
}
|
|
1787
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1788
|
+
method: 'PATCH',
|
|
1789
|
+
body: JSON.stringify({ comments: [...(task.comments ?? []), comment] }),
|
|
1790
|
+
})
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (isMachineOutput()) {
|
|
1794
|
+
console.log(JSON.stringify({ taskId, prUrl: normalizedUrl, linked: true, commented: Boolean(opts.comment) }, null, runtimeJsonOutput ? 2 : 0))
|
|
1795
|
+
} else {
|
|
1796
|
+
console.log(chalk.green('\n✓ PR linked') + ` ${chalk.bold(task.title)}`)
|
|
1797
|
+
console.log(chalk.gray(` ${normalizedUrl}${opts.comment ? ' · comment added' : ''}\n`))
|
|
1798
|
+
}
|
|
1799
|
+
} catch (err) { handleError(err) }
|
|
1800
|
+
})
|
|
1801
|
+
|
|
1802
|
+
prCmd
|
|
1803
|
+
.command('open <taskId>')
|
|
1804
|
+
.description('Open the linked PR URL for a task')
|
|
1805
|
+
.option('--no-browser', "Don't auto-open browser; print URL only")
|
|
1806
|
+
.action(async function (taskId) {
|
|
1807
|
+
const opts = this.opts()
|
|
1808
|
+
try {
|
|
1809
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1810
|
+
const url = task?.contextUrl ? String(task.contextUrl).trim() : ''
|
|
1811
|
+
if (!url) return fail('This task has no linked PR URL. Use: wadah pr link <task-id> <pr-url>')
|
|
1812
|
+
|
|
1813
|
+
if (isMachineOutput()) {
|
|
1814
|
+
console.log(JSON.stringify({ taskId, prUrl: url }, null, runtimeJsonOutput ? 2 : 0))
|
|
1815
|
+
return
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
console.log(chalk.gray(`\nPR: ${url}`))
|
|
1819
|
+
if (opts.browser) {
|
|
1820
|
+
const opened = await openBrowser(url)
|
|
1821
|
+
if (opened) console.log(chalk.green('✓ Opened in browser\n'))
|
|
1822
|
+
else console.log(chalk.yellow('Could not open browser automatically. Open the URL manually.\n'))
|
|
1823
|
+
} else {
|
|
1824
|
+
console.log()
|
|
1825
|
+
}
|
|
1826
|
+
} catch (err) { handleError(err) }
|
|
1827
|
+
})
|
|
1828
|
+
|
|
1633
1829
|
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1634
1830
|
|
|
1635
1831
|
program
|
|
@@ -2255,7 +2451,7 @@ const CLI_COMMANDS = [
|
|
|
2255
2451
|
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
|
|
2256
2452
|
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
2257
2453
|
'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
|
|
2258
|
-
'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
2454
|
+
'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'pr', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
2259
2455
|
]
|
|
2260
2456
|
|
|
2261
2457
|
// ── wadah setup-agent ─────────────────────────────────────────────────────────
|