@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 +48 -129
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +162 -0
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +133 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +35 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,105 +1,81 @@
|
|
|
1
1
|
# @haystackeditor/cli
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
|
19
|
+
Interactive setup wizard:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
npx @haystackeditor/cli init # Interactive wizard
|
|
23
|
+
npx @haystackeditor/cli init -y # Accept all defaults
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
### `haystack
|
|
26
|
+
### `haystack status`
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Check if your project is configured:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
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
|
|
34
|
+
### `haystack login`
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
Authenticate with GitHub (required for secrets management):
|
|
39
37
|
|
|
40
38
|
```bash
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
45
|
+
# Log out (removes stored credentials)
|
|
46
|
+
npx @haystackeditor/cli logout
|
|
55
47
|
```
|
|
56
48
|
|
|
57
|
-
### `haystack
|
|
49
|
+
### `haystack secrets`
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
Manage secrets that will be injected into your sandbox environment:
|
|
60
52
|
|
|
61
53
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
57
|
+
# Set a secret
|
|
58
|
+
npx @haystackeditor/cli secrets set OPENAI_API_KEY sk-xxx
|
|
68
59
|
|
|
69
|
-
|
|
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
|
-
|
|
64
|
+
Secrets are encrypted and stored securely. They're automatically injected as environment variables when the sandbox runs your app.
|
|
78
65
|
|
|
79
|
-
|
|
66
|
+
**Scopes**: By default, secrets are user-scoped. You can also scope to an org or repo:
|
|
80
67
|
|
|
81
68
|
```bash
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
## How It Works
|
|
195
110
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
10
|
-
* npx @haystackeditor/cli status
|
|
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
|
|
10
|
-
* npx @haystackeditor/cli status
|
|
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.
|
|
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();
|