@haystackeditor/cli 0.2.0 → 0.3.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 CHANGED
@@ -1,105 +1,81 @@
1
1
  # @haystackeditor/cli
2
2
 
3
- Unified CLI for Haystack verification, fixtures, and sandboxes.
3
+ Set up Haystack verification for your project. When PRs are opened, an AI agent spins up your app in a sandbox and verifies changes work correctly.
4
4
 
5
- ## Installation
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
- # Global install
9
- npm install -g @haystackeditor/cli
10
-
11
- # Or run directly with npx
12
8
  npx @haystackeditor/cli init
13
9
  ```
14
10
 
11
+ This auto-detects your framework, package manager, and ports, then creates:
12
+ - `.haystack.yml` - Configuration for the verification agent
13
+ - `.agents/skills/haystack.md` - Skill file for AI agent discovery
14
+
15
15
  ## Commands
16
16
 
17
17
  ### `haystack init`
18
18
 
19
- Interactive setup wizard to create `.haystack.yml` configuration:
19
+ Interactive setup wizard:
20
20
 
21
21
  ```bash
22
- haystack init # Interactive wizard
23
- haystack init -y # Accept all defaults
22
+ npx @haystackeditor/cli init # Interactive wizard
23
+ npx @haystackeditor/cli init -y # Accept all defaults
24
24
  ```
25
25
 
26
- ### `haystack login`
26
+ ### `haystack status`
27
27
 
28
- Authenticate with Haystack via GitHub OAuth:
28
+ Check if your project is configured:
29
29
 
30
30
  ```bash
31
- haystack login # Opens browser for GitHub OAuth
32
- haystack login --token # Use existing GitHub token
33
- haystack logout # Clear stored credentials
31
+ npx @haystackeditor/cli status
34
32
  ```
35
33
 
36
- ### `haystack secrets`
34
+ ### `haystack login`
37
35
 
38
- Manage secrets for fixtures and integrations:
36
+ Authenticate with GitHub (required for secrets management):
39
37
 
40
38
  ```bash
41
- haystack secrets list # List all secrets
42
- haystack secrets set KEY value # Set a secret
43
- haystack secrets delete KEY # Delete a secret
39
+ npx @haystackeditor/cli login
44
40
  ```
45
41
 
46
- Secrets are stored securely in the Haystack platform and referenced in `.haystack.yml` using `$VARIABLE` syntax.
47
-
48
- ### `haystack record`
49
-
50
- Record API responses as fixtures:
42
+ This uses GitHub's device flow - you'll get a code to enter at github.com/login/device.
51
43
 
52
44
  ```bash
53
- haystack record https://api.example.com/data
54
- haystack record https://api.example.com/data --output fixtures/data.json
45
+ # Log out (removes stored credentials)
46
+ npx @haystackeditor/cli logout
55
47
  ```
56
48
 
57
- ### `haystack verify`
49
+ ### `haystack secrets`
58
50
 
59
- Run verification commands from `.haystack.yml`:
51
+ Manage secrets that will be injected into your sandbox environment:
60
52
 
61
53
  ```bash
62
- haystack verify # Run all verification commands
63
- haystack verify -c build # Run specific command
64
- haystack verify --dry-run # Show commands without running
65
- ```
54
+ # List all secrets (keys only, values are never shown)
55
+ npx @haystackeditor/cli secrets list
66
56
 
67
- ### `haystack dev`
57
+ # Set a secret
58
+ npx @haystackeditor/cli secrets set OPENAI_API_KEY sk-xxx
68
59
 
69
- Start dev server with fixtures enabled:
70
-
71
- ```bash
72
- haystack dev # Start dev server
73
- haystack dev -s frontend # Start specific service (monorepo)
74
- haystack dev --no-fixtures # Start without fixture loading
60
+ # Delete a secret
61
+ npx @haystackeditor/cli secrets delete OPENAI_API_KEY
75
62
  ```
76
63
 
77
- ### `haystack sandbox`
64
+ Secrets are encrypted and stored securely. They're automatically injected as environment variables when the sandbox runs your app.
78
65
 
79
- Manage Haystack sandboxes:
66
+ **Scopes**: By default, secrets are user-scoped. You can also scope to an org or repo:
80
67
 
81
68
  ```bash
