@haystackeditor/cli 0.15.10 → 0.15.11
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/mcp.d.ts +1 -0
- package/dist/commands/mcp.js +185 -0
- package/dist/commands/pr.d.ts +10 -0
- package/dist/commands/pr.js +103 -0
- package/dist/commands/rules.d.ts +4 -0
- package/dist/commands/rules.js +98 -0
- package/dist/commands/schema-cmd.d.ts +2 -0
- package/dist/commands/schema-cmd.js +41 -0
- package/dist/commands/setup.js +62 -7
- package/dist/commands/submit.js +13 -2
- package/dist/commands/tokens.d.ts +14 -0
- package/dist/commands/tokens.js +184 -0
- package/dist/commands/triage.js +3 -2
- package/dist/commands/webhooks.d.ts +18 -0
- package/dist/commands/webhooks.js +210 -0
- package/dist/index.js +223 -4
- package/dist/schema.d.ts +21 -0
- package/dist/schema.js +20 -0
- package/dist/utils/analysis-api.d.ts +12 -5
- package/dist/utils/analysis-api.js +155 -82
- package/dist/utils/auth.d.ts +29 -0
- package/dist/utils/auth.js +70 -2
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.js +12 -2
- package/dist/utils/prompter.js +4 -1
- package/package.json +4 -2
- package/schemas/pr.v1.json +34 -0
- package/schemas/setup.v1.json +21 -0
- package/schemas/triage.v1.json +50 -0
package/dist/utils/auth.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path to a long-lived `hsk_live_*` token created by `haystack login --headless`.
|
|
3
|
+
* If present (or HAYSTACK_CLI_TOKEN is set), it takes priority over the
|
|
4
|
+
* regular OAuth credentials. CI environments use this exclusively.
|
|
5
|
+
*
|
|
6
|
+
* Why a separate file (not just an entry in credentials.json):
|
|
7
|
+
* 1. Different lifecycle — the OAuth flow renews + refreshes; hsk_live
|
|
8
|
+
* tokens are mint-once-revoke-never until explicit revocation.
|
|
9
|
+
* 2. Easier to commit accidentally and easier to .gitignore (one file).
|
|
10
|
+
* 3. Different shape — hsk_live tokens carry server-side scope that the
|
|
11
|
+
* OAuth token doesn't, and we don't want to model both in one schema.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HEADLESS_TOKEN_FILE: string;
|
|
14
|
+
interface HeadlessTokenFile {
|
|
15
|
+
token: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
created_at?: string;
|
|
19
|
+
}
|
|
1
20
|
export interface StoredAccount {
|
|
2
21
|
github_token: string;
|
|
3
22
|
created_at: string;
|
|
@@ -46,5 +65,15 @@ export declare function removeAccount(login?: string): Promise<string | null>;
|
|
|
46
65
|
export declare function getActiveLogin(): Promise<string | null>;
|
|
47
66
|
export declare function getRepoDefaultAccount(owner: string, repo: string): Promise<string | null>;
|
|
48
67
|
export declare function setRepoDefaultAccount(owner: string, repo: string, login: string): Promise<void>;
|
|
68
|
+
/** Thrown by {@link resolveAuthContext} when no usable credential is found. */
|
|
69
|
+
export declare class NotLoggedInError extends Error {
|
|
70
|
+
constructor(message?: string);
|
|
71
|
+
}
|
|
72
|
+
/** Read the headless CLI token if one is configured. Env var wins over the
|
|
73
|
+
* on-disk file so CI can override without touching the home directory. */
|
|
74
|
+
export declare function readHeadlessToken(): Promise<HeadlessTokenFile | null>;
|
|
75
|
+
export declare function writeHeadlessToken(record: HeadlessTokenFile): Promise<string>;
|
|
76
|
+
export declare function clearHeadlessToken(): Promise<void>;
|
|
49
77
|
export declare function resolveAuthContext(options?: ResolveAuthOptions): Promise<ResolvedAuthContext>;
|
|
50
78
|
export declare function loadToken(options?: ResolveAuthOptions): Promise<string | null>;
|
|
79
|
+
export {};
|
package/dist/utils/auth.js
CHANGED
|
@@ -4,6 +4,20 @@ import * as path from 'path';
|
|
|
4
4
|
const CONFIG_DIR = path.join(os.homedir(), '.haystack');
|
|
5
5
|
const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
6
6
|
const TOKEN_FILE_DISPLAY = '~/.haystack/credentials.json';
|
|
7
|
+
/**
|
|
8
|
+
* Path to a long-lived `hsk_live_*` token created by `haystack login --headless`.
|
|
9
|
+
* If present (or HAYSTACK_CLI_TOKEN is set), it takes priority over the
|
|
10
|
+
* regular OAuth credentials. CI environments use this exclusively.
|
|
11
|
+
*
|
|
12
|
+
* Why a separate file (not just an entry in credentials.json):
|
|
13
|
+
* 1. Different lifecycle — the OAuth flow renews + refreshes; hsk_live
|
|
14
|
+
* tokens are mint-once-revoke-never until explicit revocation.
|
|
15
|
+
* 2. Easier to commit accidentally and easier to .gitignore (one file).
|
|
16
|
+
* 3. Different shape — hsk_live tokens carry server-side scope that the
|
|
17
|
+
* OAuth token doesn't, and we don't want to model both in one schema.
|
|
18
|
+
*/
|
|
19
|
+
export const HEADLESS_TOKEN_FILE = path.join(CONFIG_DIR, 'cli-token');
|
|
20
|
+
const HEADLESS_TOKEN_FILE_DISPLAY = '~/.haystack/cli-token';
|
|
7
21
|
const GITHUB_LOGIN_PATTERN = /^[A-Za-z\d](?:[A-Za-z\d]|-(?=[A-Za-z\d])){0,38}$/;
|
|
8
22
|
function emptyStore() {
|
|
9
23
|
return {
|
|
@@ -337,7 +351,60 @@ export async function setRepoDefaultAccount(owner, repo, login) {
|
|
|
337
351
|
store.repoDefaults[key] = storedLogin;
|
|
338
352
|
await saveStore(store);
|
|
339
353
|
}
|
|
354
|
+
/** Thrown by {@link resolveAuthContext} when no usable credential is found. */
|
|
355
|
+
export class NotLoggedInError extends Error {
|
|
356
|
+
constructor(message = 'Not logged in. Run `haystack login` first.') {
|
|
357
|
+
super(message);
|
|
358
|
+
this.name = 'NotLoggedInError';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Read the headless CLI token if one is configured. Env var wins over the
|
|
362
|
+
* on-disk file so CI can override without touching the home directory. */
|
|
363
|
+
export async function readHeadlessToken() {
|
|
364
|
+
const envToken = process.env.HAYSTACK_CLI_TOKEN;
|
|
365
|
+
if (envToken && envToken.startsWith('hsk_live_')) {
|
|
366
|
+
return { token: envToken, label: 'env:HAYSTACK_CLI_TOKEN' };
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const raw = await fs.readFile(HEADLESS_TOKEN_FILE, 'utf8');
|
|
370
|
+
const parsed = JSON.parse(raw);
|
|
371
|
+
if (typeof parsed.token === 'string' && parsed.token.startsWith('hsk_live_')) {
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export async function writeHeadlessToken(record) {
|
|
381
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
382
|
+
// 0600 so other users on a shared machine can't read the token. The file
|
|
383
|
+
// contains a long-lived credential — mode matters here in a way that doesn't
|
|
384
|
+
// for credentials.json, which can be re-minted via OAuth at any time.
|
|
385
|
+
await fs.writeFile(HEADLESS_TOKEN_FILE, JSON.stringify(record, null, 2) + '\n', {
|
|
386
|
+
mode: 0o600,
|
|
387
|
+
});
|
|
388
|
+
return HEADLESS_TOKEN_FILE_DISPLAY;
|
|
389
|
+
}
|
|
390
|
+
export async function clearHeadlessToken() {
|
|
391
|
+
try {
|
|
392
|
+
await fs.unlink(HEADLESS_TOKEN_FILE);
|
|
393
|
+
}
|
|
394
|
+
catch { /* not present is fine */ }
|
|
395
|
+
}
|
|
340
396
|
export async function resolveAuthContext(options = {}) {
|
|
397
|
+
// hsk_live tokens win unconditionally. They were created explicitly by
|
|
398
|
+
// the operator with `haystack login --headless` (or set via env in CI)
|
|
399
|
+
// — that intent should not be overridden by a stale OAuth cookie.
|
|
400
|
+
const headless = await readHeadlessToken();
|
|
401
|
+
if (headless) {
|
|
402
|
+
return {
|
|
403
|
+
login: headless.label ?? 'headless',
|
|
404
|
+
token: headless.token,
|
|
405
|
+
source: 'active',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
341
408
|
const store = await loadCredentialsStore();
|
|
342
409
|
const { preferredLogin, owner, repo } = options;
|
|
343
410
|
if (preferredLogin) {
|
|
@@ -370,7 +437,7 @@ export async function resolveAuthContext(options = {}) {
|
|
|
370
437
|
source: 'active',
|
|
371
438
|
};
|
|
372
439
|
}
|
|
373
|
-
throw new
|
|
440
|
+
throw new NotLoggedInError();
|
|
374
441
|
}
|
|
375
442
|
export async function loadToken(options = {}) {
|
|
376
443
|
try {
|
|
@@ -378,7 +445,8 @@ export async function loadToken(options = {}) {
|
|
|
378
445
|
return context.token;
|
|
379
446
|
}
|
|
380
447
|
catch (error) {
|
|
381
|
-
|
|
448
|
+
// Classify by type, not message text (see pr-rules.yml PR007).
|
|
449
|
+
if (error instanceof NotLoggedInError) {
|
|
382
450
|
return null;
|
|
383
451
|
}
|
|
384
452
|
throw error;
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -68,6 +68,10 @@ export interface PushAuth {
|
|
|
68
68
|
* Without `auth`, falls back to `git push -u <remote>` using ambient creds.
|
|
69
69
|
*/
|
|
70
70
|
export declare function pushBranch(remoteName: string, branchName: string, auth?: PushAuth): void;
|
|
71
|
+
/** Thrown when a git remote URL is not a recognized GitHub SSH/HTTPS URL. */
|
|
72
|
+
export declare class UnsupportedRemoteUrlError extends Error {
|
|
73
|
+
constructor(url: string);
|
|
74
|
+
}
|
|
71
75
|
/**
|
|
72
76
|
* Parse remote URL to extract owner and repo
|
|
73
77
|
* Supports both SSH and HTTPS URLs
|
package/dist/utils/git.js
CHANGED
|
@@ -257,6 +257,13 @@ function translatePushError(err, repoSlug) {
|
|
|
257
257
|
// ============================================================================
|
|
258
258
|
// Remote operations
|
|
259
259
|
// ============================================================================
|
|
260
|
+
/** Thrown when a git remote URL is not a recognized GitHub SSH/HTTPS URL. */
|
|
261
|
+
export class UnsupportedRemoteUrlError extends Error {
|
|
262
|
+
constructor(url) {
|
|
263
|
+
super(`Unsupported remote URL format: ${url}`);
|
|
264
|
+
this.name = 'UnsupportedRemoteUrlError';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
260
267
|
/**
|
|
261
268
|
* Parse remote URL to extract owner and repo
|
|
262
269
|
* Supports both SSH and HTTPS URLs
|
|
@@ -285,10 +292,13 @@ export function parseRemoteUrl(remoteName = 'origin') {
|
|
|
285
292
|
remoteName,
|
|
286
293
|
};
|
|
287
294
|
}
|
|
288
|
-
throw new
|
|
295
|
+
throw new UnsupportedRemoteUrlError(url);
|
|
289
296
|
}
|
|
290
297
|
catch (err) {
|
|
291
|
-
|
|
298
|
+
// Propagate the "unrecognized URL" case as-is; wrap everything else (the
|
|
299
|
+
// `git remote get-url` command itself failing) in a friendlier message.
|
|
300
|
+
// Classify by type, not message text (see pr-rules.yml PR007).
|
|
301
|
+
if (err instanceof UnsupportedRemoteUrlError) {
|
|
292
302
|
throw err;
|
|
293
303
|
}
|
|
294
304
|
throw new Error(`Failed to get remote URL for '${remoteName}'`);
|
package/dist/utils/prompter.js
CHANGED
|
@@ -139,7 +139,10 @@ class JsonPrompter {
|
|
|
139
139
|
return id in this.answers;
|
|
140
140
|
}
|
|
141
141
|
emit(event) {
|
|
142
|
-
|
|
142
|
+
// Stamp every NDJSON event with the setup schema version so agent
|
|
143
|
+
// consumers can pin a contract. See docs/SPEC-CLI-FIRST-CLASS.md.
|
|
144
|
+
const stamped = { schema_version: '1.0.0', ...event };
|
|
145
|
+
process.stdout.write(JSON.stringify(stamped) + '\n');
|
|
143
146
|
}
|
|
144
147
|
/** Lazily open stdin only when we actually need an answer the agent must send. */
|
|
145
148
|
ensureStdin() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haystackeditor/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.11",
|
|
4
4
|
"description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"build": "tsc && rm -rf dist/assets && cp -r src/assets dist/assets",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
|
+
"check:schemas": "node scripts/check-schemas.mjs",
|
|
13
14
|
"prepublishOnly": "npm run build"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
"typescript": "5.9.3"
|
|
47
48
|
},
|
|
48
49
|
"files": [
|
|
49
|
-
"dist"
|
|
50
|
+
"dist",
|
|
51
|
+
"schemas"
|
|
50
52
|
],
|
|
51
53
|
"engines": {
|
|
52
54
|
"node": ">=18"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/pr.v1.json",
|
|
4
|
+
"title": "haystack pr <ref> --json",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "owner", "repo", "prNumber", "bucket", "state"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": { "type": "string", "const": "1.0.0" },
|
|
9
|
+
"owner": { "type": "string" },
|
|
10
|
+
"repo": { "type": "string" },
|
|
11
|
+
"prNumber": { "type": "integer" },
|
|
12
|
+
"bucket": { "type": "string" },
|
|
13
|
+
"state": { "type": "string", "enum": ["open", "closed", "merged"] },
|
|
14
|
+
"title": { "type": "string" },
|
|
15
|
+
"headSha": { "type": "string" },
|
|
16
|
+
"haystackUrl": { "type": "string" },
|
|
17
|
+
"mergeQueue": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"state": { "type": "string", "enum": ["not-enrolled", "eligible", "grace", "queued", "merging", "merged", "blocked"] },
|
|
21
|
+
"graceEndsAt": { "type": ["string", "null"] },
|
|
22
|
+
"blockedReason": { "type": ["string", "null"] }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"lastTriage": {
|
|
26
|
+
"type": ["object", "null"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"rating": { "type": "integer" },
|
|
29
|
+
"completedAt": { "type": "string" },
|
|
30
|
+
"headSha": { "type": "string" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/setup.v1.json",
|
|
4
|
+
"title": "haystack setup --json event stream",
|
|
5
|
+
"description": "NDJSON event stream. Each line is one event stamped with schema_version.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema_version", "type"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "type": "string", "const": "1.0.0" },
|
|
10
|
+
"type": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["question", "permission", "log", "progress", "result"]
|
|
13
|
+
},
|
|
14
|
+
"id": { "type": "string" },
|
|
15
|
+
"prompt": { "type": "string" },
|
|
16
|
+
"choices": { "type": "array" },
|
|
17
|
+
"level": { "type": "string", "enum": ["info", "warn", "error"] },
|
|
18
|
+
"message": { "type": "string" },
|
|
19
|
+
"result": { "type": "object" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/triage.v1.json",
|
|
4
|
+
"title": "haystack triage --json",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "owner", "repo", "prNumber", "rating", "findings"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": { "type": "string", "const": "1.0.0" },
|
|
9
|
+
"owner": { "type": "string" },
|
|
10
|
+
"repo": { "type": "string" },
|
|
11
|
+
"prNumber": { "type": "integer" },
|
|
12
|
+
"rating": { "type": "integer", "minimum": 1, "maximum": 5 },
|
|
13
|
+
"autoFixerHandling": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"category": { "type": "string" },
|
|
19
|
+
"summary": { "type": "string" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"autoFixerSkipped": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"category": { "type": "string" },
|
|
29
|
+
"summary": { "type": "string" },
|
|
30
|
+
"reason": { "type": "string" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"findings": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["category", "summary"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"category": { "type": "string" },
|
|
41
|
+
"summary": { "type": "string" },
|
|
42
|
+
"detail": { "type": "string" },
|
|
43
|
+
"agentFixPrompt": { "type": ["string", "null"] },
|
|
44
|
+
"source": { "type": ["string", "null"] },
|
|
45
|
+
"autoFixing": { "type": "boolean" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|