open-wadah 1.0.5 → 1.0.6
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 +28 -0
- package/cli.js +296 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,11 +35,39 @@ wadah complete <task-id>
|
|
|
35
35
|
|
|
36
36
|
- `wadah --help`
|
|
37
37
|
- `wadah whoami`
|
|
38
|
+
- `wadah doctor`
|
|
38
39
|
- `wadah list --open`
|
|
39
40
|
- `wadah move <task-id> <bucket>`
|
|
40
41
|
- `wadah update <task-id> --title "New title"`
|
|
41
42
|
- `wadah state`
|
|
42
43
|
|
|
44
|
+
## Global Flags
|
|
45
|
+
|
|
46
|
+
- `--profile <name>`: use a named profile (defaults to `default`)
|
|
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)
|
|
50
|
+
|
|
51
|
+
## Stable Error Codes
|
|
52
|
+
|
|
53
|
+
When `--json` is enabled (or output is non-interactive), errors return:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"error":{"code":"auth_error","message":"Not authenticated"}}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Common codes:
|
|
60
|
+
|
|
61
|
+
- `auth_error`
|
|
62
|
+
- `validation_error`
|
|
63
|
+
- `network_error`
|
|
64
|
+
- `api_error`
|
|
65
|
+
- `server_error`
|
|
66
|
+
- `authorization_pending`
|
|
67
|
+
- `invalid_device_code`
|
|
68
|
+
- `token_expired`
|
|
69
|
+
- `login_timeout`
|
|
70
|
+
|
|
43
71
|
## API URL Override
|
|
44
72
|
|
|
45
73
|
Use env var:
|
package/cli.js
CHANGED
|
@@ -6,10 +6,20 @@ 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 { dirname, resolve } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
9
12
|
|
|
10
13
|
const randomUUID = () => webcrypto.randomUUID()
|
|
11
14
|
let runtimeApiBase = null
|
|
12
15
|
const DEFAULT_API_BASE = 'https://api.openwadah.com'
|
|
16
|
+
let runtimeProfile = null
|
|
17
|
+
let runtimeToken = null
|
|
18
|
+
let runtimeJsonOutput = false
|
|
19
|
+
let runtimeQuietOutput = false
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const CLI_VERSION = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8')).version
|
|
13
23
|
|
|
14
24
|
function normalizeApiUrl(url) {
|
|
15
25
|
return String(url ?? '').trim().replace(/\/$/, '')
|
|
@@ -19,18 +29,42 @@ function normalizeApiUrl(url) {
|
|
|
19
29
|
|
|
20
30
|
const conf = new Conf({ projectName: 'open-wadah' })
|
|
21
31
|
|
|
32
|
+
function getProfile() {
|
|
33
|
+
const profile = (process.env.TASK_MANAGER_PROFILE ?? runtimeProfile ?? 'default').trim()
|
|
34
|
+
return profile || 'default'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function profileKey(key) {
|
|
38
|
+
return `profiles.${getProfile()}.${key}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function confGet(key) {
|
|
42
|
+
const scoped = conf.get(profileKey(key))
|
|
43
|
+
if (scoped !== undefined) return scoped
|
|
44
|
+
return conf.get(key)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function confSet(key, value) {
|
|
48
|
+
conf.set(profileKey(key), value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function confDelete(key) {
|
|
52
|
+
conf.delete(profileKey(key))
|
|
53
|
+
}
|
|
54
|
+
|
|
22
55
|
function getApiBase() {
|
|
23
56
|
return (
|
|
24
57
|
(process.env.TASK_MANAGER_API_URL ?? '').trim() ||
|
|
25
58
|
runtimeApiBase ||
|
|
26
|
-
|
|
59
|
+
confGet('api_url') ||
|
|
27
60
|
DEFAULT_API_BASE
|
|
28
61
|
)
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
function getAuthToken() {
|
|
32
65
|
if (process.env.TASK_MANAGER_TOKEN) return process.env.TASK_MANAGER_TOKEN
|
|
33
|
-
|
|
66
|
+
if (runtimeToken) return runtimeToken
|
|
67
|
+
return confGet('access_token') ?? null
|
|
34
68
|
}
|
|
35
69
|
|
|
36
70
|
function authHeaders() {
|
|
@@ -44,7 +78,7 @@ function authHeaders() {
|
|
|
44
78
|
// ── token refresh ─────────────────────────────────────────────────────────────
|
|
45
79
|
|
|
46
80
|
async function refreshAccessToken() {
|
|
47
|
-
const refreshToken =
|
|
81
|
+
const refreshToken = confGet('refresh_token')
|
|
48
82
|
if (!refreshToken) return false
|
|
49
83
|
try {
|
|
50
84
|
const res = await fetch(`${getApiBase()}/api/auth/refresh`, {
|
|
@@ -54,9 +88,9 @@ async function refreshAccessToken() {
|
|
|
54
88
|
})
|
|
55
89
|
if (!res.ok) return false
|
|
56
90
|
const data = await res.json()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
confSet('access_token', data.access_token)
|
|
92
|
+
confSet('refresh_token', data.refresh_token)
|
|
93
|
+
confSet('token_expires', data.expires_at)
|
|
60
94
|
return true
|
|
61
95
|
} catch {
|
|
62
96
|
return false
|
|
@@ -65,7 +99,7 @@ async function refreshAccessToken() {
|
|
|
65
99
|
|
|
66
100
|
async function ensureAuth() {
|
|
67
101
|
if (process.env.TASK_MANAGER_TOKEN) return // agent token, no refresh needed
|
|
68
|
-
const expires =
|
|
102
|
+
const expires = confGet('token_expires')
|
|
69
103
|
// Refresh if within 5 minutes of expiry or already expired
|
|
70
104
|
if (expires && Date.now() > (expires * 1000) - 300_000) {
|
|
71
105
|
await refreshAccessToken()
|
|
@@ -77,7 +111,12 @@ async function ensureAuth() {
|
|
|
77
111
|
async function api(path, options = {}, retry = true) {
|
|
78
112
|
await ensureAuth()
|
|
79
113
|
const url = `${getApiBase()}${path}`
|
|
80
|
-
|
|
114
|
+
let res
|
|
115
|
+
try {
|
|
116
|
+
res = await fetch(url, { headers: authHeaders(), ...options })
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new CliError('network_error', err.message ?? 'Network request failed')
|
|
119
|
+
}
|
|
81
120
|
|
|
82
121
|
// If 401 and we have a refresh token, try once more
|
|
83
122
|
if (res.status === 401 && retry && !process.env.TASK_MANAGER_TOKEN) {
|
|
@@ -86,7 +125,9 @@ async function api(path, options = {}, retry = true) {
|
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
const body = await res.json().catch(() => ({}))
|
|
89
|
-
if (!res.ok)
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw new CliError(mapApiErrorCode(res.status, body.error ?? body.message ?? res.statusText), body.error ?? body.message ?? res.statusText)
|
|
130
|
+
}
|
|
90
131
|
return body
|
|
91
132
|
}
|
|
92
133
|
|
|
@@ -106,6 +147,11 @@ function formatDate(ts) {
|
|
|
106
147
|
}
|
|
107
148
|
|
|
108
149
|
function printTaskList(tasks, state) {
|
|
150
|
+
if (isMachineOutput()) {
|
|
151
|
+
console.log(JSON.stringify({ tasks }, null, runtimeJsonOutput ? 2 : 0))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
const bucketMap = Object.fromEntries(state.buckets.map((b) => [b.id, b.title]))
|
|
110
156
|
const assigneeMap = Object.fromEntries(state.assignees.map((a) => [a.id, a.name]))
|
|
111
157
|
const boardMap = Object.fromEntries(state.boards.map((b) => [b.id, b.name]))
|
|
@@ -134,13 +180,17 @@ function printTaskList(tasks, state) {
|
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
function handleError(err) {
|
|
137
|
-
const msg = err.message ?? ''
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
const msg = err.message ?? 'Unexpected error'
|
|
184
|
+
const code = err.code ?? 'unknown_error'
|
|
185
|
+
const authLike =
|
|
186
|
+
code === 'auth_error' ||
|
|
140
187
|
msg.toLowerCase().includes('missing token') ||
|
|
141
188
|
msg.toLowerCase().includes('invalid token') ||
|
|
142
189
|
msg.toLowerCase().includes('not authenticated')
|
|
143
|
-
|
|
190
|
+
|
|
191
|
+
if (isMachineOutput()) {
|
|
192
|
+
console.log(JSON.stringify({ error: { code, message: msg } }))
|
|
193
|
+
} else if (authLike) {
|
|
144
194
|
console.error(chalk.red('\n✗ Not authenticated.'))
|
|
145
195
|
console.error(chalk.gray(' Humans: wadah login'))
|
|
146
196
|
console.error(chalk.gray(' Agents: TASK_MANAGER_TOKEN=<token> wadah open\n'))
|
|
@@ -182,12 +232,34 @@ function parseCompletedFlag(value) {
|
|
|
182
232
|
const v = value.trim().toLowerCase()
|
|
183
233
|
if (['true', '1', 'yes', 'y'].includes(v)) return true
|
|
184
234
|
if (['false', '0', 'no', 'n'].includes(v)) return false
|
|
185
|
-
throw new
|
|
235
|
+
throw new CliError('validation_error', '--completed must be true or false')
|
|
186
236
|
}
|
|
187
237
|
|
|
188
238
|
function fail(message) {
|
|
189
|
-
|
|
190
|
-
|
|
239
|
+
throw new CliError('validation_error', message)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class CliError extends Error {
|
|
243
|
+
constructor(code, message) {
|
|
244
|
+
super(message)
|
|
245
|
+
this.code = code
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function mapApiErrorCode(status, message) {
|
|
250
|
+
const msg = String(message ?? '').toLowerCase()
|
|
251
|
+
if (status === 401 || msg.includes('missing token') || msg.includes('invalid token') || msg.includes('not authenticated')) {
|
|
252
|
+
return 'auth_error'
|
|
253
|
+
}
|
|
254
|
+
if (status === 404 && msg.includes('invalid_device_code')) return 'invalid_device_code'
|
|
255
|
+
if (status === 410 && msg.includes('expired')) return 'token_expired'
|
|
256
|
+
if (status === 428 && msg.includes('authorization_pending')) return 'authorization_pending'
|
|
257
|
+
if (status >= 500) return 'server_error'
|
|
258
|
+
return 'api_error'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isMachineOutput() {
|
|
262
|
+
return runtimeJsonOutput || runtimeQuietOutput || !process.stdout.isTTY
|
|
191
263
|
}
|
|
192
264
|
|
|
193
265
|
function sleep(ms) {
|
|
@@ -220,12 +292,20 @@ function openBrowser(url) {
|
|
|
220
292
|
program
|
|
221
293
|
.name('wadah')
|
|
222
294
|
.description('Open Wadah CLI — shared task board for humans and agents')
|
|
223
|
-
.version(
|
|
295
|
+
.version(CLI_VERSION)
|
|
224
296
|
.option('--api <url>', 'Override API base URL for this command')
|
|
297
|
+
.option('--profile <name>', 'Use a named profile (default: default)')
|
|
298
|
+
.option('--token <token>', 'Use a token for this command only')
|
|
299
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
300
|
+
.option('--quiet', 'Silence non-error output')
|
|
225
301
|
|
|
226
302
|
program.hook('preAction', (_, actionCommand) => {
|
|
227
303
|
const opts = actionCommand.optsWithGlobals()
|
|
228
304
|
if (opts.api) runtimeApiBase = normalizeApiUrl(opts.api)
|
|
305
|
+
if (opts.profile) runtimeProfile = String(opts.profile).trim()
|
|
306
|
+
if (opts.token) runtimeToken = String(opts.token).trim()
|
|
307
|
+
runtimeJsonOutput = Boolean(opts.json)
|
|
308
|
+
runtimeQuietOutput = Boolean(opts.quiet)
|
|
229
309
|
})
|
|
230
310
|
|
|
231
311
|
// ── tm login ──────────────────────────────────────────────────────────────────
|
|
@@ -237,7 +317,7 @@ program
|
|
|
237
317
|
.option('--password', 'Use terminal email/password instead of browser login')
|
|
238
318
|
.option('--no-browser', "Don't auto-open browser; print URL only")
|
|
239
319
|
.action(async (opts) => {
|
|
240
|
-
if (opts.apiUrl)
|
|
320
|
+
if (opts.apiUrl) confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
|
|
241
321
|
|
|
242
322
|
// Browser-based login (device flow) is default so password managers can autofill.
|
|
243
323
|
if (!opts.password) {
|
|
@@ -248,7 +328,12 @@ program
|
|
|
248
328
|
body: JSON.stringify({ client: 'open-wadah-cli' }),
|
|
249
329
|
})
|
|
250
330
|
const startBody = await startRes.json().catch(() => ({}))
|
|
251
|
-
if (!startRes.ok)
|
|
331
|
+
if (!startRes.ok) {
|
|
332
|
+
throw new CliError(
|
|
333
|
+
mapApiErrorCode(startRes.status, startBody.error ?? 'Could not start browser login'),
|
|
334
|
+
startBody.error ?? 'Could not start browser login'
|
|
335
|
+
)
|
|
336
|
+
}
|
|
252
337
|
|
|
253
338
|
const verificationUrl = startBody.verification_uri_complete ?? startBody.verification_uri
|
|
254
339
|
const pollEveryMs = Math.max(1000, Number(startBody.interval ?? 2) * 1000)
|
|
@@ -276,11 +361,11 @@ program
|
|
|
276
361
|
const pollBody = await pollRes.json().catch(() => ({}))
|
|
277
362
|
|
|
278
363
|
if (pollRes.ok && pollBody.access_token) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
364
|
+
confSet('access_token', pollBody.access_token)
|
|
365
|
+
confSet('refresh_token', pollBody.refresh_token)
|
|
366
|
+
confSet('token_expires', pollBody.expires_at)
|
|
367
|
+
confSet('user_email', pollBody.user?.email ?? null)
|
|
368
|
+
confSet('user_id', pollBody.user?.id ?? null)
|
|
284
369
|
console.log(chalk.green(`\n✓ Signed in${pollBody.user?.email ? ` as ${pollBody.user.email}` : ''}\n`))
|
|
285
370
|
return
|
|
286
371
|
}
|
|
@@ -291,21 +376,24 @@ program
|
|
|
291
376
|
const pending = pollRes.status === 428 || pollBody.error === 'authorization_pending' || invalidCodeRace
|
|
292
377
|
if (!pending) {
|
|
293
378
|
if (pollRes.status === 410 || pollBody.error === 'expired_token') {
|
|
294
|
-
throw new
|
|
379
|
+
throw new CliError('token_expired', 'Browser login expired. Please run wadah login again.')
|
|
295
380
|
}
|
|
296
|
-
throw new
|
|
381
|
+
throw new CliError(mapApiErrorCode(pollRes.status, pollBody.error ?? 'Browser login failed'), pollBody.error ?? 'Browser login failed')
|
|
297
382
|
}
|
|
298
383
|
await sleep(pollEveryMs)
|
|
299
384
|
}
|
|
300
385
|
|
|
301
|
-
throw new
|
|
386
|
+
throw new CliError('login_timeout', 'Browser login timed out. Please run wadah login again.')
|
|
302
387
|
} catch (err) {
|
|
303
|
-
|
|
304
|
-
process.exit(1)
|
|
388
|
+
handleError(err)
|
|
305
389
|
}
|
|
306
390
|
return
|
|
307
391
|
}
|
|
308
392
|
|
|
393
|
+
if (!process.stdin.isTTY) {
|
|
394
|
+
fail('Password login requires an interactive terminal')
|
|
395
|
+
}
|
|
396
|
+
|
|
309
397
|
console.log(chalk.bold('\nOpen Wadah — Sign in\n'))
|
|
310
398
|
const email = await input({ message: 'Email:' })
|
|
311
399
|
const pass = await promptPassword({ message: 'Password:' })
|
|
@@ -317,17 +405,16 @@ program
|
|
|
317
405
|
body: JSON.stringify({ email, password: pass }),
|
|
318
406
|
})
|
|
319
407
|
const body = await data.json()
|
|
320
|
-
if (!data.ok) throw new
|
|
408
|
+
if (!data.ok) throw new CliError(mapApiErrorCode(data.status, body.error ?? 'Login failed'), body.error ?? 'Login failed')
|
|
321
409
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
410
|
+
confSet('access_token', body.access_token)
|
|
411
|
+
confSet('refresh_token', body.refresh_token)
|
|
412
|
+
confSet('token_expires', body.expires_at)
|
|
413
|
+
confSet('user_email', body.user.email)
|
|
414
|
+
confSet('user_id', body.user.id)
|
|
327
415
|
console.log(chalk.green(`\n✓ Signed in as ${body.user.email}\n`))
|
|
328
416
|
} catch (err) {
|
|
329
|
-
|
|
330
|
-
process.exit(1)
|
|
417
|
+
handleError(err)
|
|
331
418
|
}
|
|
332
419
|
})
|
|
333
420
|
|
|
@@ -338,7 +425,11 @@ program
|
|
|
338
425
|
.description('Create a new account')
|
|
339
426
|
.option('--api-url <url>', 'Override API base URL and save it')
|
|
340
427
|
.action(async (opts) => {
|
|
341
|
-
if (opts.apiUrl)
|
|
428
|
+
if (opts.apiUrl) confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
|
|
429
|
+
|
|
430
|
+
if (!process.stdin.isTTY) {
|
|
431
|
+
fail('Signup requires an interactive terminal')
|
|
432
|
+
}
|
|
342
433
|
|
|
343
434
|
console.log(chalk.bold('\nOpen Wadah — Create account\n'))
|
|
344
435
|
const name = await input({ message: 'Name:' })
|
|
@@ -353,7 +444,7 @@ program
|
|
|
353
444
|
body: JSON.stringify({ name, email, password: pass }),
|
|
354
445
|
})
|
|
355
446
|
const signupBody = await signupRes.json()
|
|
356
|
-
if (!signupRes.ok) throw new
|
|
447
|
+
if (!signupRes.ok) throw new CliError(mapApiErrorCode(signupRes.status, signupBody.error ?? 'Signup failed'), signupBody.error ?? 'Signup failed')
|
|
357
448
|
|
|
358
449
|
// Auto login
|
|
359
450
|
const loginRes = await fetch(`${getApiBase()}/api/auth/login`, {
|
|
@@ -369,18 +460,17 @@ program
|
|
|
369
460
|
console.log(chalk.gray(' Check your inbox and confirm your email, then run: wadah login\n'))
|
|
370
461
|
return
|
|
371
462
|
}
|
|
372
|
-
throw new
|
|
463
|
+
throw new CliError(mapApiErrorCode(loginRes.status, msg), msg)
|
|
373
464
|
}
|
|
374
465
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
466
|
+
confSet('access_token', loginBody.access_token)
|
|
467
|
+
confSet('refresh_token', loginBody.refresh_token)
|
|
468
|
+
confSet('token_expires', loginBody.expires_at)
|
|
469
|
+
confSet('user_email', loginBody.user.email)
|
|
470
|
+
confSet('user_id', loginBody.user.id)
|
|
380
471
|
console.log(chalk.green(`\n✓ Account created and signed in as ${email}\n`))
|
|
381
472
|
} catch (err) {
|
|
382
|
-
|
|
383
|
-
process.exit(1)
|
|
473
|
+
handleError(err)
|
|
384
474
|
}
|
|
385
475
|
})
|
|
386
476
|
|
|
@@ -390,12 +480,12 @@ program
|
|
|
390
480
|
.command('logout')
|
|
391
481
|
.description('Sign out and clear stored credentials')
|
|
392
482
|
.action(() => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
console.log(chalk.green('✓ Signed out\n'))
|
|
483
|
+
confDelete('access_token')
|
|
484
|
+
confDelete('refresh_token')
|
|
485
|
+
confDelete('token_expires')
|
|
486
|
+
confDelete('user_email')
|
|
487
|
+
confDelete('user_id')
|
|
488
|
+
if (!runtimeQuietOutput) console.log(chalk.green('✓ Signed out\n'))
|
|
399
489
|
})
|
|
400
490
|
|
|
401
491
|
// ── tm whoami ─────────────────────────────────────────────────────────────────
|
|
@@ -404,12 +494,47 @@ program
|
|
|
404
494
|
.command('whoami')
|
|
405
495
|
.description('Show current identity and workspace')
|
|
406
496
|
.action(async () => {
|
|
497
|
+
if (isMachineOutput()) {
|
|
498
|
+
try {
|
|
499
|
+
if (process.env.TASK_MANAGER_TOKEN || runtimeToken) {
|
|
500
|
+
const me = await api('/api/auth/me')
|
|
501
|
+
const ws = me.workspaces?.[0] ?? null
|
|
502
|
+
console.log(JSON.stringify({
|
|
503
|
+
mode: 'agent',
|
|
504
|
+
api: getApiBase(),
|
|
505
|
+
email: me.user?.email ?? null,
|
|
506
|
+
workspace: ws ? { id: ws.id, name: ws.name, role: ws.role } : null,
|
|
507
|
+
profile: getProfile(),
|
|
508
|
+
}, null, runtimeJsonOutput ? 2 : 0))
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const email = confGet('user_email')
|
|
513
|
+
if (!email) {
|
|
514
|
+
console.log(JSON.stringify({ signed_in: false, api: getApiBase(), profile: getProfile() }, null, runtimeJsonOutput ? 2 : 0))
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
const me = await api('/api/auth/me')
|
|
518
|
+
const ws = me.workspaces?.[0] ?? null
|
|
519
|
+
console.log(JSON.stringify({
|
|
520
|
+
signed_in: true,
|
|
521
|
+
api: getApiBase(),
|
|
522
|
+
email: me.user?.email ?? email,
|
|
523
|
+
workspace: ws ? { id: ws.id, name: ws.name, role: ws.role } : null,
|
|
524
|
+
profile: getProfile(),
|
|
525
|
+
}, null, runtimeJsonOutput ? 2 : 0))
|
|
526
|
+
} catch (err) {
|
|
527
|
+
handleError(err)
|
|
528
|
+
}
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
407
532
|
if (process.env.TASK_MANAGER_TOKEN) {
|
|
408
533
|
console.log(chalk.bold('\nAgent') + ' (TASK_MANAGER_TOKEN)')
|
|
409
534
|
} else {
|
|
410
|
-
const email =
|
|
535
|
+
const email = confGet('user_email')
|
|
411
536
|
if (!email) {
|
|
412
|
-
console.log(chalk.gray('\nNot signed in. Run:
|
|
537
|
+
console.log(chalk.gray('\nNot signed in. Run: wadah login\n'))
|
|
413
538
|
return
|
|
414
539
|
}
|
|
415
540
|
try {
|
|
@@ -421,7 +546,7 @@ program
|
|
|
421
546
|
console.log(chalk.gray(`API: ${getApiBase()}`))
|
|
422
547
|
console.log()
|
|
423
548
|
} catch {
|
|
424
|
-
console.log(chalk.gray(`\n${email} (token may be expired — run:
|
|
549
|
+
console.log(chalk.gray(`\n${email} (token may be expired — run: wadah login)\n`))
|
|
425
550
|
}
|
|
426
551
|
}
|
|
427
552
|
})
|
|
@@ -840,6 +965,103 @@ program
|
|
|
840
965
|
} catch (err) { handleError(err) }
|
|
841
966
|
})
|
|
842
967
|
|
|
968
|
+
program
|
|
969
|
+
.command('doctor')
|
|
970
|
+
.description('Run CLI diagnostics for API/auth/profile setup')
|
|
971
|
+
.action(async () => {
|
|
972
|
+
const checks = []
|
|
973
|
+
const addCheck = (id, status, message, details = null) => checks.push({ id, status, message, details })
|
|
974
|
+
|
|
975
|
+
addCheck('profile', 'pass', `Using profile "${getProfile()}"`)
|
|
976
|
+
addCheck('api_base', 'pass', `API base is ${getApiBase()}`)
|
|
977
|
+
|
|
978
|
+
let pingOk = false
|
|
979
|
+
try {
|
|
980
|
+
const pingRes = await fetch(`${getApiBase()}/api/ping`, { headers: { 'Content-Type': 'application/json' } })
|
|
981
|
+
if (pingRes.ok) {
|
|
982
|
+
pingOk = true
|
|
983
|
+
addCheck('api_ping', 'pass', 'API is reachable')
|
|
984
|
+
} else {
|
|
985
|
+
addCheck('api_ping', 'fail', `API ping failed with status ${pingRes.status}`)
|
|
986
|
+
}
|
|
987
|
+
} catch (err) {
|
|
988
|
+
addCheck('api_ping', 'fail', `API ping failed: ${err.message}`)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const hasEnvToken = Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken)
|
|
992
|
+
const hasSavedToken = Boolean(confGet('access_token'))
|
|
993
|
+
if (!hasEnvToken && !hasSavedToken) {
|
|
994
|
+
addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
|
|
995
|
+
} else if (hasEnvToken) {
|
|
996
|
+
addCheck('auth_token', 'pass', 'Using token from env/flag')
|
|
997
|
+
} else {
|
|
998
|
+
addCheck('auth_token', 'pass', 'Using token from profile config')
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (pingOk) {
|
|
1002
|
+
try {
|
|
1003
|
+
const meRes = await fetch(`${getApiBase()}/api/auth/me`, { headers: authHeaders() })
|
|
1004
|
+
if (meRes.ok) {
|
|
1005
|
+
const meBody = await meRes.json().catch(() => ({}))
|
|
1006
|
+
addCheck('auth_me', 'pass', `Authenticated as ${meBody.user?.email ?? 'unknown user'}`)
|
|
1007
|
+
} else if (meRes.status === 401) {
|
|
1008
|
+
addCheck('auth_me', 'warn', 'Token is missing/expired. Run wadah login.')
|
|
1009
|
+
} else {
|
|
1010
|
+
addCheck('auth_me', 'warn', `Auth check returned status ${meRes.status}`)
|
|
1011
|
+
}
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
try {
|
|
1017
|
+
const deviceRes = await fetch(`${getApiBase()}/api/auth/device/start`, {
|
|
1018
|
+
method: 'POST',
|
|
1019
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1020
|
+
body: JSON.stringify({ client: 'open-wadah-cli-doctor' }),
|
|
1021
|
+
})
|
|
1022
|
+
if (deviceRes.ok) {
|
|
1023
|
+
addCheck('device_login', 'pass', 'Browser login endpoint is available')
|
|
1024
|
+
} else {
|
|
1025
|
+
addCheck('device_login', 'warn', `Browser login endpoint returned ${deviceRes.status}`)
|
|
1026
|
+
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
addCheck('device_login', 'warn', `Browser login endpoint failed: ${err.message}`)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const failed = checks.filter((c) => c.status === 'fail').length
|
|
1033
|
+
const warned = checks.filter((c) => c.status === 'warn').length
|
|
1034
|
+
const report = {
|
|
1035
|
+
ok: failed === 0,
|
|
1036
|
+
summary: { failed, warned, total: checks.length },
|
|
1037
|
+
checks,
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (isMachineOutput()) {
|
|
1041
|
+
console.log(JSON.stringify(report, null, runtimeJsonOutput ? 2 : 0))
|
|
1042
|
+
if (failed > 0) process.exit(1)
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
console.log()
|
|
1047
|
+
console.log(chalk.bold('Open Wadah Doctor'))
|
|
1048
|
+
checks.forEach((check) => {
|
|
1049
|
+
const mark = check.status === 'pass' ? chalk.green('✓') : check.status === 'warn' ? chalk.yellow('!') : chalk.red('✗')
|
|
1050
|
+
console.log(` ${mark} ${check.id}: ${check.message}`)
|
|
1051
|
+
})
|
|
1052
|
+
console.log()
|
|
1053
|
+
if (failed > 0) {
|
|
1054
|
+
console.log(chalk.red(` ${failed} check(s) failed. Resolve failures and rerun wadah doctor.`))
|
|
1055
|
+
console.log()
|
|
1056
|
+
process.exit(1)
|
|
1057
|
+
} else if (warned > 0) {
|
|
1058
|
+
console.log(chalk.yellow(` Completed with ${warned} warning(s).`))
|
|
1059
|
+
console.log()
|
|
1060
|
+
} else {
|
|
1061
|
+
console.log(chalk.green(' All checks passed.\n'))
|
|
1062
|
+
}
|
|
1063
|
+
})
|
|
1064
|
+
|
|
843
1065
|
// ── tm invite ─────────────────────────────────────────────────────────────────
|
|
844
1066
|
|
|
845
1067
|
program
|
|
@@ -868,16 +1090,28 @@ program
|
|
|
868
1090
|
.option('--api-url <url>', 'Set API base URL')
|
|
869
1091
|
.action((opts) => {
|
|
870
1092
|
if (opts.apiUrl) {
|
|
871
|
-
|
|
872
|
-
console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
|
|
1093
|
+
confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
|
|
1094
|
+
if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
|
|
1095
|
+
return
|
|
1096
|
+
}
|
|
1097
|
+
const config = {
|
|
1098
|
+
api_url: getApiBase(),
|
|
1099
|
+
signed_in_as: confGet('user_email') ?? null,
|
|
1100
|
+
agent_mode: Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken),
|
|
1101
|
+
profile: getProfile(),
|
|
1102
|
+
config_file: conf.path,
|
|
1103
|
+
}
|
|
1104
|
+
if (isMachineOutput()) {
|
|
1105
|
+
console.log(JSON.stringify(config, null, runtimeJsonOutput ? 2 : 0))
|
|
873
1106
|
return
|
|
874
1107
|
}
|
|
875
1108
|
console.log()
|
|
876
1109
|
console.log(chalk.bold('CLI config'))
|
|
877
|
-
console.log(chalk.gray(` API URL: ${
|
|
878
|
-
console.log(chalk.gray(` Signed in as: ${
|
|
879
|
-
console.log(chalk.gray(` Agent mode: ${
|
|
880
|
-
console.log(chalk.gray(`
|
|
1110
|
+
console.log(chalk.gray(` API URL: ${config.api_url}`))
|
|
1111
|
+
console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
|
|
1112
|
+
console.log(chalk.gray(` Agent mode: ${config.agent_mode ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
|
|
1113
|
+
console.log(chalk.gray(` Profile: ${getProfile()}`))
|
|
1114
|
+
console.log(chalk.gray(` Config file: ${config.config_file}`))
|
|
881
1115
|
console.log()
|
|
882
1116
|
})
|
|
883
1117
|
|