@haystackeditor/cli 0.10.1 → 0.10.3
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/config.d.ts +1 -5
- package/dist/commands/config.js +2 -56
- package/dist/commands/init.js +0 -30
- package/dist/commands/skills.js +3 -8
- package/dist/commands/status.js +0 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -58
- package/dist/types.d.ts +4 -4
- package/dist/types.js +4 -4
- package/dist/utils/secrets.js +1 -2
- package/package.json +1 -1
- package/dist/assets/skills/prepare-haystack.md +0 -323
- package/dist/assets/skills/secrets.md +0 -164
- package/dist/assets/skills/setup-external-sandbox.md +0 -243
- package/dist/assets/skills/setup-haystack.md +0 -639
- package/dist/assets/templates/CLAUDE.md.snippet +0 -42
- package/dist/assets/templates/haystack.yml +0 -193
- package/dist/commands/secrets.d.ts +0 -33
- package/dist/commands/secrets.js +0 -216
- package/dist/tools/fixtures.d.ts +0 -38
- package/dist/tools/fixtures.js +0 -199
- package/dist/tools/setup.d.ts +0 -43
- package/dist/tools/setup.js +0 -597
- package/dist/utils/skill.d.ts +0 -10
- package/dist/utils/skill.js +0 -1671
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# =============================================================================
|
|
2
|
-
# Haystack Verification Configuration
|
|
3
|
-
# =============================================================================
|
|
4
|
-
#
|
|
5
|
-
# This file tells Haystack how to verify that PRs actually work.
|
|
6
|
-
# When a PR is opened, Haystack spins up a sandbox with your code and
|
|
7
|
-
# runs these verification flows to ensure changes work correctly.
|
|
8
|
-
#
|
|
9
|
-
# For setup help, run: /verify setup
|
|
10
|
-
# =============================================================================
|
|
11
|
-
|
|
12
|
-
name: my-saas-app
|
|
13
|
-
|
|
14
|
-
# -----------------------------------------------------------------------------
|
|
15
|
-
# Dev Server - How to run your app
|
|
16
|
-
# -----------------------------------------------------------------------------
|
|
17
|
-
dev:
|
|
18
|
-
command: pnpm dev
|
|
19
|
-
port: 3000
|
|
20
|
-
ready_pattern: "ready in" # Pattern in stdout that means server is ready
|
|
21
|
-
cwd: "." # Working directory (relative to repo root)
|
|
22
|
-
env:
|
|
23
|
-
NODE_ENV: development
|
|
24
|
-
|
|
25
|
-
# -----------------------------------------------------------------------------
|
|
26
|
-
# Authentication - How to bypass/mock auth in the sandbox
|
|
27
|
-
# -----------------------------------------------------------------------------
|
|
28
|
-
auth:
|
|
29
|
-
strategy: bypass
|
|
30
|
-
env: "SKIP_AUTH=true"
|
|
31
|
-
|
|
32
|
-
# Other options:
|
|
33
|
-
# strategy: mock_token
|
|
34
|
-
# env: "AUTH_TOKEN" # Set actual value as Haystack secret
|
|
35
|
-
|
|
36
|
-
# strategy: test_account
|
|
37
|
-
# username_env: "TEST_USER"
|
|
38
|
-
# password_env: "TEST_PASS"
|
|
39
|
-
# login_url: "/login"
|
|
40
|
-
|
|
41
|
-
# -----------------------------------------------------------------------------
|
|
42
|
-
# Fixtures - Where to get test data (so sandbox doesn't need real data)
|
|
43
|
-
# -----------------------------------------------------------------------------
|
|
44
|
-
fixtures:
|
|
45
|
-
# Pull user data from staging API
|
|
46
|
-
- pattern: "/api/users/*"
|
|
47
|
-
source: "https://staging.myapp.com/api/users"
|
|
48
|
-
headers:
|
|
49
|
-
Authorization: "Bearer $STAGING_API_TOKEN"
|
|
50
|
-
|
|
51
|
-
# Use local fixture file for config
|
|
52
|
-
- pattern: "/api/config"
|
|
53
|
-
source: "file://fixtures/config.json"
|
|
54
|
-
|
|
55
|
-
# Generate dashboard data dynamically
|
|
56
|
-
- pattern: "/api/dashboard"
|
|
57
|
-
source: "script://fixtures/generate-dashboard.ts"
|
|
58
|
-
|
|
59
|
-
# Let public endpoints go through
|
|
60
|
-
- pattern: "/api/public/*"
|
|
61
|
-
source: passthrough
|
|
62
|
-
|
|
63
|
-
# -----------------------------------------------------------------------------
|
|
64
|
-
# Verification Flows - What to actually test
|
|
65
|
-
# -----------------------------------------------------------------------------
|
|
66
|
-
# These are the flows the agent executes in the sandbox to verify PRs work.
|
|
67
|
-
# Think of them like E2E tests, but run by an AI that can adapt and report.
|
|
68
|
-
|
|
69
|
-
flows:
|
|
70
|
-
# ---------------------------------------------------------------------------
|
|
71
|
-
# Basic smoke test - runs on every PR
|
|
72
|
-
# ---------------------------------------------------------------------------
|
|
73
|
-
- name: "Homepage loads"
|
|
74
|
-
description: "Verify the homepage renders without errors"
|
|
75
|
-
trigger: always
|
|
76
|
-
steps:
|
|
77
|
-
- action: navigate
|
|
78
|
-
url: "/"
|
|
79
|
-
- action: wait_for
|
|
80
|
-
selector: "#app"
|
|
81
|
-
timeout_ms: 10000
|
|
82
|
-
- action: assert_no_errors
|
|
83
|
-
error_selectors:
|
|
84
|
-
- ".error-toast"
|
|
85
|
-
- ".error-modal"
|
|
86
|
-
- "[data-testid='error']"
|
|
87
|
-
- action: screenshot
|
|
88
|
-
name: "homepage"
|
|
89
|
-
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
# Dashboard flow - runs when dashboard files change
|
|
92
|
-
# ---------------------------------------------------------------------------
|
|
93
|
-
- name: "Dashboard functionality"
|
|
94
|
-
description: "Verify dashboard loads and displays data correctly"
|
|
95
|
-
trigger: on_change
|
|
96
|
-
watch_patterns:
|
|
97
|
-
- "src/components/dashboard/**"
|
|
98
|
-
- "src/pages/dashboard.tsx"
|
|
99
|
-
steps:
|
|
100
|
-
- action: navigate
|
|
101
|
-
url: "/dashboard"
|
|
102
|
-
- action: wait_for
|
|
103
|
-
selector: ".dashboard-grid"
|
|
104
|
-
- action: assert_exists
|
|
105
|
-
selector: ".metric-card"
|
|
106
|
-
message: "Dashboard should display metric cards"
|
|
107
|
-
- action: assert_text
|
|
108
|
-
selector: ".welcome-message"
|
|
109
|
-
contains: "Welcome"
|
|
110
|
-
- action: screenshot
|
|
111
|
-
name: "dashboard-loaded"
|
|
112
|
-
# Test interactivity
|
|
113
|
-
- action: click
|
|
114
|
-
selector: "[data-testid='refresh-button']"
|
|
115
|
-
description: "Click refresh button"
|
|
116
|
-
- action: wait_network_idle
|
|
117
|
-
timeout_ms: 5000
|
|
118
|
-
- action: assert_no_errors
|
|
119
|
-
|
|
120
|
-
# ---------------------------------------------------------------------------
|
|
121
|
-
# Settings flow - test form interactions
|
|
122
|
-
# ---------------------------------------------------------------------------
|
|
123
|
-
- name: "Settings page"
|
|
124
|
-
description: "Verify settings can be viewed and saved"
|
|
125
|
-
trigger: on_change
|
|
126
|
-
watch_patterns:
|
|
127
|
-
- "src/components/settings/**"
|
|
128
|
-
- "src/pages/settings.tsx"
|
|
129
|
-
steps:
|
|
130
|
-
- action: navigate
|
|
131
|
-
url: "/settings"
|
|
132
|
-
- action: wait_for
|
|
133
|
-
selector: "form.settings-form"
|
|
134
|
-
- action: screenshot
|
|
135
|
-
name: "settings-initial"
|
|
136
|
-
# Toggle dark mode
|
|
137
|
-
- action: click
|
|
138
|
-
selector: "#dark-mode-toggle"
|
|
139
|
-
- action: assert_exists
|
|
140
|
-
selector: "body.dark-mode"
|
|
141
|
-
message: "Dark mode should be applied"
|
|
142
|
-
- action: screenshot
|
|
143
|
-
name: "settings-dark-mode"
|
|
144
|
-
# Test form input
|
|
145
|
-
- action: type
|
|
146
|
-
selector: "#display-name"
|
|
147
|
-
text: "Test User"
|
|
148
|
-
- action: click
|
|
149
|
-
selector: "button[type='submit']"
|
|
150
|
-
- action: wait_network_idle
|
|
151
|
-
- action: assert_no_errors
|
|
152
|
-
|
|
153
|
-
# ---------------------------------------------------------------------------
|
|
154
|
-
# Checkout flow - critical path, always run
|
|
155
|
-
# ---------------------------------------------------------------------------
|
|
156
|
-
- name: "Checkout flow"
|
|
157
|
-
description: "Verify the complete checkout process works"
|
|
158
|
-
trigger: always
|
|
159
|
-
steps:
|
|
160
|
-
- action: navigate
|
|
161
|
-
url: "/products"
|
|
162
|
-
- action: wait_for
|
|
163
|
-
selector: ".product-grid"
|
|
164
|
-
- action: click
|
|
165
|
-
selector: ".product-card:first-child .add-to-cart"
|
|
166
|
-
description: "Add first product to cart"
|
|
167
|
-
- action: wait_for
|
|
168
|
-
selector: ".cart-count"
|
|
169
|
-
- action: assert_text
|
|
170
|
-
selector: ".cart-count"
|
|
171
|
-
contains: "1"
|
|
172
|
-
- action: navigate
|
|
173
|
-
url: "/cart"
|
|
174
|
-
- action: wait_for
|
|
175
|
-
selector: ".cart-items"
|
|
176
|
-
- action: click
|
|
177
|
-
selector: ".checkout-button"
|
|
178
|
-
- action: wait_for
|
|
179
|
-
selector: ".checkout-form"
|
|
180
|
-
- action: screenshot
|
|
181
|
-
name: "checkout-form"
|
|
182
|
-
compare_baseline: true # Visual regression check
|
|
183
|
-
|
|
184
|
-
# -----------------------------------------------------------------------------
|
|
185
|
-
# Global Settings
|
|
186
|
-
# -----------------------------------------------------------------------------
|
|
187
|
-
settings:
|
|
188
|
-
default_timeout_ms: 10000
|
|
189
|
-
error_selectors:
|
|
190
|
-
- ".error-toast"
|
|
191
|
-
- ".error-boundary"
|
|
192
|
-
- "[role='alert'][aria-live='assertive']"
|
|
193
|
-
screenshot_each_step: false # Set true for debugging
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
* Get a secret value (for programmatic use)
|
|
17
|
-
* Returns the value to stdout for piping to other commands.
|
|
18
|
-
* Use --quiet to suppress all output except the value.
|
|
19
|
-
*/
|
|
20
|
-
export declare function getSecret(key: string, options: {
|
|
21
|
-
quiet?: boolean;
|
|
22
|
-
}): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Get multiple secrets needed by a repo's .haystack.json
|
|
25
|
-
* Returns JSON object with key-value pairs.
|
|
26
|
-
*/
|
|
27
|
-
export declare function getSecretsForRepo(keys: string[], options: {
|
|
28
|
-
json?: boolean;
|
|
29
|
-
}): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Delete a secret
|
|
32
|
-
*/
|
|
33
|
-
export declare function deleteSecret(key: string): Promise<void>;
|
package/dist/commands/secrets.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
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
|
-
* Get a secret value (for programmatic use)
|
|
106
|
-
* Returns the value to stdout for piping to other commands.
|
|
107
|
-
* Use --quiet to suppress all output except the value.
|
|
108
|
-
*/
|
|
109
|
-
export async function getSecret(key, options) {
|
|
110
|
-
const token = await requireAuth();
|
|
111
|
-
if (!key) {
|
|
112
|
-
console.error(chalk.red('\nUsage: haystack secrets get KEY\n'));
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const response = await apiRequest('GET', `/${encodeURIComponent(key)}`, token);
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
if (response.status === 401) {
|
|
119
|
-
if (!options.quiet) {
|
|
120
|
-
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
121
|
-
}
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
if (response.status === 404) {
|
|
125
|
-
if (!options.quiet) {
|
|
126
|
-
console.error(chalk.yellow(`\nSecret ${key} not found.\n`));
|
|
127
|
-
}
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
throw new Error(`Failed to get secret: ${response.status}`);
|
|
131
|
-
}
|
|
132
|
-
const data = await response.json();
|
|
133
|
-
// Output just the value (no newline if quiet, for piping)
|
|
134
|
-
if (options.quiet) {
|
|
135
|
-
process.stdout.write(data.value);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
console.log(data.value);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
if (!options.quiet) {
|
|
143
|
-
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
144
|
-
}
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Get multiple secrets needed by a repo's .haystack.json
|
|
150
|
-
* Returns JSON object with key-value pairs.
|
|
151
|
-
*/
|
|
152
|
-
export async function getSecretsForRepo(keys, options) {
|
|
153
|
-
const token = await requireAuth();
|
|
154
|
-
if (keys.length === 0) {
|
|
155
|
-
console.error(chalk.red('\nUsage: haystack secrets get-for-repo KEY1 KEY2 ...\n'));
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const response = await apiRequest('POST', '/batch', token, { keys });
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
if (response.status === 401) {
|
|
162
|
-
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
throw new Error(`Failed to get secrets: ${response.status}`);
|
|
166
|
-
}
|
|
167
|
-
const data = await response.json();
|
|
168
|
-
if (options.json) {
|
|
169
|
-
console.log(JSON.stringify(data.secrets));
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
if (data.missing.length > 0) {
|
|
173
|
-
console.log(chalk.yellow(`\nMissing secrets: ${data.missing.join(', ')}\n`));
|
|
174
|
-
}
|
|
175
|
-
console.log(chalk.bold('\nSecrets loaded:\n'));
|
|
176
|
-
for (const [k, v] of Object.entries(data.secrets)) {
|
|
177
|
-
console.log(` ${chalk.cyan(k)}: ${chalk.dim('***' + v.slice(-4))}`);
|
|
178
|
-
}
|
|
179
|
-
console.log();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Delete a secret
|
|
189
|
-
*/
|
|
190
|
-
export async function deleteSecret(key) {
|
|
191
|
-
const token = await requireAuth();
|
|
192
|
-
if (!key) {
|
|
193
|
-
console.error(chalk.red('\nUsage: haystack secrets delete KEY\n'));
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
console.log(chalk.dim(`\nDeleting secret ${key}...`));
|
|
197
|
-
try {
|
|
198
|
-
const response = await apiRequest('DELETE', `/${encodeURIComponent(key)}`, token);
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
if (response.status === 401) {
|
|
201
|
-
console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
if (response.status === 404) {
|
|
205
|
-
console.error(chalk.yellow(`\nSecret ${key} not found.\n`));
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
throw new Error(`Failed to delete secret: ${response.status}`);
|
|
209
|
-
}
|
|
210
|
-
console.log(chalk.green(`\nSecret ${chalk.bold(key)} deleted.\n`));
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
}
|
package/dist/tools/fixtures.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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;
|
package/dist/tools/fixtures.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
}
|