@haystackeditor/cli 0.8.0 → 0.9.0
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 +105 -12
- package/dist/assets/hooks/agent-context/detect.ts +136 -0
- package/dist/assets/hooks/agent-context/format.ts +99 -0
- package/dist/assets/hooks/agent-context/index.ts +39 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
- package/dist/assets/hooks/agent-context/types.ts +58 -0
- package/dist/assets/hooks/llm-rules-template.md +56 -0
- package/dist/assets/hooks/package.json +11 -0
- package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
- package/dist/assets/hooks/scripts/post-commit.sh +4 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
- package/dist/assets/hooks/scripts/pre-push.sh +25 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
- package/dist/assets/hooks/truncation-checker/index.ts +595 -0
- package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
- package/dist/assets/skills/prepare-haystack.md +323 -0
- package/dist/assets/skills/secrets.md +164 -0
- package/dist/assets/skills/setup-external-sandbox.md +243 -0
- package/dist/assets/skills/setup-haystack.md +639 -0
- package/dist/assets/skills/submit.md +154 -0
- package/dist/assets/templates/CLAUDE.md.snippet +42 -0
- package/dist/assets/templates/haystack.yml +193 -0
- package/dist/commands/check-pending.d.ts +19 -0
- package/dist/commands/check-pending.js +217 -0
- package/dist/commands/config.d.ts +18 -12
- package/dist/commands/config.js +327 -52
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- package/dist/commands/install-session-hooks.d.ts +16 -0
- package/dist/commands/install-session-hooks.js +302 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +365 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +80 -0
- package/dist/commands/submit.d.ts +22 -0
- package/dist/commands/submit.js +428 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +317 -2
- package/dist/tools/detect.d.ts +50 -0
- package/dist/tools/detect.js +853 -0
- package/dist/tools/fixtures.d.ts +38 -0
- package/dist/tools/fixtures.js +199 -0
- package/dist/tools/setup.d.ts +43 -0
- package/dist/tools/setup.js +597 -0
- package/dist/triage/prompts.d.ts +31 -0
- package/dist/triage/prompts.js +266 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +325 -0
- package/dist/triage/traces.d.ts +20 -0
- package/dist/triage/traces.js +305 -0
- package/dist/triage/types.d.ts +47 -0
- package/dist/triage/types.js +7 -0
- package/dist/types.d.ts +1387 -191
- package/dist/types.js +254 -2
- package/dist/utils/analysis-api.d.ts +108 -0
- package/dist/utils/analysis-api.js +194 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/git.d.ts +80 -0
- package/dist/utils/git.js +302 -0
- package/dist/utils/github-api.d.ts +83 -0
- package/dist/utils/github-api.js +266 -0
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -0
- package/dist/utils/pending-state.d.ts +38 -0
- package/dist/utils/pending-state.js +86 -0
- package/dist/utils/secrets.js +3 -3
- package/dist/utils/skill.d.ts +1 -1
- package/dist/utils/skill.js +658 -1
- package/package.json +5 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture definition for loading.
|
|
3
|
+
* This is constructed from HaystackConfig's verification.fixtures Record.
|
|
4
|
+
*/
|
|
5
|
+
export interface Fixture {
|
|
6
|
+
/** URL pattern to intercept (e.g., "/api/users/*") */
|
|
7
|
+
pattern: string;
|
|
8
|
+
/** Source to load data from (file://, https://, s3://, r2://, script://, or passthrough) */
|
|
9
|
+
source: string;
|
|
10
|
+
/** Optional headers for remote sources */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
/** Optional cookies for remote sources (for login-required endpoints) */
|
|
13
|
+
cookies?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FixtureData {
|
|
16
|
+
pattern: string;
|
|
17
|
+
data: unknown;
|
|
18
|
+
source: string;
|
|
19
|
+
}
|
|
20
|
+
export interface FixtureLoaderOptions {
|
|
21
|
+
/** Base directory for relative file paths */
|
|
22
|
+
baseDir: string;
|
|
23
|
+
/** Environment variables for token substitution */
|
|
24
|
+
env: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load fixture data from various sources
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadFixture(fixture: Fixture, options: FixtureLoaderOptions): Promise<FixtureData>;
|
|
30
|
+
/**
|
|
31
|
+
* Load all fixtures from config
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadAllFixtures(fixtures: Fixture[], options: FixtureLoaderOptions): Promise<FixtureData[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a mock fixture based on a pattern
|
|
36
|
+
* Useful for scaffolding fixture files
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateMockFixture(pattern: string): unknown;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Load fixture data from various sources
|
|
5
|
+
*/
|
|
6
|
+
export async function loadFixture(fixture, options) {
|
|
7
|
+
const { source, pattern, headers, cookies } = fixture;
|
|
8
|
+
// Substitute env vars in source, headers, and cookies
|
|
9
|
+
const resolvedSource = substituteEnvVars(source, options.env);
|
|
10
|
+
const resolvedHeaders = headers
|
|
11
|
+
? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, substituteEnvVars(v, options.env)]))
|
|
12
|
+
: undefined;
|
|
13
|
+
const resolvedCookies = cookies ? substituteEnvVars(cookies, options.env) : undefined;
|
|
14
|
+
let data;
|
|
15
|
+
if (source === 'passthrough') {
|
|
16
|
+
// Passthrough means don't intercept - return marker
|
|
17
|
+
return {
|
|
18
|
+
pattern,
|
|
19
|
+
data: { __passthrough: true },
|
|
20
|
+
source: 'passthrough',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (source.startsWith('file://')) {
|
|
24
|
+
data = await loadFromFile(resolvedSource.slice(7), options.baseDir);
|
|
25
|
+
}
|
|
26
|
+
else if (source.startsWith('https://') || source.startsWith('http://')) {
|
|
27
|
+
data = await loadFromUrl(resolvedSource, resolvedHeaders, resolvedCookies);
|
|
28
|
+
}
|
|
29
|
+
else if (source.startsWith('s3://')) {
|
|
30
|
+
data = await loadFromS3(resolvedSource.slice(5));
|
|
31
|
+
}
|
|
32
|
+
else if (source.startsWith('r2://')) {
|
|
33
|
+
data = await loadFromR2(resolvedSource.slice(5));
|
|
34
|
+
}
|
|
35
|
+
else if (source.startsWith('script://')) {
|
|
36
|
+
data = await loadFromScript(resolvedSource.slice(9), options.baseDir);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`Unknown fixture source type: ${source}`);
|
|
40
|
+
}
|
|
41
|
+
return { pattern, data, source: resolvedSource };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load all fixtures from config
|
|
45
|
+
*/
|
|
46
|
+
export async function loadAllFixtures(fixtures, options) {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const fixture of fixtures) {
|
|
49
|
+
try {
|
|
50
|
+
const data = await loadFixture(fixture, options);
|
|
51
|
+
results.push(data);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(`Failed to load fixture for pattern ${fixture.pattern}:`, error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
function substituteEnvVars(str, env) {
|
|
61
|
+
return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
|
|
62
|
+
return env[varName] || process.env[varName] || '';
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function loadFromFile(filePath, baseDir) {
|
|
66
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(baseDir, filePath);
|
|
67
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
68
|
+
return JSON.parse(content);
|
|
69
|
+
}
|
|
70
|
+
async function loadFromUrl(url, headers, cookies) {
|
|
71
|
+
// Build headers, including Cookie header if provided
|
|
72
|
+
const requestHeaders = { ...headers };
|
|
73
|
+
if (cookies) {
|
|
74
|
+
requestHeaders['Cookie'] = cookies;
|
|
75
|
+
}
|
|
76
|
+
const response = await fetch(url, {
|
|
77
|
+
headers: requestHeaders,
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
return response.json();
|
|
83
|
+
}
|
|
84
|
+
async function loadFromS3(s3Path) {
|
|
85
|
+
// s3Path format: bucket/key
|
|
86
|
+
const [bucket, ...keyParts] = s3Path.split('/');
|
|
87
|
+
const key = keyParts.join('/');
|
|
88
|
+
// Use AWS SDK if available, otherwise fall back to presigned URL approach
|
|
89
|
+
try {
|
|
90
|
+
// Dynamic import to make SDK optional
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
92
|
+
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
93
|
+
const client = new S3Client({});
|
|
94
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
95
|
+
const response = await client.send(command);
|
|
96
|
+
const body = await response.Body?.transformToString();
|
|
97
|
+
if (!body)
|
|
98
|
+
throw new Error('Empty response from S3');
|
|
99
|
+
return JSON.parse(body);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// If SDK not available, provide helpful error
|
|
103
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
104
|
+
throw new Error('S3 fixtures require @aws-sdk/client-s3. Install it with: pnpm add @aws-sdk/client-s3');
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function loadFromR2(r2Path) {
|
|
110
|
+
// r2Path format: bucket/key
|
|
111
|
+
// R2 uses S3-compatible API, so we can use the same SDK
|
|
112
|
+
const [bucket, ...keyParts] = r2Path.split('/');
|
|
113
|
+
const key = keyParts.join('/');
|
|
114
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
|
|
115
|
+
if (!accountId) {
|
|
116
|
+
throw new Error('R2 fixtures require CLOUDFLARE_ACCOUNT_ID or CF_ACCOUNT_ID env var');
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
// Dynamic import to make SDK optional
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
121
|
+
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
122
|
+
const client = new S3Client({
|
|
123
|
+
region: 'auto',
|
|
124
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
125
|
+
credentials: {
|
|
126
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
|
|
127
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
131
|
+
const response = await client.send(command);
|
|
132
|
+
const body = await response.Body?.transformToString();
|
|
133
|
+
if (!body)
|
|
134
|
+
throw new Error('Empty response from R2');
|
|
135
|
+
return JSON.parse(body);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
139
|
+
throw new Error('R2 fixtures require @aws-sdk/client-s3. Install it with: pnpm add @aws-sdk/client-s3');
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function loadFromScript(scriptPath, baseDir) {
|
|
145
|
+
const fullPath = path.isAbsolute(scriptPath)
|
|
146
|
+
? scriptPath
|
|
147
|
+
: path.join(baseDir, scriptPath);
|
|
148
|
+
// Dynamic import the script - it should export a default function or 'generate' function
|
|
149
|
+
const module = await import(fullPath);
|
|
150
|
+
const generator = module.default || module.generate;
|
|
151
|
+
if (typeof generator !== 'function') {
|
|
152
|
+
throw new Error(`Script ${scriptPath} must export a default function or 'generate' function`);
|
|
153
|
+
}
|
|
154
|
+
return generator();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generate a mock fixture based on a pattern
|
|
158
|
+
* Useful for scaffolding fixture files
|
|
159
|
+
*/
|
|
160
|
+
export function generateMockFixture(pattern) {
|
|
161
|
+
// Generate sensible mock data based on the pattern
|
|
162
|
+
if (pattern.includes('user')) {
|
|
163
|
+
return {
|
|
164
|
+
id: 1,
|
|
165
|
+
name: 'Test User',
|
|
166
|
+
email: 'test@example.com',
|
|
167
|
+
avatar: 'https://placekitten.com/100/100',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (pattern.includes('dashboard') || pattern.includes('analytics')) {
|
|
171
|
+
return {
|
|
172
|
+
totalUsers: 1234,
|
|
173
|
+
activeUsers: 567,
|
|
174
|
+
revenue: 12345.67,
|
|
175
|
+
chartData: [
|
|
176
|
+
{ date: '2024-01-01', value: 100 },
|
|
177
|
+
{ date: '2024-01-02', value: 120 },
|
|
178
|
+
{ date: '2024-01-03', value: 115 },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (pattern.includes('config') || pattern.includes('settings')) {
|
|
183
|
+
return {
|
|
184
|
+
theme: 'light',
|
|
185
|
+
language: 'en',
|
|
186
|
+
notifications: true,
|
|
187
|
+
features: {
|
|
188
|
+
darkMode: true,
|
|
189
|
+
betaFeatures: false,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// Default generic response
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
data: [],
|
|
197
|
+
message: `Mock data for ${pattern}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HaystackConfig } from '../types.js';
|
|
2
|
+
import { ProjectDetection, DetectedService } from './detect.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interactive setup flow for .haystack.json
|
|
5
|
+
*
|
|
6
|
+
* The MCP tool will return questions one at a time.
|
|
7
|
+
* The agent asks the user, collects answers, and calls back
|
|
8
|
+
* until the config is complete.
|
|
9
|
+
*/
|
|
10
|
+
export interface SetupQuestion {
|
|
11
|
+
id: string;
|
|
12
|
+
question: string;
|
|
13
|
+
type: 'text' | 'select' | 'multiselect' | 'confirm';
|
|
14
|
+
options?: string[];
|
|
15
|
+
default?: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SetupState {
|
|
19
|
+
step: 'project_name' | 'is_monorepo' | 'services_list' | 'service_details' | 'dev_command' | 'dev_port' | 'dev_ready_pattern' | 'verification_commands' | 'auth_needs_signin' | 'auth_strategy' | 'auth_env' | 'auth_token_type' | 'auth_token_name' | 'auth_token_secret' | 'db_needs_database' | 'db_strategy' | 'db_env' | 'db_seed_command' | 'complete';
|
|
20
|
+
answers: Record<string, unknown>;
|
|
21
|
+
config: Partial<HaystackConfig>;
|
|
22
|
+
/** For monorepo setup: which service we're configuring */
|
|
23
|
+
currentServiceIndex?: number;
|
|
24
|
+
/** For monorepo setup: list of services to configure */
|
|
25
|
+
serviceNames?: string[];
|
|
26
|
+
/** Auto-detected project info */
|
|
27
|
+
detection?: ProjectDetection;
|
|
28
|
+
/** Auto-detected services (for smart defaults) */
|
|
29
|
+
detectedServices?: DetectedService[];
|
|
30
|
+
}
|
|
31
|
+
export declare function createInitialState(rootDir?: string): SetupState;
|
|
32
|
+
/**
|
|
33
|
+
* Get a default value based on auto-detection.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getDetectedDefault(state: SetupState, questionId: string): string | undefined;
|
|
36
|
+
export declare function getNextQuestion(state: SetupState): SetupQuestion | null;
|
|
37
|
+
export declare function advanceStep(state: SetupState): SetupState;
|
|
38
|
+
/**
|
|
39
|
+
* Build the final .haystack.json config from collected answers.
|
|
40
|
+
* This generates the schema that sandbox.py expects.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildConfig(state: SetupState): HaystackConfig;
|
|
43
|
+
export declare function configToJson(config: HaystackConfig): string;
|