82
- haystack sandbox create # Create sandbox for current branch
83
- haystack sandbox status # Check sandbox status
84
- haystack sandbox open # Open sandbox in browser
85
- haystack sandbox logs # Stream sandbox logs
86
- haystack sandbox destroy # Destroy sandbox
87
- ```
88
-
89
- ### `haystack mcp`
69
+ # Org-scoped (available to all repos in the org)
70
+ npx @haystackeditor/cli secrets set API_KEY xxx --scope org --scope-id myorg
90
71
 
91
- Run as MCP server for Claude Code integration:
92
-
93
- ```bash
94
- # Add to Claude Code
95
- claude mcp add haystack -- npx @haystackeditor/cli mcp
72
+ # Repo-scoped (available only to this repo)
73
+ npx @haystackeditor/cli secrets set API_KEY xxx --scope repo --scope-id owner/repo
96
74
  ```
97
75
 
98
76
  ## Configuration
99
77
 
100
- Create `.haystack.yml` in your project root:
101
-
102
- ### Simple Project
78
+ The `init` command creates `.haystack.yml`:
103
79
 
104
80
  ```yaml
105
81
  version: "1"
@@ -118,84 +94,27 @@ verification:
118
94
  run: pnpm build
119
95
  - name: lint
120
96
  run: pnpm lint
121
- - name: typecheck
122
- run: pnpm tsc --noEmit
123
- ```
124
-
125
- ### Monorepo
126
-
127
- ```yaml
128
- version: "1"
129
- name: my-monorepo
130
-
131
- services:
132
- frontend:
133
- command: pnpm dev
134
- port: 3000
135
- ready_pattern: "Local:"
136
- env:
137
- SKIP_AUTH: "true"
138
-
139
- api:
140
- root: packages/api
141
- command: pnpm dev
142
- port: 8080
143
- ready_pattern: "listening"
144
-
145
- worker:
146
- root: infra/worker
147
- command: pnpm dev
148
- ready_pattern: "Ready"
149
-
150
- analysis:
151
- root: packages/analysis
152
- type: batch
153
- command: pnpm start
154
-
155
- verification:
156
- commands:
157
- - name: build
158
- run: pnpm build
159
- - name: test
160
- run: pnpm test
161
-
162
- fixtures:
163
- "*/api/github/*":
164
- source: file://fixtures/github-api.json
165
-
166
- "*/api/analysis/*":
167
- source: pr://haystackeditor/example-repo/42
168
97
  ```
169
98
 
170
- ## Fixtures
171
-
172
- Fixtures mock external API responses during development and testing. Sources:
173
-
174
- | Prefix | Description |
175
- |--------|-------------|
176
- | `file://` | Local JSON file |
177
- | `https://` | Remote URL (cached) |
178
- | `s3://` | AWS S3 bucket |
179
- | `r2://` | Cloudflare R2 bucket |
180
- | `pr://owner/repo/number` | Haystack PR analysis data |
181
- | `recorded://id` | Previously recorded fixture |
182
- | `passthrough` | Don't intercept, let request through |
99
+ ### Customizing After Init
183
100
 
184
- ### Using Secrets in Fixtures
101
+ | If your app has... | Add this |
102
+ |-------------------|----------|
103
+ | Login/authentication | Auth bypass env var in `dev_server.env` |
104
+ | Key user journeys | Flows describing what to verify |
105
+ | API calls needing auth | Fixtures to mock responses |
185
106
 
186
- ```yaml
187
- fixtures:
188
- "*/api/private/*":
189
- source: s3://my-bucket/fixtures/data.json
190
- headers:
191
- Authorization: Bearer $AWS_TOKEN
192
- ```
107
+ See the generated `.agents/skills/haystack.md` for full documentation on flows, fixtures, and monorepo configuration.
193
108
 
194
- Set the secret:
109
+ ## How It Works
195
110
 
196
- ```bash
197
- haystack secrets set AWS_TOKEN "your-token-here"
198
- ```
111
+ 1. You run `npx @haystackeditor/cli init` and commit the config
112
+ 2. When a PR is opened, Haystack's AI agent:
113
+ - Spins up your app in a Modal sandbox
114
+ - Reads the flows to understand what to verify
115
+ - Navigates the app autonomously
116
+ - Captures screenshots and evidence
117
+ - Reports results on the PR
199
118
 
200
119
  ## License
