@toast-ninja/toast-cli 0.1.2 → 0.1.4
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 +19 -6
- package/package.json +1 -1
- package/skills/review-ci/SKILL.md +2 -2
- package/src/cli.js +136 -29
package/README.md
CHANGED
|
@@ -22,25 +22,38 @@ npm install -g ./packages/toast-cli
|
|
|
22
22
|
## Review CI Auth
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
toast
|
|
25
|
+
toast auth
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
The default auth flow opens a browser, completes GitHub auth through Toast, and
|
|
29
|
-
stores
|
|
29
|
+
stores an org-scoped Toast token after the localhost callback returns. When run
|
|
30
|
+
inside a GitHub checkout, `toast auth` infers the org from `origin`.
|
|
31
|
+
|
|
32
|
+
Use `--org` when authenticating outside a repo or when you need an explicit
|
|
33
|
+
scope:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
toast auth --org toast-ninja
|
|
37
|
+
```
|
|
30
38
|
|
|
31
39
|
If browser auth is not available, use GitHub device auth explicitly:
|
|
32
40
|
|
|
33
41
|
```bash
|
|
34
|
-
toast
|
|
42
|
+
toast auth --device
|
|
35
43
|
```
|
|
36
44
|
|
|
37
45
|
For automation or local debugging with an existing GitHub token:
|
|
38
46
|
|
|
39
47
|
```bash
|
|
40
|
-
toast
|
|
48
|
+
toast auth --org toast-ninja --github-token "$GITHUB_TOKEN"
|
|
41
49
|
```
|
|
42
50
|
|
|
43
|
-
That token must be a user token that can prove active membership in the
|
|
51
|
+
That token must be a user token that can prove active membership in the org.
|
|
52
|
+
GitHub Actions' repository `GITHUB_TOKEN` is not a user login token; use
|
|
53
|
+
`TOAST_TOKEN` in CI.
|
|
54
|
+
|
|
55
|
+
The older `toast review-ci auth --repo ORG/REPO` command still works as a
|
|
56
|
+
compatibility alias.
|
|
44
57
|
|
|
45
58
|
## Review CI
|
|
46
59
|
|
|
@@ -93,7 +106,7 @@ toast review-ci instructions
|
|
|
93
106
|
Before using `gh` to rediscover review context, run:
|
|
94
107
|
|
|
95
108
|
```bash
|
|
96
|
-
toast review-ci next --repo
|
|
109
|
+
toast review-ci next --repo ORG/REPO --pr NUMBER --head HEAD_SHA
|
|
97
110
|
```
|
|
98
111
|
|
|
99
112
|
Fix every unresolved thread/comment listed by URL. Reply with what changed,
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ Use Toast Review CI before rediscovering review context with `gh`.
|
|
|
10
10
|
1. Run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
toast review-ci next --repo
|
|
13
|
+
toast review-ci next --repo ORG/REPO --pr NUMBER --head HEAD_SHA
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
2. Fix every `blocking_items` entry. Prefer the URL and body from Toast over a fresh GitHub search.
|
|
@@ -19,7 +19,7 @@ toast review-ci next --repo OWNER/REPO --pr NUMBER --head HEAD_SHA
|
|
|
19
19
|
5. Push the fix, then wait on the exact new head:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
toast review-ci wait --repo
|
|
22
|
+
toast review-ci wait --repo ORG/REPO --pr NUMBER --head HEAD_SHA
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Only report completion after Review CI returns `acknowledged`.
|
package/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ const REVIEW_CI_SKILL_PATH = path.join(
|
|
|
26
26
|
'SKILL.md',
|
|
27
27
|
)
|
|
28
28
|
const REVIEW_CI_COMMANDS = new Set(['review-ci'])
|
|
29
|
+
const AUTH_COMMANDS = new Set(['auth', 'login'])
|
|
29
30
|
|
|
30
31
|
const sleepDefault = ms =>
|
|
31
32
|
new Promise(resolve => {
|
|
@@ -58,13 +59,107 @@ const parseArgs = args => {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
const parseRepo = repoFullName => {
|
|
61
|
-
|
|
62
|
+
if (repoFullName === true) throw new Error('Missing --repo')
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
const parts = String(repoFullName || '').split('/')
|
|
65
|
+
const [owner, repo] = parts
|
|
66
|
+
|
|
67
|
+
if (parts.length !== 2 || !owner || !repo) {
|
|
68
|
+
throw new Error('--repo must be ORG/REPO')
|
|
69
|
+
}
|
|
64
70
|
|
|
65
71
|
return { owner, repo, fullName: `${owner}/${repo}` }
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
const parseOrg = org => {
|
|
75
|
+
if (org === true) throw new Error('Missing --org')
|
|
76
|
+
|
|
77
|
+
const normalizedOrg = String(org || '').trim()
|
|
78
|
+
|
|
79
|
+
if (!normalizedOrg || normalizedOrg.includes('/')) {
|
|
80
|
+
throw new Error('--org must be a GitHub organization name')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalizedOrg
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parseGithubRemoteUrl = remoteUrl => {
|
|
87
|
+
const value = String(remoteUrl || '').trim()
|
|
88
|
+
const sshMatch = value.match(/^[^@/:]+@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/)
|
|
89
|
+
|
|
90
|
+
if (sshMatch) return parseRepo(`${sshMatch[1]}/${sshMatch[2]}`)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(value)
|
|
94
|
+
|
|
95
|
+
if (!['git:', 'http:', 'https:', 'ssh:'].includes(url.protocol)) return null
|
|
96
|
+
|
|
97
|
+
const parts = url.pathname
|
|
98
|
+
.replace(/^\/+/, '')
|
|
99
|
+
.replace(/\/+$/, '')
|
|
100
|
+
.replace(/\.git$/, '')
|
|
101
|
+
.split('/')
|
|
102
|
+
|
|
103
|
+
if (parts.length !== 2) return null
|
|
104
|
+
|
|
105
|
+
return parseRepo(`${parts[0]}/${parts[1]}`)
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const getGitRemoteUrlDefault = () =>
|
|
112
|
+
childProcess
|
|
113
|
+
.execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
116
|
+
})
|
|
117
|
+
.trim()
|
|
118
|
+
|
|
119
|
+
const getAuthScope = ({ options, getGitRemoteUrl }) => {
|
|
120
|
+
const explicitOrg = options.org ? parseOrg(options.org) : null
|
|
121
|
+
const explicitRepo = options.repo ? parseRepo(options.repo) : null
|
|
122
|
+
|
|
123
|
+
if (explicitOrg && explicitRepo && explicitOrg !== explicitRepo.owner) {
|
|
124
|
+
throw new Error('--org must match --repo owner')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (explicitOrg) {
|
|
128
|
+
return {
|
|
129
|
+
org: explicitOrg,
|
|
130
|
+
...(explicitRepo ? { repo: explicitRepo.fullName } : {}),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (explicitRepo) {
|
|
135
|
+
return {
|
|
136
|
+
org: explicitRepo.owner,
|
|
137
|
+
repo: explicitRepo.fullName,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let inferredRepo = null
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
inferredRepo = parseGithubRemoteUrl(getGitRemoteUrl())
|
|
145
|
+
} catch (e) {
|
|
146
|
+
inferredRepo = null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!inferredRepo) {
|
|
150
|
+
throw new Error('Missing --org (or run from a GitHub repo with origin set)')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
org: inferredRepo.owner,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const getAuthScopeBody = scope => ({
|
|
159
|
+
org: scope.org,
|
|
160
|
+
...(scope.repo ? { repo: scope.repo } : {}),
|
|
161
|
+
})
|
|
162
|
+
|
|
68
163
|
const requireOption = (options, key) => {
|
|
69
164
|
if (!options[key]) throw new Error(`Missing --${key}`)
|
|
70
165
|
|
|
@@ -114,12 +209,18 @@ const parseBrowserCallbackAuth = ({ requestUrl, expectedState }) => {
|
|
|
114
209
|
token_type: callbackUrl.searchParams.get('token_type'),
|
|
115
210
|
expires_in: Number(callbackUrl.searchParams.get('expires_in')),
|
|
116
211
|
github_login: callbackUrl.searchParams.get('github_login'),
|
|
212
|
+
org: callbackUrl.searchParams.get('org'),
|
|
117
213
|
repo: callbackUrl.searchParams.get('repo'),
|
|
118
214
|
}
|
|
119
215
|
|
|
120
216
|
if (auth.code) return { code: auth.code }
|
|
121
217
|
|
|
122
|
-
if (
|
|
218
|
+
if (
|
|
219
|
+
!auth.token ||
|
|
220
|
+
!auth.github_login ||
|
|
221
|
+
(!auth.org && !auth.repo) ||
|
|
222
|
+
!auth.expires_in
|
|
223
|
+
) {
|
|
123
224
|
throw new Error('Review CI auth callback was missing credentials')
|
|
124
225
|
}
|
|
125
226
|
|
|
@@ -236,9 +337,7 @@ const requestReviewState = async ({
|
|
|
236
337
|
const token = getToken({ options, env, config })
|
|
237
338
|
|
|
238
339
|
if (!token)
|
|
239
|
-
throw new Error(
|
|
240
|
-
'Missing Toast token. Run `toast review-ci auth` or set TOAST_TOKEN.',
|
|
241
|
-
)
|
|
340
|
+
throw new Error('Missing Toast token. Run `toast auth` or set TOAST_TOKEN.')
|
|
242
341
|
|
|
243
342
|
const requestOptions = {
|
|
244
343
|
method: 'GET',
|
|
@@ -370,7 +469,12 @@ const handleWait = async context => {
|
|
|
370
469
|
}
|
|
371
470
|
|
|
372
471
|
const completeLogin = ({ context, auth, apiUrl }) => {
|
|
373
|
-
const existingConfig = context.readConfig()
|
|
472
|
+
const existingConfig = { ...context.readConfig() }
|
|
473
|
+
const org = auth.org || (auth.repo ? parseRepo(auth.repo).owner : null)
|
|
474
|
+
|
|
475
|
+
if (!org) throw new Error('Login response was missing org')
|
|
476
|
+
delete existingConfig.repo
|
|
477
|
+
|
|
374
478
|
const expiresAt = new Date(
|
|
375
479
|
toMillis(context.now()) + auth.expires_in * 1000,
|
|
376
480
|
).toISOString()
|
|
@@ -378,18 +482,18 @@ const completeLogin = ({ context, auth, apiUrl }) => {
|
|
|
378
482
|
context.writeConfig({
|
|
379
483
|
...existingConfig,
|
|
380
484
|
apiUrl,
|
|
381
|
-
|
|
485
|
+
org,
|
|
382
486
|
token: auth.token,
|
|
383
487
|
githubLogin: auth.github_login,
|
|
384
488
|
expiresAt,
|
|
385
489
|
})
|
|
386
|
-
context.stdout.write(`Authenticated ${auth.github_login} for ${
|
|
490
|
+
context.stdout.write(`Authenticated ${auth.github_login} for ${org}\n`)
|
|
387
491
|
context.stdout.write(
|
|
388
492
|
'Agent setup: run `toast review-ci instructions --install-codex` to install Review CI guidance.\n',
|
|
389
493
|
)
|
|
390
494
|
}
|
|
391
495
|
|
|
392
|
-
const exchangeGithubToken = async ({ context, options, apiUrl,
|
|
496
|
+
const exchangeGithubToken = async ({ context, options, apiUrl, scope }) => {
|
|
393
497
|
const githubToken =
|
|
394
498
|
options['github-token'] === true
|
|
395
499
|
? context.env.GITHUB_TOKEN
|
|
@@ -401,7 +505,7 @@ const exchangeGithubToken = async ({ context, options, apiUrl, repo }) => {
|
|
|
401
505
|
method: 'POST',
|
|
402
506
|
url: `${apiUrl}/api/review-ci/auth/github`,
|
|
403
507
|
body: {
|
|
404
|
-
|
|
508
|
+
...getAuthScopeBody(scope),
|
|
405
509
|
github_access_token: githubToken,
|
|
406
510
|
},
|
|
407
511
|
})
|
|
@@ -413,13 +517,11 @@ const exchangeGithubToken = async ({ context, options, apiUrl, repo }) => {
|
|
|
413
517
|
return response.body
|
|
414
518
|
}
|
|
415
519
|
|
|
416
|
-
const runDeviceLogin = async ({ context, apiUrl,
|
|
520
|
+
const runDeviceLogin = async ({ context, apiUrl, scope }) => {
|
|
417
521
|
const startResponse = await context.request({
|
|
418
522
|
method: 'POST',
|
|
419
523
|
url: `${apiUrl}/api/review-ci/auth/github/device/start`,
|
|
420
|
-
body:
|
|
421
|
-
repo: repo.fullName,
|
|
422
|
-
},
|
|
524
|
+
body: getAuthScopeBody(scope),
|
|
423
525
|
})
|
|
424
526
|
|
|
425
527
|
if (startResponse.statusCode >= 400) {
|
|
@@ -440,7 +542,7 @@ const runDeviceLogin = async ({ context, apiUrl, repo }) => {
|
|
|
440
542
|
method: 'POST',
|
|
441
543
|
url: `${apiUrl}/api/review-ci/auth/github/device/complete`,
|
|
442
544
|
body: {
|
|
443
|
-
|
|
545
|
+
...getAuthScopeBody(scope),
|
|
444
546
|
device_code: device.device_code,
|
|
445
547
|
},
|
|
446
548
|
})
|
|
@@ -459,7 +561,7 @@ const runDeviceLogin = async ({ context, apiUrl, repo }) => {
|
|
|
459
561
|
throw new Error('Device login timed out')
|
|
460
562
|
}
|
|
461
563
|
|
|
462
|
-
const runBrowserLogin = async ({ context, apiUrl,
|
|
564
|
+
const runBrowserLogin = async ({ context, apiUrl, scope }) => {
|
|
463
565
|
const callback = await context.createBrowserCallbackServer()
|
|
464
566
|
|
|
465
567
|
try {
|
|
@@ -467,9 +569,7 @@ const runBrowserLogin = async ({ context, apiUrl, repo }) => {
|
|
|
467
569
|
method: 'POST',
|
|
468
570
|
url: `${apiUrl}/api/review-ci/auth/github/browser/start`,
|
|
469
571
|
body: {
|
|
470
|
-
|
|
471
|
-
owner: repo.owner,
|
|
472
|
-
repository: repo.repo,
|
|
572
|
+
...getAuthScopeBody(scope),
|
|
473
573
|
redirect_uri: callback.redirectUri,
|
|
474
574
|
state: callback.state,
|
|
475
575
|
},
|
|
@@ -505,6 +605,7 @@ const runBrowserLogin = async ({ context, apiUrl, repo }) => {
|
|
|
505
605
|
url: `${apiUrl}/api/review-ci/auth/github/browser/complete`,
|
|
506
606
|
body: {
|
|
507
607
|
code: callbackAuth.code,
|
|
608
|
+
...getAuthScopeBody(scope),
|
|
508
609
|
},
|
|
509
610
|
})
|
|
510
611
|
|
|
@@ -523,16 +624,19 @@ const runBrowserLogin = async ({ context, apiUrl, repo }) => {
|
|
|
523
624
|
const handleAuth = async context => {
|
|
524
625
|
const config = context.readConfig()
|
|
525
626
|
const { options } = parseArgs(context.commandArgs || context.argv.slice(3))
|
|
526
|
-
const
|
|
627
|
+
const scope = getAuthScope({
|
|
628
|
+
options,
|
|
629
|
+
getGitRemoteUrl: context.getGitRemoteUrl,
|
|
630
|
+
})
|
|
527
631
|
const apiUrl = getApiUrl({ options, env: context.env, config })
|
|
528
632
|
let auth
|
|
529
633
|
|
|
530
634
|
if (options['github-token']) {
|
|
531
|
-
auth = await exchangeGithubToken({ context, options, apiUrl,
|
|
635
|
+
auth = await exchangeGithubToken({ context, options, apiUrl, scope })
|
|
532
636
|
} else if (options.device) {
|
|
533
|
-
auth = await runDeviceLogin({ context, apiUrl,
|
|
637
|
+
auth = await runDeviceLogin({ context, apiUrl, scope })
|
|
534
638
|
} else {
|
|
535
|
-
auth = await runBrowserLogin({ context, apiUrl,
|
|
639
|
+
auth = await runBrowserLogin({ context, apiUrl, scope })
|
|
536
640
|
}
|
|
537
641
|
|
|
538
642
|
completeLogin({ context, auth, apiUrl })
|
|
@@ -572,10 +676,11 @@ const handleInstructions = async context => {
|
|
|
572
676
|
|
|
573
677
|
const printHelp = stdout => {
|
|
574
678
|
stdout.write(`Usage:
|
|
575
|
-
toast
|
|
576
|
-
toast review-ci
|
|
577
|
-
toast review-ci
|
|
578
|
-
toast review-ci
|
|
679
|
+
toast auth [--org ORG] [--device|--github-token TOKEN]
|
|
680
|
+
toast review-ci auth --repo ORG/REPO [--device|--github-token TOKEN]
|
|
681
|
+
toast review-ci status --repo ORG/REPO --pr NUMBER --head SHA
|
|
682
|
+
toast review-ci next --repo ORG/REPO --pr NUMBER --head SHA
|
|
683
|
+
toast review-ci wait --repo ORG/REPO --pr NUMBER --head SHA
|
|
579
684
|
toast review-ci instructions [--install-codex]
|
|
580
685
|
`)
|
|
581
686
|
}
|
|
@@ -601,6 +706,7 @@ const main = async ({
|
|
|
601
706
|
now = () => new Date(),
|
|
602
707
|
createBrowserCallbackServer = createBrowserCallbackServerDefault,
|
|
603
708
|
openBrowser = openBrowserDefault,
|
|
709
|
+
getGitRemoteUrl = getGitRemoteUrlDefault,
|
|
604
710
|
stdout = process.stdout,
|
|
605
711
|
stderr = process.stderr,
|
|
606
712
|
}) => {
|
|
@@ -614,12 +720,13 @@ const main = async ({
|
|
|
614
720
|
now,
|
|
615
721
|
createBrowserCallbackServer,
|
|
616
722
|
openBrowser,
|
|
723
|
+
getGitRemoteUrl,
|
|
617
724
|
stdout,
|
|
618
725
|
stderr,
|
|
619
726
|
}
|
|
620
727
|
|
|
621
728
|
try {
|
|
622
|
-
if (argv[2]
|
|
729
|
+
if (AUTH_COMMANDS.has(argv[2])) {
|
|
623
730
|
return await handleAuth({ ...context, commandArgs: argv.slice(3) })
|
|
624
731
|
}
|
|
625
732
|
|