@haystackeditor/cli 0.15.15 → 0.15.16
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/dist/commands/webhooks.d.ts +15 -3
- package/dist/commands/webhooks.js +49 -15
- package/dist/index.js +12 -8
- package/package.json +5 -3
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** currentRepo() for token-account resolution. Outside a git repo there is
|
|
2
|
+
* simply no repo context — expected for id-based commands run from anywhere,
|
|
3
|
+
* so that case stays quiet. A remote that EXISTS but isn't recognized is a
|
|
4
|
+
* different story: repo-scoped account defaults silently won't apply, so say
|
|
5
|
+
* so (classified by error type per PR007). */
|
|
6
|
+
export declare function tryCurrentRepo(): string | null;
|
|
7
|
+
/** Token-resolution scope for adminFetch: explicit repo wins, else the cwd's
|
|
8
|
+
* origin remote, else no scope (active account). Exported for tests. */
|
|
9
|
+
export declare function resolveTokenScope(repoFullName?: string): {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
} | Record<string, never>;
|
|
1
13
|
export declare function registerWebhook(opts: {
|
|
2
14
|
url: string;
|
|
3
15
|
events?: string[];
|
|
@@ -8,11 +20,11 @@ export declare function listWebhooks(opts: {
|
|
|
8
20
|
repo?: string;
|
|
9
21
|
json?: boolean;
|
|
10
22
|
}): Promise<void>;
|
|
11
|
-
export declare function rotateWebhookSecret(id: string): Promise<void>;
|
|
12
|
-
export declare function setWebhookEnabled(id: string, enabled: boolean): Promise<void>;
|
|
23
|
+
export declare function rotateWebhookSecret(id: string, repo?: string): Promise<void>;
|
|
24
|
+
export declare function setWebhookEnabled(id: string, enabled: boolean, repo?: string): Promise<void>;
|
|
13
25
|
export declare function listDeliveries(opts: {
|
|
14
26
|
repo?: string;
|
|
15
27
|
limit?: number;
|
|
16
28
|
json?: boolean;
|
|
17
29
|
}): Promise<void>;
|
|
18
|
-
export declare function replayDelivery(id: string): Promise<void>;
|
|
30
|
+
export declare function replayDelivery(id: string, repo?: string): Promise<void>;
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* to create the registration row and obtain the HMAC secret.
|
|
12
12
|
*
|
|
13
13
|
* Remote queries (`list`, `deliveries`, `rotate-secret`, `replay`, etc.)
|
|
14
|
-
* Hit the worker's /admin endpoints
|
|
15
|
-
* side;
|
|
16
|
-
*
|
|
14
|
+
* Hit the worker's /admin endpoints with the user's Bearer token
|
|
15
|
+
* (introspected server-side; push access to the target repo required).
|
|
16
|
+
* Account resolution is repo-scoped so `haystack login` repoDefaults
|
|
17
|
+
* apply; id-based commands accept --repo when run outside the repo's
|
|
18
|
+
* checkout.
|
|
17
19
|
*
|
|
18
20
|
* Secret handling:
|
|
19
21
|
* The worker returns the freshly-minted HMAC secret ONCE, on register or
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
25
27
|
import { resolve } from 'node:path';
|
|
26
28
|
import chalk from 'chalk';
|
|
27
|
-
import { parseRemoteUrl } from '../utils/git.js';
|
|
29
|
+
import { parseRemoteUrl, UnsupportedRemoteUrlError } from '../utils/git.js';
|
|
28
30
|
import { loadToken } from '../utils/auth.js';
|
|
29
31
|
const DEFAULT_WORKER_URL = process.env.HAYSTACK_WEBHOOKS_URL ??
|
|
30
32
|
'https://haystack-webhooks.akshaysg.workers.dev';
|
|
@@ -57,8 +59,40 @@ function currentRepo() {
|
|
|
57
59
|
const r = parseRemoteUrl('origin');
|
|
58
60
|
return `${r.owner}/${r.repo}`;
|
|
59
61
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
/** currentRepo() for token-account resolution. Outside a git repo there is
|
|
63
|
+
* simply no repo context — expected for id-based commands run from anywhere,
|
|
64
|
+
* so that case stays quiet. A remote that EXISTS but isn't recognized is a
|
|
65
|
+
* different story: repo-scoped account defaults silently won't apply, so say
|
|
66
|
+
* so (classified by error type per PR007). */
|
|
67
|
+
export function tryCurrentRepo() {
|
|
68
|
+
try {
|
|
69
|
+
return currentRepo();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof UnsupportedRemoteUrlError) {
|
|
73
|
+
process.stderr.write(chalk.dim(` [origin remote not recognized (${err.message}); using the active account, not repo defaults]\n`));
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Token-resolution scope for adminFetch: explicit repo wins, else the cwd's
|
|
79
|
+
* origin remote, else no scope (active account). Exported for tests. */
|
|
80
|
+
export function resolveTokenScope(repoFullName) {
|
|
81
|
+
const target = repoFullName ?? tryCurrentRepo();
|
|
82
|
+
if (!target || !target.includes('/'))
|
|
83
|
+
return {};
|
|
84
|
+
const [owner, repo] = target.split('/');
|
|
85
|
+
if (!owner || !repo)
|
|
86
|
+
return {};
|
|
87
|
+
return { owner, repo };
|
|
88
|
+
}
|
|
89
|
+
async function adminFetch(path, init = {}, repoFullName) {
|
|
90
|
+
// Resolve the token FOR THE TARGET REPO so `haystack login`'s repoDefaults
|
|
91
|
+
// apply, matching every other repo-scoped command (triage, submit). A bare
|
|
92
|
+
// loadToken() used the active account regardless of repo — with multiple
|
|
93
|
+
// saved accounts, register/list could 403 with "no push access" even
|
|
94
|
+
// though the repo's default account had admin (found 2026-07-06).
|
|
95
|
+
const token = await loadToken(resolveTokenScope(repoFullName));
|
|
62
96
|
const headers = {
|
|
63
97
|
'Content-Type': 'application/json',
|
|
64
98
|
...(init.headers ?? {}),
|
|
@@ -99,7 +133,7 @@ export async function registerWebhook(opts) {
|
|
|
99
133
|
const res = await adminFetch('/admin/registrations', {
|
|
100
134
|
method: 'POST',
|
|
101
135
|
body: { repo, url: opts.url, events },
|
|
102
|
-
});
|
|
136
|
+
}, repo);
|
|
103
137
|
const data = (await readJsonOrFail(res));
|
|
104
138
|
// Update .haystack.json. Append; do not overwrite an existing entry — the
|
|
105
139
|
// operator may want multiple webhooks pointing at different endpoints.
|
|
@@ -132,7 +166,7 @@ export async function registerWebhook(opts) {
|
|
|
132
166
|
// ─── list ────────────────────────────────────────────────────────────────────
|
|
133
167
|
export async function listWebhooks(opts) {
|
|
134
168
|
const repo = opts.repo ?? currentRepo();
|
|
135
|
-
const res = await adminFetch(`/admin/registrations?repo=${encodeURIComponent(repo)}
|
|
169
|
+
const res = await adminFetch(`/admin/registrations?repo=${encodeURIComponent(repo)}`, {}, repo);
|
|
136
170
|
const data = (await readJsonOrFail(res));
|
|
137
171
|
if (opts.json) {
|
|
138
172
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -154,10 +188,10 @@ export async function listWebhooks(opts) {
|
|
|
154
188
|
}
|
|
155
189
|
}
|
|
156
190
|
// ─── rotate-secret ───────────────────────────────────────────────────────────
|
|
157
|
-
export async function rotateWebhookSecret(id) {
|
|
191
|
+
export async function rotateWebhookSecret(id, repo) {
|
|
158
192
|
const res = await adminFetch(`/admin/registrations/${encodeURIComponent(id)}/rotate-secret`, {
|
|
159
193
|
method: 'POST',
|
|
160
|
-
});
|
|
194
|
+
}, repo);
|
|
161
195
|
const data = (await readJsonOrFail(res));
|
|
162
196
|
console.log(chalk.green('✓ Secret rotated'));
|
|
163
197
|
console.log('');
|
|
@@ -167,11 +201,11 @@ export async function rotateWebhookSecret(id) {
|
|
|
167
201
|
console.log(chalk.red.bold('The previous secret is now invalid. Update your receiver before the next event fires.'));
|
|
168
202
|
}
|
|
169
203
|
// ─── enable / disable ────────────────────────────────────────────────────────
|
|
170
|
-
export async function setWebhookEnabled(id, enabled) {
|
|
204
|
+
export async function setWebhookEnabled(id, enabled, repo) {
|
|
171
205
|
const action = enabled ? 'enable' : 'disable';
|
|
172
206
|
const res = await adminFetch(`/admin/registrations/${encodeURIComponent(id)}/${action}`, {
|
|
173
207
|
method: 'POST',
|
|
174
|
-
});
|
|
208
|
+
}, repo);
|
|
175
209
|
await readJsonOrFail(res);
|
|
176
210
|
console.log(chalk.green(`✓ Webhook ${id} ${action}d`));
|
|
177
211
|
}
|
|
@@ -179,7 +213,7 @@ export async function setWebhookEnabled(id, enabled) {
|
|
|
179
213
|
export async function listDeliveries(opts) {
|
|
180
214
|
const repo = opts.repo ?? currentRepo();
|
|
181
215
|
const limit = opts.limit ?? 20;
|
|
182
|
-
const res = await adminFetch(`/admin/deliveries?repo=${encodeURIComponent(repo)}&limit=${limit}
|
|
216
|
+
const res = await adminFetch(`/admin/deliveries?repo=${encodeURIComponent(repo)}&limit=${limit}`, {}, repo);
|
|
183
217
|
const data = (await readJsonOrFail(res));
|
|
184
218
|
if (opts.json) {
|
|
185
219
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -201,10 +235,10 @@ export async function listDeliveries(opts) {
|
|
|
201
235
|
console.log(` ${chalk.dim('error:')} ${d.last_error}`);
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
|
-
export async function replayDelivery(id) {
|
|
238
|
+
export async function replayDelivery(id, repo) {
|
|
205
239
|
const res = await adminFetch(`/admin/deliveries/${encodeURIComponent(id)}/replay`, {
|
|
206
240
|
method: 'POST',
|
|
207
|
-
});
|
|
241
|
+
}, repo);
|
|
208
242
|
await readJsonOrFail(res);
|
|
209
243
|
console.log(chalk.green(`✓ Replay queued for ${id}`));
|
|
210
244
|
}
|
package/dist/index.js
CHANGED
|
@@ -732,9 +732,10 @@ webhooks
|
|
|
732
732
|
webhooks
|
|
733
733
|
.command('rotate-secret <id>')
|
|
734
734
|
.description('Rotate the HMAC secret for a registration (prints the new secret once)')
|
|
735
|
-
.
|
|
735
|
+
.option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
|
|
736
|
+
.action(async (id, opts) => {
|
|
736
737
|
try {
|
|
737
|
-
await rotateWebhookSecret(id);
|
|
738
|
+
await rotateWebhookSecret(id, opts.repo);
|
|
738
739
|
}
|
|
739
740
|
catch (err) {
|
|
740
741
|
console.error(chalk.red('rotate-secret failed:'), err instanceof Error ? err.message : err);
|
|
@@ -744,9 +745,10 @@ webhooks
|
|
|
744
745
|
webhooks
|
|
745
746
|
.command('disable <id>')
|
|
746
747
|
.description('Disable a registration (stops delivery; existing in-flight retries continue)')
|
|
747
|
-
.
|
|
748
|
+
.option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
|
|
749
|
+
.action(async (id, opts) => {
|
|
748
750
|
try {
|
|
749
|
-
await setWebhookEnabled(id, false);
|
|
751
|
+
await setWebhookEnabled(id, false, opts.repo);
|
|
750
752
|
}
|
|
751
753
|
catch (err) {
|
|
752
754
|
console.error(chalk.red('disable failed:'), err instanceof Error ? err.message : err);
|
|
@@ -756,9 +758,10 @@ webhooks
|
|
|
756
758
|
webhooks
|
|
757
759
|
.command('enable <id>')
|
|
758
760
|
.description('Re-enable a previously disabled registration')
|
|
759
|
-
.
|
|
761
|
+
.option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
|
|
762
|
+
.action(async (id, opts) => {
|
|
760
763
|
try {
|
|
761
|
-
await setWebhookEnabled(id, true);
|
|
764
|
+
await setWebhookEnabled(id, true, opts.repo);
|
|
762
765
|
}
|
|
763
766
|
catch (err) {
|
|
764
767
|
console.error(chalk.red('enable failed:'), err instanceof Error ? err.message : err);
|
|
@@ -783,9 +786,10 @@ webhooks
|
|
|
783
786
|
webhooks
|
|
784
787
|
.command('replay <id>')
|
|
785
788
|
.description('Replay a previous delivery (idempotent via X-Haystack-Delivery)')
|
|
786
|
-
.
|
|
789
|
+
.option('--repo <owner/name>', 'Repo the delivery belongs to (for account resolution outside its checkout)')
|
|
790
|
+
.action(async (id, opts) => {
|
|
787
791
|
try {
|
|
788
|
-
await replayDelivery(id);
|
|
792
|
+
await replayDelivery(id, opts.repo);
|
|
789
793
|
}
|
|
790
794
|
catch (err) {
|
|
791
795
|
console.error(chalk.red('replay failed:'), err instanceof Error ? err.message : err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haystackeditor/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.16",
|
|
4
4
|
"description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"check:schemas": "node scripts/check-schemas.mjs",
|
|
14
14
|
"release": "node scripts/release.mjs",
|
|
15
|
-
"prepublishOnly": "node scripts/no-direct-publish.mjs"
|
|
15
|
+
"prepublishOnly": "node scripts/no-direct-publish.mjs",
|
|
16
|
+
"test": "vitest run"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"haystack",
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
"@types/inquirer": "9.0.9",
|
|
49
50
|
"@types/node": "20.19.30",
|
|
50
51
|
"@types/ws": "8.18.1",
|
|
51
|
-
"typescript": "5.9.3"
|
|
52
|
+
"typescript": "5.9.3",
|
|
53
|
+
"vitest": "4.1.10"
|
|
52
54
|
},
|
|
53
55
|
"files": [
|
|
54
56
|
"dist",
|