@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.
@@ -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. Auth is CF Access on the worker
15
- * side; the CLI relies on the operator having a CF Access session
16
- * (cookie or service-token).
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
- async function adminFetch(path, init = {}) {
61
- const token = await loadToken();
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
- .action(async (id) => {
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
- .action(async (id) => {
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
- .action(async (id) => {
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
- .action(async (id) => {
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.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",