@haystackeditor/cli 0.2.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 +202 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +299 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.js +42 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +41 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.js +6 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.js +74 -0
- package/dist/utils/detect.d.ts +10 -0
- package/dist/utils/detect.js +306 -0
- package/dist/utils/secrets.d.ts +47 -0
- package/dist/utils/secrets.js +241 -0
- package/dist/utils/skill.d.ts +4 -0
- package/dist/utils/skill.js +249 -0
- package/package.json +51 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haystack CLI Types
|
|
3
|
+
*
|
|
4
|
+
* These types define the .haystack.yml schema that sandbox.py reads.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Main configuration file schema (.haystack.yml)
|
|
8
|
+
*/
|
|
9
|
+
export interface HaystackConfig {
|
|
10
|
+
/** Schema version - must be "1" */
|
|
11
|
+
version: '1';
|
|
12
|
+
/** Project name for display */
|
|
13
|
+
name?: string;
|
|
14
|
+
/** Package manager (auto-detected if not specified) */
|
|
15
|
+
package_manager?: 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
16
|
+
/**
|
|
17
|
+
* Single dev server configuration.
|
|
18
|
+
* Use this for simple projects with one service.
|
|
19
|
+
* Mutually exclusive with `services`.
|
|
20
|
+
*/
|
|
21
|
+
dev_server?: DevServer;
|
|
22
|
+
/**
|
|
23
|
+
* Multiple services configuration.
|
|
24
|
+
* Use this for monorepos with multiple services.
|
|
25
|
+
* Mutually exclusive with `dev_server`.
|
|
26
|
+
*/
|
|
27
|
+
services?: Record<string, Service>;
|
|
28
|
+
/** Verification configuration */
|
|
29
|
+
verification?: VerificationConfig;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Dev server configuration (simple mode)
|
|
33
|
+
*/
|
|
34
|
+
export interface DevServer {
|
|
35
|
+
/** Command to start the dev server (e.g., "pnpm dev") */
|
|
36
|
+
command: string;
|
|
37
|
+
/** Port the server runs on */
|
|
38
|
+
port: number;
|
|
39
|
+
/** Pattern in stdout indicating server is ready (e.g., "Local:") */
|
|
40
|
+
ready_pattern?: string;
|
|
41
|
+
/** Environment variables to set (e.g., { SKIP_AUTH: "true" }) */
|
|
42
|
+
env?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Service configuration (monorepo mode)
|
|
46
|
+
*/
|
|
47
|
+
export interface Service {
|
|
48
|
+
/** Directory relative to repo root (e.g., "infra/api-worker") */
|
|
49
|
+
root?: string;
|
|
50
|
+
/** Command to start the service */
|
|
51
|
+
command: string;
|
|
52
|
+
/** Port the service runs on (omit for batch jobs) */
|
|
53
|
+
port?: number;
|
|
54
|
+
/** Pattern in stdout indicating service is ready */
|
|
55
|
+
ready_pattern?: string;
|
|
56
|
+
/** Service type: "server" (default) or "batch" */
|
|
57
|
+
type?: 'server' | 'batch';
|
|
58
|
+
/** Environment variables to set */
|
|
59
|
+
env?: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verification configuration
|
|
63
|
+
*/
|
|
64
|
+
export interface VerificationConfig {
|
|
65
|
+
/** Commands to run during sandbox creation */
|
|
66
|
+
commands?: VerificationCommand[];
|
|
67
|
+
/** Fixture definitions - array of pattern/source pairs */
|
|
68
|
+
fixtures?: Fixture[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fixture definition - pattern to match and source to load from
|
|
72
|
+
*/
|
|
73
|
+
export interface Fixture {
|
|
74
|
+
/** URL pattern to match (e.g., "/api/github/repos/*") */
|
|
75
|
+
pattern: string;
|
|
76
|
+
/**
|
|
77
|
+
* Source to load data from. Supports environment variable substitution:
|
|
78
|
+
* - $VAR, ${VAR}, ${VAR:-default}
|
|
79
|
+
*
|
|
80
|
+
* Source types:
|
|
81
|
+
* - file://path/to/file.json - Local file
|
|
82
|
+
* - https://staging.example.com/api/... - Remote URL (e.g., staging server)
|
|
83
|
+
* - https://${STAGING_HOST}/api/... - Remote URL with env var
|
|
84
|
+
* - s3://bucket/key - AWS S3 (uses AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
85
|
+
* - r2://bucket/key - Cloudflare R2 (uses R2_ACCOUNT_ID, R2_ACCESS_KEY_ID)
|
|
86
|
+
* - passthrough - Don't intercept, let request through to real API
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Pull from staging with auth
|
|
90
|
+
* source: "https://${STAGING_URL:-staging.example.com}/api/users"
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // S3 with region override
|
|
94
|
+
* source: "s3://my-bucket/fixtures/data.json"
|
|
95
|
+
*/
|
|
96
|
+
source: string;
|
|
97
|
+
/**
|
|
98
|
+
* Headers for remote sources.
|
|
99
|
+
* Supports environment variable substitution: $VAR, ${VAR}, ${VAR:-default}
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* headers:
|
|
103
|
+
* Authorization: "Bearer $STAGING_API_TOKEN"
|
|
104
|
+
* Cookie: "$STAGING_SESSION_COOKIE"
|
|
105
|
+
* X-Api-Key: "${API_KEY:-test-key}"
|
|
106
|
+
*/
|
|
107
|
+
headers?: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A verification command (e.g., build, lint, typecheck)
|
|
111
|
+
*/
|
|
112
|
+
export interface VerificationCommand {
|
|
113
|
+
/** Human-readable name */
|
|
114
|
+
name: string;
|
|
115
|
+
/** Shell command to run */
|
|
116
|
+
run: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @deprecated Use Fixture interface instead. This is kept for backwards compatibility.
|
|
120
|
+
*/
|
|
121
|
+
export interface FixtureEntry {
|
|
122
|
+
source: string;
|
|
123
|
+
headers?: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Project detection results
|
|
127
|
+
*/
|
|
128
|
+
export interface DetectedProject {
|
|
129
|
+
/** Detected framework */
|
|
130
|
+
framework?: 'vite' | 'nextjs' | 'remix' | 'nuxt' | 'sveltekit' | 'astro' | 'cra' | 'cloudflare-worker';
|
|
131
|
+
/** Detected package manager */
|
|
132
|
+
packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
133
|
+
/** Whether this is a monorepo */
|
|
134
|
+
isMonorepo: boolean;
|
|
135
|
+
/** Monorepo tool if detected */
|
|
136
|
+
monorepoTool?: 'pnpm-workspaces' | 'lerna' | 'nx' | 'turborepo' | 'rush';
|
|
137
|
+
/** Detected services in monorepo */
|
|
138
|
+
services?: DetectedService[];
|
|
139
|
+
/** Suggested dev command */
|
|
140
|
+
suggestedDevCommand?: string;
|
|
141
|
+
/** Suggested port */
|
|
142
|
+
suggestedPort?: number;
|
|
143
|
+
/** Suggested ready pattern */
|
|
144
|
+
suggestedReadyPattern?: string;
|
|
145
|
+
/** Suggested auth bypass env var */
|
|
146
|
+
suggestedAuthBypass?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Detected service in a monorepo
|
|
150
|
+
*/
|
|
151
|
+
export interface DetectedService {
|
|
152
|
+
name: string;
|
|
153
|
+
root: string;
|
|
154
|
+
type: 'server' | 'batch';
|
|
155
|
+
framework?: string;
|
|
156
|
+
suggestedCommand?: string;
|
|
157
|
+
suggestedPort?: number;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Auth credentials stored in Haystack
|
|
161
|
+
*/
|
|
162
|
+
export interface HaystackCredentials {
|
|
163
|
+
/** GitHub access token */
|
|
164
|
+
token: string;
|
|
165
|
+
/** Token expiration (ISO string) */
|
|
166
|
+
expiresAt?: string;
|
|
167
|
+
/** Associated GitHub username */
|
|
168
|
+
username?: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Stored secrets (in Haystack platform)
|
|
172
|
+
*/
|
|
173
|
+
export interface StoredSecret {
|
|
174
|
+
key: string;
|
|
175
|
+
createdAt: string;
|
|
176
|
+
updatedAt: string;
|
|
177
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { HaystackConfig } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Find the config file by walking up the directory tree
|
|
7
|
+
*/
|
|
8
|
+
export declare function findConfigPath(startDir?: string): Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Load and parse the config file
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadConfig(configPath?: string): Promise<HaystackConfig | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Save config to file
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveConfig(config: HaystackConfig, configPath?: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if config exists
|
|
19
|
+
*/
|
|
20
|
+
export declare function configExists(startDir?: string): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Get the project root (directory containing .haystack.yml or current dir)
|
|
23
|
+
*/
|
|
24
|
+
export declare function getProjectRoot(): Promise<string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file utilities
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as yaml from 'yaml';
|
|
7
|
+
const CONFIG_FILENAME = '.haystack.yml';
|
|
8
|
+
/**
|
|
9
|
+
* Find the config file by walking up the directory tree
|
|
10
|
+
*/
|
|
11
|
+
export async function findConfigPath(startDir = process.cwd()) {
|
|
12
|
+
let dir = startDir;
|
|
13
|
+
while (true) {
|
|
14
|
+
const configPath = path.join(dir, CONFIG_FILENAME);
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(configPath);
|
|
17
|
+
return configPath;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Not found, try parent
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) {
|
|
24
|
+
// Reached root
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load and parse the config file
|
|
32
|
+
*/
|
|
33
|
+
export async function loadConfig(configPath) {
|
|
34
|
+
const resolvedPath = configPath || (await findConfigPath());
|
|
35
|
+
if (!resolvedPath) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
40
|
+
return yaml.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new Error(`Failed to parse ${resolvedPath}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Save config to file
|
|
48
|
+
*/
|
|
49
|
+
export async function saveConfig(config, configPath) {
|
|
50
|
+
const resolvedPath = configPath || path.join(process.cwd(), CONFIG_FILENAME);
|
|
51
|
+
const content = yaml.stringify(config, {
|
|
52
|
+
indent: 2,
|
|
53
|
+
lineWidth: 0, // Don't wrap lines
|
|
54
|
+
});
|
|
55
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
56
|
+
return resolvedPath;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if config exists
|
|
60
|
+
*/
|
|
61
|
+
export async function configExists(startDir) {
|
|
62
|
+
const configPath = await findConfigPath(startDir);
|
|
63
|
+
return configPath !== null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the project root (directory containing .haystack.yml or current dir)
|
|
67
|
+
*/
|
|
68
|
+
export async function getProjectRoot() {
|
|
69
|
+
const configPath = await findConfigPath();
|
|
70
|
+
if (configPath) {
|
|
71
|
+
return path.dirname(configPath);
|
|
72
|
+
}
|
|
73
|
+
return process.cwd();
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects framework, package manager, monorepo structure, and services.
|
|
5
|
+
*/
|
|
6
|
+
import type { DetectedProject } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Detect project configuration
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectProject(rootDir?: string): Promise<DetectedProject>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects framework, package manager, monorepo structure, and services.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
/**
|
|
9
|
+
* Detect project configuration
|
|
10
|
+
*/
|
|
11
|
+
export async function detectProject(rootDir = process.cwd()) {
|
|
12
|
+
const result = {
|
|
13
|
+
packageManager: await detectPackageManager(rootDir),
|
|
14
|
+
isMonorepo: false,
|
|
15
|
+
};
|
|
16
|
+
// Detect monorepo
|
|
17
|
+
const monorepo = await detectMonorepo(rootDir);
|
|
18
|
+
if (monorepo) {
|
|
19
|
+
result.isMonorepo = true;
|
|
20
|
+
result.monorepoTool = monorepo.tool;
|
|
21
|
+
result.services = await detectServices(rootDir, monorepo.workspaces);
|
|
22
|
+
}
|
|
23
|
+
// Detect framework
|
|
24
|
+
result.framework = await detectFramework(rootDir);
|
|
25
|
+
// Set suggestions based on framework
|
|
26
|
+
setSuggestions(result);
|
|
27
|
+
// Detect auth bypass from .env.example
|
|
28
|
+
result.suggestedAuthBypass = await detectAuthBypass(rootDir);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Detect package manager from lockfiles
|
|
33
|
+
*/
|
|
34
|
+
async function detectPackageManager(rootDir) {
|
|
35
|
+
const checks = [
|
|
36
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
37
|
+
{ file: 'yarn.lock', manager: 'yarn' },
|
|
38
|
+
{ file: 'bun.lockb', manager: 'bun' },
|
|
39
|
+
{ file: 'package-lock.json', manager: 'npm' },
|
|
40
|
+
];
|
|
41
|
+
for (const { file, manager } of checks) {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(path.join(rootDir, file));
|
|
44
|
+
return manager;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Not found, continue
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return 'npm'; // Default
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detect monorepo structure
|
|
54
|
+
*/
|
|
55
|
+
async function detectMonorepo(rootDir) {
|
|
56
|
+
// Check pnpm workspaces
|
|
57
|
+
try {
|
|
58
|
+
const pnpmWorkspace = await fs.readFile(path.join(rootDir, 'pnpm-workspace.yaml'), 'utf-8');
|
|
59
|
+
const match = pnpmWorkspace.match(/packages:\s*\n([\s\S]*?)(?:\n\w|$)/);
|
|
60
|
+
if (match) {
|
|
61
|
+
const workspaces = match[1]
|
|
62
|
+
.split('\n')
|
|
63
|
+
.map((line) => line.trim().replace(/^-\s*['"]?|['"]?$/g, ''))
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
return { tool: 'pnpm-workspaces', workspaces };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Not found
|
|
70
|
+
}
|
|
71
|
+
// Check package.json workspaces (yarn/npm)
|
|
72
|
+
try {
|
|
73
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
74
|
+
if (pkgJson.workspaces) {
|
|
75
|
+
const workspaces = Array.isArray(pkgJson.workspaces)
|
|
76
|
+
? pkgJson.workspaces
|
|
77
|
+
: pkgJson.workspaces.packages || [];
|
|
78
|
+
return { tool: 'pnpm-workspaces', workspaces };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Not found
|
|
83
|
+
}
|
|
84
|
+
// Check for nx.json
|
|
85
|
+
try {
|
|
86
|
+
await fs.access(path.join(rootDir, 'nx.json'));
|
|
87
|
+
return { tool: 'nx', workspaces: ['packages/*', 'apps/*'] };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Not found
|
|
91
|
+
}
|
|
92
|
+
// Check for turbo.json
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(path.join(rootDir, 'turbo.json'));
|
|
95
|
+
return { tool: 'turborepo', workspaces: ['packages/*', 'apps/*'] };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Not found
|
|
99
|
+
}
|
|
100
|
+
// Check for lerna.json
|
|
101
|
+
try {
|
|
102
|
+
await fs.access(path.join(rootDir, 'lerna.json'));
|
|
103
|
+
return { tool: 'lerna', workspaces: ['packages/*'] };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Not found
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Detect services in a monorepo
|
|
112
|
+
*/
|
|
113
|
+
async function detectServices(rootDir, workspacePatterns) {
|
|
114
|
+
const services = [];
|
|
115
|
+
const glob = await import('fast-glob');
|
|
116
|
+
for (const pattern of workspacePatterns) {
|
|
117
|
+
const matches = await glob.default(pattern, {
|
|
118
|
+
cwd: rootDir,
|
|
119
|
+
onlyDirectories: true,
|
|
120
|
+
ignore: ['node_modules'],
|
|
121
|
+
});
|
|
122
|
+
for (const match of matches) {
|
|
123
|
+
const service = await detectService(rootDir, match);
|
|
124
|
+
if (service) {
|
|
125
|
+
services.push(service);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Also check common directories
|
|
130
|
+
const commonDirs = ['infra', 'services', 'apps', 'packages'];
|
|
131
|
+
for (const dir of commonDirs) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = await fs.readdir(path.join(rootDir, dir), {
|
|
134
|
+
withFileTypes: true,
|
|
135
|
+
});
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
const servicePath = path.join(dir, entry.name);
|
|
139
|
+
// Skip if already detected
|
|
140
|
+
if (services.some((s) => s.root === servicePath))
|
|
141
|
+
continue;
|
|
142
|
+
const service = await detectService(rootDir, servicePath);
|
|
143
|
+
if (service) {
|
|
144
|
+
services.push(service);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Directory doesn't exist
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return services;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Detect a single service
|
|
157
|
+
*/
|
|
158
|
+
async function detectService(rootDir, servicePath) {
|
|
159
|
+
const fullPath = path.join(rootDir, servicePath);
|
|
160
|
+
// Check for package.json
|
|
161
|
+
let pkgJson = null;
|
|
162
|
+
try {
|
|
163
|
+
pkgJson = JSON.parse(await fs.readFile(path.join(fullPath, 'package.json'), 'utf-8'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null; // No package.json, not a service
|
|
167
|
+
}
|
|
168
|
+
const name = pkgJson?.name?.replace(/^@[^/]+\//, '') || path.basename(servicePath);
|
|
169
|
+
const scripts = pkgJson?.scripts || {};
|
|
170
|
+
// Detect if it's a Cloudflare Worker
|
|
171
|
+
const isWorker = await hasFile(fullPath, 'wrangler.toml') ||
|
|
172
|
+
await hasFile(fullPath, 'wrangler.jsonc') ||
|
|
173
|
+
await hasFile(fullPath, 'wrangler.json');
|
|
174
|
+
// Detect framework
|
|
175
|
+
let framework;
|
|
176
|
+
let suggestedCommand;
|
|
177
|
+
let suggestedPort;
|
|
178
|
+
if (isWorker) {
|
|
179
|
+
framework = 'cloudflare-worker';
|
|
180
|
+
suggestedCommand = 'pnpm wrangler dev --local';
|
|
181
|
+
suggestedPort = 8787;
|
|
182
|
+
}
|
|
183
|
+
else if (await hasFile(fullPath, 'vite.config.ts') || await hasFile(fullPath, 'vite.config.js')) {
|
|
184
|
+
framework = 'vite';
|
|
185
|
+
suggestedCommand = 'pnpm dev';
|
|
186
|
+
suggestedPort = 5173;
|
|
187
|
+
}
|
|
188
|
+
else if (await hasFile(fullPath, 'next.config.js') || await hasFile(fullPath, 'next.config.ts')) {
|
|
189
|
+
framework = 'nextjs';
|
|
190
|
+
suggestedCommand = 'pnpm dev';
|
|
191
|
+
suggestedPort = 3000;
|
|
192
|
+
}
|
|
193
|
+
else if (scripts.dev) {
|
|
194
|
+
suggestedCommand = 'pnpm dev';
|
|
195
|
+
suggestedPort = 3000;
|
|
196
|
+
}
|
|
197
|
+
else if (scripts.start) {
|
|
198
|
+
suggestedCommand = 'pnpm start';
|
|
199
|
+
}
|
|
200
|
+
// Determine if it's a batch job or server
|
|
201
|
+
const type = name.includes('analysis') || name.includes('batch') || name.includes('job')
|
|
202
|
+
? 'batch'
|
|
203
|
+
: 'server';
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
root: servicePath,
|
|
207
|
+
type,
|
|
208
|
+
framework,
|
|
209
|
+
suggestedCommand,
|
|
210
|
+
suggestedPort: type === 'server' ? suggestedPort : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Detect framework from config files
|
|
215
|
+
*/
|
|
216
|
+
async function detectFramework(rootDir) {
|
|
217
|
+
const checks = [
|
|
218
|
+
{ files: ['vite.config.ts', 'vite.config.js'], framework: 'vite' },
|
|
219
|
+
{ files: ['next.config.js', 'next.config.ts', 'next.config.mjs'], framework: 'nextjs' },
|
|
220
|
+
{ files: ['remix.config.js', 'remix.config.ts'], framework: 'remix' },
|
|
221
|
+
{ files: ['nuxt.config.ts', 'nuxt.config.js'], framework: 'nuxt' },
|
|
222
|
+
{ files: ['svelte.config.js', 'svelte.config.ts'], framework: 'sveltekit' },
|
|
223
|
+
{ files: ['astro.config.mjs', 'astro.config.ts'], framework: 'astro' },
|
|
224
|
+
{ files: ['wrangler.toml', 'wrangler.jsonc'], framework: 'cloudflare-worker' },
|
|
225
|
+
];
|
|
226
|
+
for (const { files, framework } of checks) {
|
|
227
|
+
for (const file of files) {
|
|
228
|
+
if (await hasFile(rootDir, file)) {
|
|
229
|
+
return framework;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Check for CRA
|
|
234
|
+
try {
|
|
235
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
236
|
+
if (pkgJson.dependencies?.['react-scripts']) {
|
|
237
|
+
return 'cra';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Not found
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Detect auth bypass env var from .env.example
|
|
247
|
+
*/
|
|
248
|
+
async function detectAuthBypass(rootDir) {
|
|
249
|
+
const envFiles = ['.env.example', '.env.local.example', '.env.development'];
|
|
250
|
+
for (const file of envFiles) {
|
|
251
|
+
try {
|
|
252
|
+
const content = await fs.readFile(path.join(rootDir, file), 'utf-8');
|
|
253
|
+
// Common patterns
|
|
254
|
+
const patterns = [
|
|
255
|
+
/^(SKIP_AUTH)=/m,
|
|
256
|
+
/^(BYPASS_AUTH)=/m,
|
|
257
|
+
/^(AUTH_DISABLED)=/m,
|
|
258
|
+
/^(MOCK_AUTH)=/m,
|
|
259
|
+
/^(NEXT_PUBLIC_MOCK_AUTH)=/m,
|
|
260
|
+
/^(VITE_MOCK_AUTH)=/m,
|
|
261
|
+
/^(VITE_SKIP_AUTH)=/m,
|
|
262
|
+
];
|
|
263
|
+
for (const pattern of patterns) {
|
|
264
|
+
const match = content.match(pattern);
|
|
265
|
+
if (match) {
|
|
266
|
+
return `${match[1]}=true`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// File not found
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return 'SKIP_AUTH=true'; // Default suggestion
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Set suggestions based on detected framework
|
|
278
|
+
*/
|
|
279
|
+
function setSuggestions(result) {
|
|
280
|
+
const frameworkDefaults = {
|
|
281
|
+
vite: { command: 'pnpm dev', port: 5173, ready: 'Local:' },
|
|
282
|
+
nextjs: { command: 'pnpm dev', port: 3000, ready: 'Ready' },
|
|
283
|
+
remix: { command: 'pnpm dev', port: 3000, ready: 'started' },
|
|
284
|
+
nuxt: { command: 'pnpm dev', port: 3000, ready: 'Local:' },
|
|
285
|
+
sveltekit: { command: 'pnpm dev', port: 5173, ready: 'Local:' },
|
|
286
|
+
astro: { command: 'pnpm dev', port: 4321, ready: 'Local:' },
|
|
287
|
+
cra: { command: 'pnpm start', port: 3000, ready: 'Compiled' },
|
|
288
|
+
'cloudflare-worker': { command: 'pnpm wrangler dev', port: 8787, ready: 'Ready' },
|
|
289
|
+
};
|
|
290
|
+
const defaults = result.framework ? frameworkDefaults[result.framework] : null;
|
|
291
|
+
result.suggestedDevCommand = defaults?.command || `${result.packageManager} dev`;
|
|
292
|
+
result.suggestedPort = defaults?.port || 3000;
|
|
293
|
+
result.suggestedReadyPattern = defaults?.ready || 'ready|started|listening|Local:';
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Check if a file exists
|
|
297
|
+
*/
|
|
298
|
+
async function hasFile(dir, file) {
|
|
299
|
+
try {
|
|
300
|
+
await fs.access(path.join(dir, file));
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Scanning & Validation
|
|
3
|
+
*
|
|
4
|
+
* Detects potential hardcoded secrets in configuration files
|
|
5
|
+
* before they can be committed to version control.
|
|
6
|
+
*
|
|
7
|
+
* Based on patterns from:
|
|
8
|
+
* - GitHub secret scanning
|
|
9
|
+
* - detect-secrets
|
|
10
|
+
* - truffleHog
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Detected secret finding
|
|
14
|
+
*/
|
|
15
|
+
export interface SecretFinding {
|
|
16
|
+
pattern: string;
|
|
17
|
+
description: string;
|
|
18
|
+
severity: 'high' | 'medium' | 'low';
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
match: string;
|
|
22
|
+
file: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scan content for potential secrets
|
|
26
|
+
*/
|
|
27
|
+
export declare function scanForSecrets(content: string, filename: string): SecretFinding[];
|
|
28
|
+
/**
|
|
29
|
+
* Scan a file for secrets
|
|
30
|
+
*/
|
|
31
|
+
export declare function scanFile(filePath: string): Promise<SecretFinding[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Scan .haystack.yml specifically for secrets
|
|
34
|
+
*/
|
|
35
|
+
export declare function scanHaystackConfig(configPath: string): Promise<SecretFinding[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a config doesn't contain hardcoded secrets
|
|
38
|
+
* Returns true if safe, false if secrets detected
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateConfigSecurity(configPath: string): Promise<{
|
|
41
|
+
safe: boolean;
|
|
42
|
+
findings: SecretFinding[];
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Get a security report string for display
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatSecurityReport(findings: SecretFinding[]): string;
|