open-wadah 1.2.2 → 1.3.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 +111 -50
- package/cli.js +376 -29
- package/package.json +1 -1
- package/test/cli.test.js +7 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# open-wadah
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line interface for Wadah task boards and autonomous agent workflows.
|
|
4
|
+
|
|
5
|
+
`open-wadah` gives teams and AI agents a fast, scriptable way to plan, assign, and deliver work from the terminal.
|
|
6
|
+
|
|
7
|
+
## Why teams use Wadah CLI
|
|
8
|
+
|
|
9
|
+
- Manage tasks, assignees, and board flow without leaving your terminal.
|
|
10
|
+
- Automate workflows with stable JSON output for scripts and CI.
|
|
11
|
+
- Power autonomous agents with scoped agent tokens.
|
|
12
|
+
- Keep human and agent work on the same board and process.
|
|
4
13
|
|
|
5
14
|
## Install
|
|
6
15
|
|
|
@@ -8,83 +17,135 @@ Open WADAH CLI — shared task board for humans and agents. Use it from the term
|
|
|
8
17
|
npm install -g open-wadah
|
|
9
18
|
```
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
Available commands: `wadah`, `ow`, `tm`.
|
|
12
21
|
|
|
13
|
-
##
|
|
22
|
+
## 60-second quick start
|
|
14
23
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
1. Sign in:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
wadah login
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Open your assigned tasks:
|
|
18
31
|
|
|
19
32
|
```bash
|
|
20
33
|
wadah open
|
|
21
|
-
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Create and move work:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
wadah add "Fix login issue" --board "Main"
|
|
40
|
+
wadah move <task-id> <bucket-id>
|
|
22
41
|
wadah complete <task-id>
|
|
23
42
|
```
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
Default API URL is `https://api.openwadah.com`. Override with `--api <url>` or `WADAH_API_URL`.
|
|
26
45
|
|
|
27
|
-
|
|
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]` |
|
|
46
|
+
## Core workflows
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
### Daily task operations
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
```bash
|
|
51
|
+
wadah list --board "Main"
|
|
52
|
+
wadah search "billing"
|
|
53
|
+
wadah view <task-id>
|
|
54
|
+
wadah update <task-id> --notes "Root cause + next steps"
|
|
55
|
+
wadah comment <task-id> "Investigating now"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Dependencies and subtasks
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
wadah add "Ship release notes" --blocks <task-id>
|
|
62
|
+
wadah subtask add <task-id> "Write tests"
|
|
63
|
+
wadah subtask list <task-id>
|
|
64
|
+
wadah subtask toggle <task-id> <subtask-id>
|
|
65
|
+
```
|
|
43
66
|
|
|
44
|
-
|
|
67
|
+
### Board administration
|
|
45
68
|
|
|
46
69
|
```bash
|
|
47
|
-
wadah
|
|
48
|
-
|
|
70
|
+
wadah board create "Backend"
|
|
71
|
+
wadah bucket create "In Review" --board <board-id>
|
|
72
|
+
wadah assignee create "Cursor Agent" --type agent
|
|
73
|
+
wadah buckets --board "Backend" --json
|
|
49
74
|
```
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
## Command surface
|
|
77
|
+
|
|
78
|
+
| Area | Commands |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Auth | `login`, `signup`, `logout`, `whoami` |
|
|
81
|
+
| Tasks | `open`, `list`, `search`, `requested`, `add`, `view`, `update`, `move`, `assign`, `comment`, `complete`, `reopen`, `delete` |
|
|
82
|
+
| Subtasks | `subtask list`, `subtask add`, `subtask toggle`, `subtask delete` |
|
|
83
|
+
| Boards & members | `boards`, `buckets`, `assignees`, `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
84
|
+
| Docs | `docs`, `doc create/show/update/delete` |
|
|
85
|
+
| Calendar | `calendar`, `calendar add/update/delete` |
|
|
86
|
+
| Files | `folders`, `files`, `folder create`, `mkdir`, `upload` |
|
|
87
|
+
| Agent tokens | `agent-tokens`, `agent-token create/delete` |
|
|
88
|
+
| Utilities | `doctor`, `completion bash`, `completion zsh`, `state`, `members`, `invite`, `config`, `do` |
|
|
89
|
+
|
|
90
|
+
Use `wadah --help` or `wadah <command> --help` for full flags and examples.
|
|
91
|
+
|
|
92
|
+
## AI agent mode
|
|
93
|
+
|
|
94
|
+
For autonomous workflows (Cursor, Claude Code, GitHub Actions):
|
|
95
|
+
|
|
96
|
+
- Create an agent token:
|
|
52
97
|
|
|
53
98
|
```bash
|
|
54
|
-
wadah
|
|
55
|
-
source ~/.zshrc
|
|
99
|
+
wadah agent-token create "My Agent"
|
|
56
100
|
```
|
|
57
101
|
|
|
58
|
-
|
|
102
|
+
- Set it as `WADAH_AGENT_TOKEN` in your environment.
|
|
103
|
+
- Use `--json` for deterministic machine output (`wadah open --json`).
|
|
104
|
+
- Optional natural-language command:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
wadah do "add a task to fix onboarding bug"
|
|
108
|
+
```
|
|
59
109
|
|
|
60
|
-
|
|
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.
|
|
110
|
+
`wadah do` requires `OPENAI_API_KEY`.
|
|
65
111
|
|
|
66
112
|
## Global flags
|
|
67
113
|
|
|
68
|
-
- `--api <url
|
|
69
|
-
- `--profile <name
|
|
70
|
-
- `--token <token
|
|
71
|
-
- `--json
|
|
72
|
-
- `--quiet
|
|
114
|
+
- `--api <url>`: API base URL
|
|
115
|
+
- `--profile <name>`: config profile
|
|
116
|
+
- `--token <token>`: token for current command only
|
|
117
|
+
- `--json`: machine-readable output
|
|
118
|
+
- `--quiet`: reduce non-essential output
|
|
73
119
|
|
|
74
|
-
##
|
|
120
|
+
## Environment variables
|
|
75
121
|
|
|
76
|
-
|
|
122
|
+
- `WADAH_AGENT_TOKEN`: preferred auth token for automation
|
|
123
|
+
- `WADAH_API_URL`: override API base URL
|
|
124
|
+
- `OPENAI_API_KEY`: required for `wadah do`
|
|
125
|
+
|
|
126
|
+
## Shell completion
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Bash
|
|
130
|
+
wadah completion bash >> ~/.bashrc && source ~/.bashrc
|
|
131
|
+
|
|
132
|
+
# Zsh
|
|
133
|
+
wadah completion zsh >> ~/.zshrc && source ~/.zshrc
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development
|
|
77
137
|
|
|
78
138
|
```bash
|
|
79
|
-
npm
|
|
80
|
-
|
|
81
|
-
cd task-manager-cli && npm test
|
|
139
|
+
npm test
|
|
140
|
+
node cli.js --help
|
|
82
141
|
```
|
|
83
142
|
|
|
84
|
-
##
|
|
143
|
+
## JSON error codes
|
|
144
|
+
|
|
145
|
+
When running with `--json`, failures include structured error codes:
|
|
85
146
|
|
|
86
|
-
- `auth_error`
|
|
87
|
-
- `validation_error`
|
|
88
|
-
- `network_error`
|
|
89
|
-
- `api_error`
|
|
90
|
-
- `config`
|
|
147
|
+
- `auth_error`
|
|
148
|
+
- `validation_error`
|
|
149
|
+
- `network_error`
|
|
150
|
+
- `api_error`
|
|
151
|
+
- `config`
|
package/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ function normalizeApiUrl(url) {
|
|
|
31
31
|
const conf = new Conf({ projectName: 'open-wadah' })
|
|
32
32
|
|
|
33
33
|
function getProfile() {
|
|
34
|
-
const profile = (process.env.
|
|
34
|
+
const profile = (process.env.WADAH_PROFILE ?? runtimeProfile ?? 'default').trim()
|
|
35
35
|
return profile || 'default'
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -55,15 +55,28 @@ function confDelete(key) {
|
|
|
55
55
|
|
|
56
56
|
function getApiBase() {
|
|
57
57
|
return (
|
|
58
|
-
(process.env.
|
|
58
|
+
(process.env.WADAH_API_URL ?? '').trim() ||
|
|
59
59
|
runtimeApiBase ||
|
|
60
60
|
confGet('api_url') ||
|
|
61
61
|
DEFAULT_API_BASE
|
|
62
62
|
)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Agent token from env: reads WADAH_AGENT_TOKEN. */
|
|
66
|
+
function envAgentToken() {
|
|
67
|
+
const w = (process.env.WADAH_AGENT_TOKEN ?? '').trim()
|
|
68
|
+
return w || null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Which env var supplied the agent token (for status output). */
|
|
72
|
+
function envAgentTokenVarName() {
|
|
73
|
+
if ((process.env.WADAH_AGENT_TOKEN ?? '').trim()) return 'WADAH_AGENT_TOKEN'
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
function getAuthToken() {
|
|
66
|
-
|
|
78
|
+
const fromEnv = envAgentToken()
|
|
79
|
+
if (fromEnv) return fromEnv
|
|
67
80
|
if (runtimeToken) return runtimeToken
|
|
68
81
|
return confGet('access_token') ?? null
|
|
69
82
|
}
|
|
@@ -99,7 +112,7 @@ async function refreshAccessToken() {
|
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
async function ensureAuth() {
|
|
102
|
-
if (
|
|
115
|
+
if (envAgentToken()) return // agent token, no refresh needed
|
|
103
116
|
const expires = confGet('token_expires')
|
|
104
117
|
// Refresh if within 5 minutes of expiry or already expired
|
|
105
118
|
if (expires && Date.now() > (expires * 1000) - 300_000) {
|
|
@@ -120,7 +133,7 @@ async function api(path, options = {}, retry = true) {
|
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
// If 401 and we have a refresh token, try once more
|
|
123
|
-
if (res.status === 401 && retry && !
|
|
136
|
+
if (res.status === 401 && retry && !envAgentToken()) {
|
|
124
137
|
const refreshed = await refreshAccessToken()
|
|
125
138
|
if (refreshed) return api(path, options, false)
|
|
126
139
|
}
|
|
@@ -197,7 +210,7 @@ function handleError(err) {
|
|
|
197
210
|
} else if (authLike) {
|
|
198
211
|
console.error(chalk.red('\n✗ Not authenticated.'))
|
|
199
212
|
console.error(chalk.gray(' Humans: wadah login'))
|
|
200
|
-
console.error(chalk.gray(' Agents:
|
|
213
|
+
console.error(chalk.gray(' Agents: WADAH_AGENT_TOKEN=<token> wadah open\n'))
|
|
201
214
|
} else if (code === 'network_error') {
|
|
202
215
|
console.error(chalk.red('\n✗ Network error — could not reach the API.'))
|
|
203
216
|
console.error(chalk.gray(` Check your connection or API URL: ${getApiBase()}`))
|
|
@@ -279,6 +292,23 @@ function findBucket(buckets, query) {
|
|
|
279
292
|
return buckets.find((b) => b.id === query || b.title.toLowerCase().includes(q)) ?? null
|
|
280
293
|
}
|
|
281
294
|
|
|
295
|
+
function findSubtask(subtasks, query) {
|
|
296
|
+
const raw = String(query ?? '').trim()
|
|
297
|
+
if (!raw) return null
|
|
298
|
+
const q = raw.toLowerCase()
|
|
299
|
+
const byId = subtasks.filter((s) => s.id === raw || s.id.startsWith(raw))
|
|
300
|
+
if (byId.length === 1) return byId[0]
|
|
301
|
+
if (byId.length > 1) {
|
|
302
|
+
throw new CliError('validation_error', `Ambiguous subtask id prefix: ${raw}`)
|
|
303
|
+
}
|
|
304
|
+
const byTitle = subtasks.filter((s) => String(s.title ?? '').toLowerCase() === q)
|
|
305
|
+
if (byTitle.length === 1) return byTitle[0]
|
|
306
|
+
if (byTitle.length > 1) {
|
|
307
|
+
throw new CliError('validation_error', `Multiple subtasks have title: ${raw}`)
|
|
308
|
+
}
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
|
|
282
312
|
function parseCompletedFlag(value) {
|
|
283
313
|
if (typeof value !== 'string') return null
|
|
284
314
|
const v = value.trim().toLowerCase()
|
|
@@ -410,6 +440,8 @@ Available commands and their args (use these exact command names):
|
|
|
410
440
|
- repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
|
|
411
441
|
- repo list (or repos): args = []. List linked GitHub repositories.
|
|
412
442
|
- repo remove: args = [repo id]. Unlink a repository.
|
|
443
|
+
- pr link: args = [task id, pr url]. Attach a PR URL to a task.
|
|
444
|
+
- pr open: args = [task id]. Open the linked PR URL for a task.
|
|
413
445
|
|
|
414
446
|
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
415
447
|
|
|
@@ -719,7 +751,7 @@ program
|
|
|
719
751
|
.action(async () => {
|
|
720
752
|
if (isMachineOutput()) {
|
|
721
753
|
try {
|
|
722
|
-
if (
|
|
754
|
+
if (envAgentToken() || runtimeToken) {
|
|
723
755
|
const me = await api('/api/auth/me')
|
|
724
756
|
const ws = me.workspaces?.[0] ?? null
|
|
725
757
|
console.log(JSON.stringify({
|
|
@@ -752,8 +784,8 @@ program
|
|
|
752
784
|
return
|
|
753
785
|
}
|
|
754
786
|
|
|
755
|
-
if (
|
|
756
|
-
console.log(chalk.bold('\nAgent') +
|
|
787
|
+
if (envAgentToken()) {
|
|
788
|
+
console.log(chalk.bold('\nAgent') + ` (${envAgentTokenVarName()})`)
|
|
757
789
|
} else {
|
|
758
790
|
const email = confGet('user_email')
|
|
759
791
|
if (!email) {
|
|
@@ -1233,7 +1265,16 @@ program
|
|
|
1233
1265
|
if (requester) console.log(chalk.gray(` Requested: ${requester}`))
|
|
1234
1266
|
if (task.dueDate) console.log(chalk.gray(` Due: ${formatDate(task.dueDate)}`))
|
|
1235
1267
|
if (task.repo) console.log(chalk.gray(` Repo: ${task.repo}`))
|
|
1236
|
-
if (task.
|
|
1268
|
+
if (Array.isArray(task.contextLinks) && task.contextLinks.length > 0) {
|
|
1269
|
+
console.log(chalk.gray('\n Links:'))
|
|
1270
|
+
task.contextLinks.forEach((l) => {
|
|
1271
|
+
const lab = l.label ? `${l.label}: ` : ''
|
|
1272
|
+
console.log(chalk.gray(` ${lab}${l.url}`))
|
|
1273
|
+
})
|
|
1274
|
+
if (task.contextUrl) console.log(chalk.gray(` Primary: ${task.contextUrl}`))
|
|
1275
|
+
} else if (task.contextUrl) {
|
|
1276
|
+
console.log(chalk.gray(` Link: ${task.contextUrl}`))
|
|
1277
|
+
}
|
|
1237
1278
|
if (task.tags?.length) {
|
|
1238
1279
|
const relTags = task.tags.filter((t) => t.startsWith('blocks:') || t.startsWith('blocked-by:'))
|
|
1239
1280
|
const plainTags = task.tags.filter((t) => !t.startsWith('blocks:') && !t.startsWith('blocked-by:'))
|
|
@@ -1280,6 +1321,87 @@ program
|
|
|
1280
1321
|
} catch (err) { handleError(err) }
|
|
1281
1322
|
})
|
|
1282
1323
|
|
|
1324
|
+
const subtaskCmd = program.command('subtask').description('Manage task subtasks')
|
|
1325
|
+
|
|
1326
|
+
subtaskCmd
|
|
1327
|
+
.command('list <taskId>')
|
|
1328
|
+
.description('List subtasks on a task')
|
|
1329
|
+
.action(async (taskId) => {
|
|
1330
|
+
try {
|
|
1331
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1332
|
+
const subtasks = task.subtasks ?? []
|
|
1333
|
+
if (isMachineOutput()) {
|
|
1334
|
+
console.log(JSON.stringify({ taskId, subtasks }, null, runtimeJsonOutput ? 2 : 0))
|
|
1335
|
+
return
|
|
1336
|
+
}
|
|
1337
|
+
console.log()
|
|
1338
|
+
if (subtasks.length === 0) {
|
|
1339
|
+
console.log(chalk.gray(' No subtasks.\n'))
|
|
1340
|
+
return
|
|
1341
|
+
}
|
|
1342
|
+
console.log(chalk.bold(task.title))
|
|
1343
|
+
subtasks.forEach((s) => {
|
|
1344
|
+
const status = s.completed ? chalk.green('✓') : '○'
|
|
1345
|
+
console.log(` ${status} ${s.title} ${chalk.gray(s.id.slice(0, 8))}`)
|
|
1346
|
+
})
|
|
1347
|
+
console.log()
|
|
1348
|
+
} catch (err) { handleError(err) }
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
subtaskCmd
|
|
1352
|
+
.command('add <taskId> <title>')
|
|
1353
|
+
.description('Add a subtask to a task')
|
|
1354
|
+
.action(async (taskId, title) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const trimmedTitle = String(title ?? '').trim()
|
|
1357
|
+
if (!trimmedTitle) return fail('Subtask title is required')
|
|
1358
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1359
|
+
const nextSubtask = { id: randomUUID(), title: trimmedTitle, completed: false }
|
|
1360
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1361
|
+
method: 'PATCH',
|
|
1362
|
+
body: JSON.stringify({ subtasks: [...(task.subtasks ?? []), nextSubtask] }),
|
|
1363
|
+
})
|
|
1364
|
+
console.log(chalk.green('\n✓ Subtask added') + ` ${chalk.bold(nextSubtask.title)} ${chalk.gray(`(${nextSubtask.id.slice(0, 8)})`)}\n`)
|
|
1365
|
+
} catch (err) { handleError(err) }
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1368
|
+
subtaskCmd
|
|
1369
|
+
.command('toggle <taskId> <subtaskId>')
|
|
1370
|
+
.description('Toggle subtask completion')
|
|
1371
|
+
.action(async (taskId, subtaskId) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1374
|
+
const subtasks = task.subtasks ?? []
|
|
1375
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1376
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1377
|
+
const nextSubtasks = subtasks.map((s) => s.id === target.id ? { ...s, completed: !s.completed } : s)
|
|
1378
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1379
|
+
method: 'PATCH',
|
|
1380
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1381
|
+
})
|
|
1382
|
+
const marker = !target.completed ? chalk.green('✓') : '○'
|
|
1383
|
+
console.log(chalk.green('\n✓ Subtask updated') + ` ${marker} ${chalk.bold(target.title)}\n`)
|
|
1384
|
+
} catch (err) { handleError(err) }
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
subtaskCmd
|
|
1388
|
+
.command('delete <taskId> <subtaskId>')
|
|
1389
|
+
.description('Delete a subtask from a task')
|
|
1390
|
+
.action(async (taskId, subtaskId) => {
|
|
1391
|
+
try {
|
|
1392
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1393
|
+
const subtasks = task.subtasks ?? []
|
|
1394
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1395
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1396
|
+
const nextSubtasks = subtasks.filter((s) => s.id !== target.id)
|
|
1397
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1398
|
+
method: 'PATCH',
|
|
1399
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1400
|
+
})
|
|
1401
|
+
console.log(chalk.green('\n✓ Subtask deleted') + ` ${chalk.bold(target.title)}\n`)
|
|
1402
|
+
} catch (err) { handleError(err) }
|
|
1403
|
+
})
|
|
1404
|
+
|
|
1283
1405
|
// ── tm boards ─────────────────────────────────────────────────────────────────
|
|
1284
1406
|
|
|
1285
1407
|
program
|
|
@@ -1371,13 +1493,65 @@ const boardCmd = program.command('board').description('Create or delete a board
|
|
|
1371
1493
|
boardCmd
|
|
1372
1494
|
.command('create <name>')
|
|
1373
1495
|
.description('Create a new board')
|
|
1374
|
-
.
|
|
1496
|
+
.option('--flow <columns>', 'Comma-separated column flow, e.g. "Backlog,Ready,Doing,QA,Done"')
|
|
1497
|
+
.action(async function (name) {
|
|
1375
1498
|
try {
|
|
1499
|
+
const opts = this.opts()
|
|
1376
1500
|
const board = await api('/api/boards', { method: 'POST', body: JSON.stringify({ name: String(name).trim() }) })
|
|
1501
|
+
const createdBuckets = []
|
|
1502
|
+
if (opts.flow) {
|
|
1503
|
+
const flowColumns = String(opts.flow)
|
|
1504
|
+
.split(',')
|
|
1505
|
+
.map((c) => c.trim())
|
|
1506
|
+
.filter(Boolean)
|
|
1507
|
+
if (flowColumns.length === 0) {
|
|
1508
|
+
return fail('Flow is empty. Provide comma-separated columns, e.g. --flow "Backlog,Doing,Done".')
|
|
1509
|
+
}
|
|
1510
|
+
for (const column of flowColumns) {
|
|
1511
|
+
if (!column) continue
|
|
1512
|
+
const bucket = await api('/api/buckets', {
|
|
1513
|
+
method: 'POST',
|
|
1514
|
+
body: JSON.stringify({ title: column, boardId: board.id }),
|
|
1515
|
+
})
|
|
1516
|
+
createdBuckets.push(bucket)
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1377
1519
|
if (isMachineOutput()) {
|
|
1378
|
-
console.log(JSON.stringify(
|
|
1520
|
+
console.log(JSON.stringify(
|
|
1521
|
+
{ ...board, flow: createdBuckets.map((b) => ({ id: b.id, title: b.title })) },
|
|
1522
|
+
null,
|
|
1523
|
+
runtimeJsonOutput ? 2 : 0
|
|
1524
|
+
))
|
|
1379
1525
|
} else {
|
|
1380
1526
|
console.log(chalk.green('\n✓ Board created') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1527
|
+
if (createdBuckets.length > 0) {
|
|
1528
|
+
console.log(chalk.gray(' Flow columns:'))
|
|
1529
|
+
createdBuckets.forEach((b) => console.log(` - ${b.title} ${chalk.gray(b.id?.slice(0, 8))}`))
|
|
1530
|
+
console.log()
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
} catch (err) { handleError(err) }
|
|
1534
|
+
})
|
|
1535
|
+
|
|
1536
|
+
boardCmd
|
|
1537
|
+
.command('rename <id> <name>')
|
|
1538
|
+
.description('Rename a board')
|
|
1539
|
+
.action(async (id, name) => {
|
|
1540
|
+
try {
|
|
1541
|
+
const payload = JSON.stringify({ name: String(name).trim() })
|
|
1542
|
+
let board
|
|
1543
|
+
try {
|
|
1544
|
+
board = await api(`/api/boards/${id}`, { method: 'PATCH', body: payload })
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
// Backward compatibility: some deployments only support PUT for board rename.
|
|
1547
|
+
const notFound = err instanceof CliError && err.code === 'api_error' && String(err.message).toLowerCase().includes('not found')
|
|
1548
|
+
if (!notFound) throw err
|
|
1549
|
+
board = await api(`/api/boards/${id}`, { method: 'PUT', body: payload })
|
|
1550
|
+
}
|
|
1551
|
+
if (isMachineOutput()) {
|
|
1552
|
+
console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(chalk.green('\n✓ Board renamed') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1381
1555
|
}
|
|
1382
1556
|
} catch (err) { handleError(err) }
|
|
1383
1557
|
})
|
|
@@ -1630,6 +1804,179 @@ repoCmd
|
|
|
1630
1804
|
} catch (err) { handleError(err) }
|
|
1631
1805
|
})
|
|
1632
1806
|
|
|
1807
|
+
const prCmd = program.command('pr').description('Attach and open pull requests on tasks')
|
|
1808
|
+
|
|
1809
|
+
prCmd
|
|
1810
|
+
.command('link <taskId> <prUrl>')
|
|
1811
|
+
.description('Attach a PR URL to a task')
|
|
1812
|
+
.option('--comment', 'Add a task comment with the PR link')
|
|
1813
|
+
.action(async function (taskId, prUrl) {
|
|
1814
|
+
const opts = this.opts()
|
|
1815
|
+
try {
|
|
1816
|
+
let parsed
|
|
1817
|
+
try {
|
|
1818
|
+
parsed = new URL(String(prUrl).trim())
|
|
1819
|
+
} catch {
|
|
1820
|
+
return fail('Provide a valid PR URL (e.g. https://github.com/owner/repo/pull/123)')
|
|
1821
|
+
}
|
|
1822
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
1823
|
+
return fail('PR URL must start with http:// or https://')
|
|
1824
|
+
}
|
|
1825
|
+
const normalizedUrl = parsed.toString()
|
|
1826
|
+
|
|
1827
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1828
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1829
|
+
method: 'PATCH',
|
|
1830
|
+
body: JSON.stringify({ contextUrl: normalizedUrl }),
|
|
1831
|
+
})
|
|
1832
|
+
if (opts.comment) {
|
|
1833
|
+
const comment = {
|
|
1834
|
+
id: randomUUID(),
|
|
1835
|
+
text: `Linked PR: ${normalizedUrl}`,
|
|
1836
|
+
createdAt: Date.now(),
|
|
1837
|
+
}
|
|
1838
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1839
|
+
method: 'PATCH',
|
|
1840
|
+
body: JSON.stringify({ comments: [...(task.comments ?? []), comment] }),
|
|
1841
|
+
})
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
if (isMachineOutput()) {
|
|
1845
|
+
console.log(JSON.stringify({ taskId, prUrl: normalizedUrl, linked: true, commented: Boolean(opts.comment) }, null, runtimeJsonOutput ? 2 : 0))
|
|
1846
|
+
} else {
|
|
1847
|
+
console.log(chalk.green('\n✓ PR linked') + ` ${chalk.bold(task.title)}`)
|
|
1848
|
+
console.log(chalk.gray(` ${normalizedUrl}${opts.comment ? ' · comment added' : ''}\n`))
|
|
1849
|
+
}
|
|
1850
|
+
} catch (err) { handleError(err) }
|
|
1851
|
+
})
|
|
1852
|
+
|
|
1853
|
+
prCmd
|
|
1854
|
+
.command('open <taskId>')
|
|
1855
|
+
.description('Open the linked PR URL for a task')
|
|
1856
|
+
.option('--no-browser', "Don't auto-open browser; print URL only")
|
|
1857
|
+
.action(async function (taskId) {
|
|
1858
|
+
const opts = this.opts()
|
|
1859
|
+
try {
|
|
1860
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1861
|
+
const url = task?.contextUrl ? String(task.contextUrl).trim() : ''
|
|
1862
|
+
if (!url) return fail('This task has no linked PR URL. Use: wadah pr link <task-id> <pr-url>')
|
|
1863
|
+
|
|
1864
|
+
if (isMachineOutput()) {
|
|
1865
|
+
console.log(JSON.stringify({ taskId, prUrl: url }, null, runtimeJsonOutput ? 2 : 0))
|
|
1866
|
+
return
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
console.log(chalk.gray(`\nPR: ${url}`))
|
|
1870
|
+
if (opts.browser) {
|
|
1871
|
+
const opened = await openBrowser(url)
|
|
1872
|
+
if (opened) console.log(chalk.green('✓ Opened in browser\n'))
|
|
1873
|
+
else console.log(chalk.yellow('Could not open browser automatically. Open the URL manually.\n'))
|
|
1874
|
+
} else {
|
|
1875
|
+
console.log()
|
|
1876
|
+
}
|
|
1877
|
+
} catch (err) { handleError(err) }
|
|
1878
|
+
})
|
|
1879
|
+
|
|
1880
|
+
const linkCmd = program.command('link').description('Attach labeled URLs to tasks (Vercel, Figma, docs — without replacing the PR)')
|
|
1881
|
+
|
|
1882
|
+
function normalizeHttpUrlForLink(input) {
|
|
1883
|
+
let parsed
|
|
1884
|
+
try {
|
|
1885
|
+
parsed = new URL(String(input).trim())
|
|
1886
|
+
} catch {
|
|
1887
|
+
return null
|
|
1888
|
+
}
|
|
1889
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return null
|
|
1890
|
+
return parsed.toString()
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const MAX_TASK_LINKS = 20
|
|
1894
|
+
|
|
1895
|
+
linkCmd
|
|
1896
|
+
.command('add <taskId> <url>')
|
|
1897
|
+
.description('Append a URL to a task (use --label for display)')
|
|
1898
|
+
.option('--label <text>', 'Short label, e.g. Vercel, Figma', '')
|
|
1899
|
+
.action(async function (taskId, url) {
|
|
1900
|
+
const opts = this.opts()
|
|
1901
|
+
try {
|
|
1902
|
+
const normalizedUrl = normalizeHttpUrlForLink(url)
|
|
1903
|
+
if (!normalizedUrl) return fail('Provide a valid http(s) URL')
|
|
1904
|
+
|
|
1905
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1906
|
+
const existing = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1907
|
+
if (existing.length >= MAX_TASK_LINKS) {
|
|
1908
|
+
return fail(`A task can have at most ${MAX_TASK_LINKS} links. Remove one with: wadah link remove <task-id> <link-id>`)
|
|
1909
|
+
}
|
|
1910
|
+
const label = String(opts.label ?? '').trim().slice(0, 64)
|
|
1911
|
+
const next = [...existing, { id: randomUUID(), label, url: normalizedUrl }]
|
|
1912
|
+
|
|
1913
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1914
|
+
method: 'PATCH',
|
|
1915
|
+
body: JSON.stringify({ contextLinks: next }),
|
|
1916
|
+
})
|
|
1917
|
+
|
|
1918
|
+
if (isMachineOutput()) {
|
|
1919
|
+
console.log(JSON.stringify({ taskId, url: normalizedUrl, label, added: true, links: next }, null, runtimeJsonOutput ? 2 : 0))
|
|
1920
|
+
} else {
|
|
1921
|
+
console.log(chalk.green('\n✓ Link added') + ` ${chalk.bold(task.title)}`)
|
|
1922
|
+
console.log(chalk.gray(` ${label ? `${label} · ` : ''}${normalizedUrl}\n`))
|
|
1923
|
+
}
|
|
1924
|
+
} catch (err) { handleError(err) }
|
|
1925
|
+
})
|
|
1926
|
+
|
|
1927
|
+
linkCmd
|
|
1928
|
+
.command('list <taskId>')
|
|
1929
|
+
.description('List URLs attached to a task')
|
|
1930
|
+
.action(async (taskId) => {
|
|
1931
|
+
try {
|
|
1932
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1933
|
+
const links = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1934
|
+
if (isMachineOutput()) {
|
|
1935
|
+
console.log(JSON.stringify({ taskId, links, contextUrl: task.contextUrl ?? null }, null, runtimeJsonOutput ? 2 : 0))
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
if (links.length === 0) {
|
|
1939
|
+
console.log(chalk.gray('\nNo links on this task. Add one: wadah link add <task-id> <url> --label Vercel\n'))
|
|
1940
|
+
return
|
|
1941
|
+
}
|
|
1942
|
+
console.log()
|
|
1943
|
+
links.forEach((l) => {
|
|
1944
|
+
const lab = l.label ? chalk.cyan(l.label) : chalk.gray('link')
|
|
1945
|
+
console.log(` ${chalk.dim(l.id)} ${lab}`)
|
|
1946
|
+
console.log(chalk.gray(` ${l.url}`))
|
|
1947
|
+
})
|
|
1948
|
+
if (task.contextUrl) {
|
|
1949
|
+
console.log(chalk.gray(`\n Primary (e.g. wadah pr open): ${task.contextUrl}\n`))
|
|
1950
|
+
} else {
|
|
1951
|
+
console.log()
|
|
1952
|
+
}
|
|
1953
|
+
} catch (err) { handleError(err) }
|
|
1954
|
+
})
|
|
1955
|
+
|
|
1956
|
+
linkCmd
|
|
1957
|
+
.command('remove <taskId> <linkId>')
|
|
1958
|
+
.description('Remove a link by id (from wadah link list --json)')
|
|
1959
|
+
.action(async (taskId, linkId) => {
|
|
1960
|
+
try {
|
|
1961
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1962
|
+
const existing = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1963
|
+
const needle = String(linkId).trim()
|
|
1964
|
+
const next = existing.filter((l) => l.id !== needle)
|
|
1965
|
+
if (next.length === existing.length) {
|
|
1966
|
+
return fail(`No link with id matching "${needle}". Use: wadah link list ${taskId} --json`)
|
|
1967
|
+
}
|
|
1968
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1969
|
+
method: 'PATCH',
|
|
1970
|
+
body: JSON.stringify({ contextLinks: next }),
|
|
1971
|
+
})
|
|
1972
|
+
if (isMachineOutput()) {
|
|
1973
|
+
console.log(JSON.stringify({ taskId, removed: needle, links: next }, null, runtimeJsonOutput ? 2 : 0))
|
|
1974
|
+
} else {
|
|
1975
|
+
console.log(chalk.green('\n✓ Link removed') + ` ${chalk.bold(task.title)}\n`)
|
|
1976
|
+
}
|
|
1977
|
+
} catch (err) { handleError(err) }
|
|
1978
|
+
})
|
|
1979
|
+
|
|
1633
1980
|
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1634
1981
|
|
|
1635
1982
|
program
|
|
@@ -1662,7 +2009,7 @@ const agentTokenCmd = program.command('agent-token').description('Create or dele
|
|
|
1662
2009
|
|
|
1663
2010
|
agentTokenCmd
|
|
1664
2011
|
.command('create [name]')
|
|
1665
|
-
.description('Create an agent token; token is shown once — store it as
|
|
2012
|
+
.description('Create an agent token; token is shown once — store it as WADAH_AGENT_TOKEN')
|
|
1666
2013
|
.option('--name <text>', 'Token name (if not passed as argument)')
|
|
1667
2014
|
.option('--assignee-name <text>', 'Display name for the agent (default: same as name)')
|
|
1668
2015
|
.action(async function (nameArg) {
|
|
@@ -1680,9 +2027,9 @@ agentTokenCmd
|
|
|
1680
2027
|
console.log(chalk.yellow('\n Token (store it now — shown once):'))
|
|
1681
2028
|
console.log(` ${result.token}`)
|
|
1682
2029
|
console.log(chalk.gray('\n Store it so you don’t lose it:'))
|
|
1683
|
-
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret →
|
|
1684
|
-
console.log(chalk.gray(' · Cursor: Settings → Environment variables →
|
|
1685
|
-
console.log(chalk.gray(' · Shell: export
|
|
2030
|
+
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret → WADAH_AGENT_TOKEN'))
|
|
2031
|
+
console.log(chalk.gray(' · Cursor: Settings → Environment variables → WADAH_AGENT_TOKEN'))
|
|
2032
|
+
console.log(chalk.gray(' · Shell: export WADAH_AGENT_TOKEN="<paste token here>"\n'))
|
|
1686
2033
|
}
|
|
1687
2034
|
} catch (err) { handleError(err) }
|
|
1688
2035
|
})
|
|
@@ -2006,12 +2353,12 @@ program
|
|
|
2006
2353
|
addCheck('api_ping', 'fail', `API ping failed: ${err.message}`)
|
|
2007
2354
|
}
|
|
2008
2355
|
|
|
2009
|
-
const hasEnvToken = Boolean(
|
|
2356
|
+
const hasEnvToken = Boolean(envAgentToken() || runtimeToken)
|
|
2010
2357
|
const hasSavedToken = Boolean(confGet('access_token'))
|
|
2011
2358
|
if (!hasEnvToken && !hasSavedToken) {
|
|
2012
2359
|
addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
|
|
2013
2360
|
} else if (hasEnvToken) {
|
|
2014
|
-
addCheck('auth_token', 'pass',
|
|
2361
|
+
addCheck('auth_token', 'pass', `Using agent token from env/flag (${envAgentTokenVarName()})`)
|
|
2015
2362
|
} else {
|
|
2016
2363
|
addCheck('auth_token', 'pass', 'Using token from profile config')
|
|
2017
2364
|
}
|
|
@@ -2037,7 +2384,7 @@ program
|
|
|
2037
2384
|
addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
|
|
2038
2385
|
}
|
|
2039
2386
|
|
|
2040
|
-
// Agent-specific checks (only when
|
|
2387
|
+
// Agent-specific checks (only when an env agent token is set)
|
|
2041
2388
|
if (hasEnvToken && meBody) {
|
|
2042
2389
|
const isAgent = meBody.workspaces?.[0]?.role === 'agent'
|
|
2043
2390
|
if (!isAgent) {
|
|
@@ -2163,7 +2510,7 @@ program
|
|
|
2163
2510
|
const config = {
|
|
2164
2511
|
api_url: getApiBase(),
|
|
2165
2512
|
signed_in_as: confGet('user_email') ?? null,
|
|
2166
|
-
agent_mode: Boolean(
|
|
2513
|
+
agent_mode: Boolean(envAgentToken() || runtimeToken),
|
|
2167
2514
|
profile: getProfile(),
|
|
2168
2515
|
default_board: defaultBoard ?? null,
|
|
2169
2516
|
config_file: conf.path,
|
|
@@ -2176,7 +2523,7 @@ program
|
|
|
2176
2523
|
console.log(chalk.bold('CLI config'))
|
|
2177
2524
|
console.log(chalk.gray(` API URL: ${config.api_url}`))
|
|
2178
2525
|
console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
|
|
2179
|
-
console.log(chalk.gray(` Agent mode: ${config.agent_mode ?
|
|
2526
|
+
console.log(chalk.gray(` Agent mode: ${config.agent_mode ? `yes (${envAgentTokenVarName() ?? 'token flag'} set)` : 'no'}`))
|
|
2180
2527
|
console.log(chalk.gray(` Default board: ${defaultBoard ?? '— (not set, use: wadah config --default-board "Name")'}`))
|
|
2181
2528
|
console.log(chalk.gray(` Profile: ${getProfile()}`))
|
|
2182
2529
|
console.log(chalk.gray(` Config file: ${config.config_file}`))
|
|
@@ -2255,7 +2602,7 @@ const CLI_COMMANDS = [
|
|
|
2255
2602
|
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
|
|
2256
2603
|
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
2257
2604
|
'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',
|
|
2605
|
+
'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'pr', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
2259
2606
|
]
|
|
2260
2607
|
|
|
2261
2608
|
// ── wadah setup-agent ─────────────────────────────────────────────────────────
|
|
@@ -2265,7 +2612,7 @@ program
|
|
|
2265
2612
|
.description('Interactive wizard: create an agent token, write it to your shell profile, and verify everything works')
|
|
2266
2613
|
.option('--name <name>', 'Agent name (skip prompt)')
|
|
2267
2614
|
.option('--board <board>', 'Default board name (skip prompt)')
|
|
2268
|
-
.option('--skip-env', 'Skip writing
|
|
2615
|
+
.option('--skip-env', 'Skip writing WADAH_AGENT_TOKEN to shell profile (print instructions only)')
|
|
2269
2616
|
.action(async (opts) => {
|
|
2270
2617
|
const readline = await import('node:readline/promises')
|
|
2271
2618
|
const os = await import('node:os')
|
|
@@ -2329,18 +2676,18 @@ program
|
|
|
2329
2676
|
|
|
2330
2677
|
// Step 4: write env var to shell profile
|
|
2331
2678
|
console.log()
|
|
2332
|
-
console.log(chalk.bold('Step 4/5 Setting up
|
|
2679
|
+
console.log(chalk.bold('Step 4/5 Setting up WADAH_AGENT_TOKEN…'))
|
|
2333
2680
|
|
|
2334
2681
|
const shell = process.env.SHELL ?? ''
|
|
2335
2682
|
const profileMap = { zsh: '.zshrc', bash: '.bashrc', fish: '.config/fish/config.fish' }
|
|
2336
2683
|
const profileKey = Object.keys(profileMap).find((k) => shell.includes(k))
|
|
2337
2684
|
const profileFile = profileKey ? path.join(os.homedir(), profileMap[profileKey]) : null
|
|
2338
2685
|
const marker = '# added by wadah setup-agent'
|
|
2339
|
-
const exportLine = `\nexport
|
|
2686
|
+
const exportLine = `\nexport WADAH_AGENT_TOKEN="${rawToken}" ${marker}\n`
|
|
2340
2687
|
|
|
2341
2688
|
if (opts.skipEnv || !profileFile) {
|
|
2342
2689
|
console.log(chalk.gray(' Skipping automatic profile write. Add this line manually:'))
|
|
2343
|
-
console.log(chalk.gray(` export
|
|
2690
|
+
console.log(chalk.gray(` export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2344
2691
|
const rcGuess = shell.includes('zsh') ? '~/.zshrc' : shell.includes('fish') ? '~/.config/fish/config.fish' : '~/.bashrc'
|
|
2345
2692
|
console.log(chalk.gray(` Then reload: source ${rcGuess}`))
|
|
2346
2693
|
} else {
|
|
@@ -2350,7 +2697,7 @@ program
|
|
|
2350
2697
|
const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : ''
|
|
2351
2698
|
const filtered = existing
|
|
2352
2699
|
.split('\n')
|
|
2353
|
-
.filter((line) => !line.includes('
|
|
2700
|
+
.filter((line) => !line.includes('WADAH_AGENT_TOKEN=') && !line.includes(marker))
|
|
2354
2701
|
.join('\n')
|
|
2355
2702
|
const next = `${filtered.trimEnd()}${exportLine}`
|
|
2356
2703
|
fs.writeFileSync(profileFile, `${next.endsWith('\n') ? next : `${next}\n`}`)
|
|
@@ -2358,10 +2705,10 @@ program
|
|
|
2358
2705
|
console.log(chalk.gray(` Reload now: source ${profileFile}`))
|
|
2359
2706
|
} catch (e) {
|
|
2360
2707
|
console.log(chalk.yellow(` ⚠ Could not write to ${profileFile}: ${e.message}`))
|
|
2361
|
-
console.log(chalk.gray(` Add manually: export
|
|
2708
|
+
console.log(chalk.gray(` Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2362
2709
|
}
|
|
2363
2710
|
} else {
|
|
2364
|
-
console.log(chalk.gray(` Skipped. Add manually: export
|
|
2711
|
+
console.log(chalk.gray(` Skipped. Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2365
2712
|
}
|
|
2366
2713
|
}
|
|
2367
2714
|
|
package/package.json
CHANGED
package/test/cli.test.js
CHANGED
|
@@ -11,7 +11,7 @@ function runCli(args) {
|
|
|
11
11
|
return new Promise((resolvePromise, reject) => {
|
|
12
12
|
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
13
13
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
-
env: { ...process.env,
|
|
14
|
+
env: { ...process.env, WADAH_AGENT_TOKEN: '' },
|
|
15
15
|
})
|
|
16
16
|
let stdout = ''
|
|
17
17
|
let stderr = ''
|
|
@@ -47,6 +47,12 @@ test('wadah open --help shows open command help', async () => {
|
|
|
47
47
|
assert.match(stdout, /open|List/)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('wadah link --help shows link subcommands', async () => {
|
|
51
|
+
const { code, stdout } = await runCli(['link', '--help'])
|
|
52
|
+
assert.equal(code, 0)
|
|
53
|
+
assert.match(stdout, /add|list|remove/)
|
|
54
|
+
})
|
|
55
|
+
|
|
50
56
|
test('wadah doctor runs without crashing', async () => {
|
|
51
57
|
const { code } = await runCli(['doctor'])
|
|
52
58
|
// Doctor may exit 0 or 1 depending on API/token; we only assert it doesn't throw
|