@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.
@@ -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>;
@@ -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
- }
@@ -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;
@@ -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
- }