201
120
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Login command - GitHub OAuth device flow
3
+ */
4
+ /**
5
+ * Load token from disk
6
+ */
7
+ export declare function loadToken(): Promise<string | null>;
8
+ export declare function loginCommand(): Promise<void>;
9
+ export declare function logoutCommand(): Promise<void>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Login command - GitHub OAuth device flow
3
+ */
4
+ import chalk from 'chalk';
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ const GITHUB_CLIENT_ID = 'Ov23liW3JE38D3gZWa85'; // Haystack GitHub OAuth App
9
+ const CONFIG_DIR = path.join(os.homedir(), '.haystack');
10
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
11
+ /**
12
+ * Start device flow and get user code
13
+ */
14
+ async function startDeviceFlow() {
15
+ const response = await fetch('https://github.com/login/device/code', {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Accept': 'application/json',
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ body: JSON.stringify({
22
+ client_id: GITHUB_CLIENT_ID,
23
+ scope: 'read:user read:org repo',
24
+ }),
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Failed to start device flow: ${response.status}`);
28
+ }
29
+ return response.json();
30
+ }
31
+ /**
32
+ * Poll for access token
33
+ */
34
+ async function pollForToken(deviceCode, interval) {
35
+ while (true) {
36
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
37
+ const response = await fetch('https://github.com/login/oauth/access_token', {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Accept': 'application/json',
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({
44
+ client_id: GITHUB_CLIENT_ID,
45
+ device_code: deviceCode,
46
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
47
+ }),
48
+ });
49
+ const data = await response.json();
50
+ if (data.access_token) {
51
+ return data.access_token;
52
+ }
53
+ if (data.error === 'authorization_pending') {
54
+ // Still waiting for user
55
+ continue;
56
+ }
57
+ if (data.error === 'slow_down') {
58
+ // Increase interval
59
+ interval += 5;
60
+ continue;
61
+ }
62
+ if (data.error === 'expired_token') {
63
+ throw new Error('Authorization timed out. Please try again.');
64
+ }
65
+ if (data.error === 'access_denied') {
66
+ throw new Error('Authorization denied by user.');
67
+ }
68
+ throw new Error(`OAuth error: ${data.error}`);
69
+ }
70
+ }
71
+ /**
72
+ * Save token to disk
73
+ */
74
+ async function saveToken(token) {
75
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
76
+ const credentials = {
77
+ github_token: token,
78
+ created_at: new Date().toISOString(),
79
+ };
80
+ await fs.writeFile(TOKEN_FILE, JSON.stringify(credentials, null, 2), {
81
+ mode: 0o600, // Owner read/write only
82
+ });
83
+ }
84
+ /**
85
+ * Load token from disk
86
+ */
87
+ export async function loadToken() {
88
+ try {
89
+ const content = await fs.readFile(TOKEN_FILE, 'utf-8');
90
+ const credentials = JSON.parse(content);
91
+ return credentials.github_token;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * Verify token is valid by calling GitHub API
99
+ */
100
+ async function verifyToken(token) {
101
+ try {
102
+ const response = await fetch('https://api.github.com/user', {
103
+ headers: {
104
+ 'Authorization': `Bearer ${token}`,
105
+ 'User-Agent': 'Haystack-CLI',
106
+ },
107
+ });
108
+ if (!response.ok) {
109
+ return null;
110
+ }
111
+ return response.json();
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ export async function loginCommand() {
118
+ console.log(chalk.bold('\nHaystack Login\n'));
119
+ // Check if already logged in
120
+ const existingToken = await loadToken();
121
+ if (existingToken) {
122
+ const user = await verifyToken(existingToken);
123
+ if (user) {
124
+ console.log(chalk.green(`Already logged in as ${chalk.bold(user.login)}`));
125
+ console.log(chalk.dim('Run `haystack logout` to sign out.\n'));
126
+ return;
127
+ }
128
+ }
129
+ console.log('Authenticating with GitHub...\n');
130
+ try {
131
+ // Start device flow
132
+ const deviceFlow = await startDeviceFlow();
133
+ console.log(chalk.yellow('Open this URL in your browser:\n'));
134
+ console.log(` ${chalk.bold(deviceFlow.verification_uri)}\n`);
135
+ console.log(chalk.yellow('And enter this code:\n'));
136
+ console.log(` ${chalk.bold.cyan(deviceFlow.user_code)}\n`);
137
+ console.log(chalk.dim('Waiting for authorization...'));
138
+ // Poll for token
139
+ const token = await pollForToken(deviceFlow.device_code, deviceFlow.interval);
140
+ // Verify and save
141
+ const user = await verifyToken(token);
142
+ if (!user) {
143
+ throw new Error('Failed to verify token');
144
+ }
145
+ await saveToken(token);
146
+ console.log(chalk.green(`\nLogged in as ${chalk.bold(user.login)}`));
147
+ console.log(chalk.dim('Credentials saved to ~/.haystack/credentials.json\n'));
148
+ }
149
+ catch (error) {
150
+ console.error(chalk.red(`\nLogin failed: ${error.message}\n`));
151
+ process.exit(1);
152
+ }
153
+ }
154
+ export async function logoutCommand() {
155
+ try {
156
+ await fs.unlink(TOKEN_FILE);
157
+ console.log(chalk.green('\nLogged out successfully.\n'));
158
+ }
159
+ catch {
160
+ console.log(chalk.yellow('\nNot logged in.\n'));
161
+ }
162
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Secrets commands - manage secrets stored on Haystack Platform
3
+ */
4
+ /**
5
+ * List all secrets (keys only, not values)
6
+ */
7
+ export declare function listSecrets(): Promise<void>;
8
+ /**
9
+ * Set a secret
10
+ */
11
+ export declare function setSecret(key: string, value: string, options: {
12
+ scope?: string;
13
+ scopeId?: string;
14
+ }): Promise<void>;
15
+ /**
16
+ * Delete a secret
17
+ */
18
+ export declare function deleteSecret(key: string): Promise<void>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Secrets commands - manage secrets stored on Haystack Platform
3
+ */
4
+ import chalk from 'chalk';
5
+ import { loadToken } from './login.js';
6
+ const API_BASE = 'https://haystackeditor.com/api/secrets';
7
+ async function requireAuth() {
8
+ const token = await loadToken();
9
+ if (!token) {
10
+ console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
11
+ process.exit(1);
12
+ }
13
+ return token;
14
+ }
15
+ async function apiRequest(method, path, token, body) {
16
+ const url = `${API_BASE}${path}`;
17
+ const response = await fetch(url, {
18
+ method,
19
+ headers: {
20
+ 'Authorization': `Bearer ${token}`,
21
+ 'Content-Type': 'application/json',
22
+ 'User-Agent': 'Haystack-CLI',
23
+ },
24
+ body: body ? JSON.stringify(body) : undefined,
25
+ });
26
+ return response;
27
+ }
28
+ /**
29
+ * List all secrets (keys only, not values)
30
+ */
31
+ export async function listSecrets() {
32
+ const token = await requireAuth();
33
+ console.log(chalk.dim('\nFetching secrets...\n'));
34
+ try {
35
+ const response = await apiRequest('GET', '', token);
36
+ if (!response.ok) {
37
+ if (response.status === 401) {
38
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
39
+ process.exit(1);
40
+ }
41
+ throw new Error(`Failed to list secrets: ${response.status}`);
42
+ }
43
+ const data = await response.json();
44
+ const secrets = data.secrets || [];
45
+ if (secrets.length === 0) {
46
+ console.log(chalk.yellow('No secrets found.\n'));
47
+ console.log(chalk.dim('Set a secret with: haystack secrets set KEY VALUE\n'));
48
+ return;
49
+ }
50
+ console.log(chalk.bold('Your secrets:\n'));
51
+ for (const secret of secrets) {
52
+ const scope = secret.scope === 'user' ? '' : chalk.dim(` (${secret.scope}: ${secret.scopeId})`);
53
+ console.log(` ${chalk.cyan(secret.key)}${scope}`);
54
+ }
55
+ console.log();
56
+ }
57
+ catch (error) {
58
+ console.error(chalk.red(`\nError: ${error.message}\n`));
59
+ process.exit(1);
60
+ }
61
+ }
62
+ /**
63
+ * Set a secret
64
+ */
65
+ export async function setSecret(key, value, options) {
66
+ const token = await requireAuth();
67
+ if (!key || !value) {
68
+ console.error(chalk.red('\nUsage: haystack secrets set KEY VALUE\n'));
69
+ process.exit(1);
70
+ }
71
+ // Validate key format
72
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
73
+ console.error(chalk.red('\nSecret key must be uppercase with underscores (e.g., MY_API_KEY)\n'));
74
+ process.exit(1);
75
+ }
76
+ console.log(chalk.dim(`\nSetting secret ${key}...`));
77
+ try {
78
+ const body = {
79
+ key,
80
+ plaintextValue: value, // Server will encrypt
81
+ };
82
+ if (options.scope) {
83
+ body.scope = options.scope;
84
+ }
85
+ if (options.scopeId) {
86
+ body.scopeId = options.scopeId;
87
+ }
88
+ const response = await apiRequest('POST', '', token, body);
89
+ if (!response.ok) {
90
+ if (response.status === 401) {
91
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
92
+ process.exit(1);
93
+ }
94
+ const error = await response.json().catch(() => ({}));
95
+ throw new Error(error.error || `Failed to set secret: ${response.status}`);
96
+ }
97
+ console.log(chalk.green(`\nSecret ${chalk.bold(key)} saved.\n`));
98
+ }
99
+ catch (error) {
100
+ console.error(chalk.red(`\nError: ${error.message}\n`));
101
+ process.exit(1);
102
+ }
103
+ }
104
+ /**
105
+ * Delete a secret
106
+ */
107
+ export async function deleteSecret(key) {
108
+ const token = await requireAuth();
109
+ if (!key) {
110
+ console.error(chalk.red('\nUsage: haystack secrets delete KEY\n'));
111
+ process.exit(1);
112
+ }
113
+ console.log(chalk.dim(`\nDeleting secret ${key}...`));
114
+ try {
115
+ const response = await apiRequest('DELETE', `/${encodeURIComponent(key)}`, token);
116
+ if (!response.ok) {
117
+ if (response.status === 401) {
118
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
119
+ process.exit(1);
120
+ }
121
+ if (response.status === 404) {
122
+ console.error(chalk.yellow(`\nSecret ${key} not found.\n`));
123
+ process.exit(1);
124
+ }
125
+ throw new Error(`Failed to delete secret: ${response.status}`);
126
+ }
127
+ console.log(chalk.green(`\nSecret ${chalk.bold(key)} deleted.\n`));
128
+ }
129
+ catch (error) {
130
+ console.error(chalk.red(`\nError: ${error.message}\n`));
131
+ process.exit(1);
132
+ }
133
+ }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * This enables AI agents to spin up sandboxes of your app for testing.
7
7
  *
8
8
  * Usage:
9
- * npx @haystackeditor/cli init # Set up .haystack.yml
10
- * npx @haystackeditor/cli status # Check configuration
9
+ * npx @haystackeditor/cli init # Set up .haystack.yml
10
+ * npx @haystackeditor/cli status # Check configuration
11
+ * npx @haystackeditor/cli login # Authenticate with GitHub
12
+ * npx @haystackeditor/cli secrets list # List stored secrets
11
13
  */
12
14
  export {};
package/dist/index.js CHANGED
@@ -6,17 +6,21 @@
6
6
  * This enables AI agents to spin up sandboxes of your app for testing.
7
7
  *
8
8
  * Usage:
9
- * npx @haystackeditor/cli init # Set up .haystack.yml
10
- * npx @haystackeditor/cli status # Check configuration
9
+ * npx @haystackeditor/cli init # Set up .haystack.yml
10
+ * npx @haystackeditor/cli status # Check configuration
11
+ * npx @haystackeditor/cli login # Authenticate with GitHub
12
+ * npx @haystackeditor/cli secrets list # List stored secrets
11
13
  */
12
14
  import { Command } from 'commander';
13
15
  import { statusCommand } from './commands/status.js';
14
16
  import { initCommand } from './commands/init.js';
17
+ import { loginCommand, logoutCommand } from './commands/login.js';
18
+ import { listSecrets, setSecret, deleteSecret } from './commands/secrets.js';
15
19
  const program = new Command();
16
20
  program
17
21
  .name('haystack')
18
22
  .description('Set up Haystack verification for your project')
19
- .version('0.2.0');
23
+ .version('0.3.0');
20
24
  program
21
25
  .command('init')
22
26
  .description('Create .haystack.yml configuration')
@@ -34,6 +38,34 @@ program
34
38
  .command('status')
35
39
  .description('Check if .haystack.yml exists and is valid')
36
40
  .action(statusCommand);
41
+ program
42
+ .command('login')
43
+ .description('Authenticate with GitHub')
44
+ .action(loginCommand);
45
+ program
46
+ .command('logout')
47
+ .description('Remove stored credentials')
48
+ .action(logoutCommand);
49
+ // Secrets subcommands
50
+ const secrets = program
51
+ .command('secrets')
52
+ .description('Manage secrets for sandbox environments');
53
+ secrets
54
+ .command('list')
55
+ .description('List all secrets (keys only)')
56
+ .action(listSecrets);
57
+ secrets
58
+ .command('set <key> <value>')
59
+ .description('Set a secret')
60
+ .option('--scope <scope>', 'Scope: user, org, or repo (default: user)')
61
+ .option('--scope-id <id>', 'Scope ID (org name or owner/repo)')
62
+ .action((key, value, options) => {
63
+ setSecret(key, value, options);
64
+ });
65
+ secrets
66
+ .command('delete <key>')
67
+ .description('Delete a secret')
68
+ .action(deleteSecret);
37
69
  // Show help if no command provided
38
70
  if (process.argv.length === 2) {
39
71
  program.help();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Set up Haystack verification for your project",
5
5
  "type": "module",
6
6
  "bin": {