@evolve.labs/devflow 0.8.3 → 0.9.1

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/lib/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const path = require('node:path');
2
2
 
3
- const VERSION = '0.8.3';
3
+ const VERSION = '0.9.1';
4
4
 
5
5
  // Root of the installed npm package (where source files live)
6
6
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
package/lib/web.js CHANGED
@@ -1,17 +1,80 @@
1
1
  const { execSync, spawn } = require('node:child_process');
2
2
  const path = require('node:path');
3
3
  const fs = require('node:fs');
4
- const { PACKAGE_ROOT } = require('./constants');
4
+ const os = require('node:os');
5
+ const { PACKAGE_ROOT, VERSION, WEB_COPY_DIRS, WEB_COPY_FILES } = require('./constants');
6
+
7
+ /**
8
+ * When installed via npm, the web/ directory lives inside node_modules.
9
+ * Next.js/SWC does not properly compile TypeScript files within node_modules,
10
+ * so we copy the source to ~/.devflow/_web/ (a normal directory) and run from there.
11
+ */
12
+ function resolveWebDir() {
13
+ const packageWebDir = path.join(PACKAGE_ROOT, 'web');
14
+
15
+ if (!fs.existsSync(packageWebDir)) {
16
+ return null;
17
+ }
18
+
19
+ // If not inside node_modules, use directly (dev mode / cloned repo)
20
+ if (!PACKAGE_ROOT.includes('node_modules')) {
21
+ return packageWebDir;
22
+ }
23
+
24
+ // Running from npm install — copy source to local cache
25
+ const cacheDir = path.join(os.homedir(), '.devflow', '_web');
26
+ const versionFile = path.join(cacheDir, '.devflow-version');
27
+ const cachedVersion = fs.existsSync(versionFile)
28
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
29
+ : '';
30
+
31
+ if (cachedVersion === VERSION && fs.existsSync(path.join(cacheDir, 'package.json'))) {
32
+ return cacheDir;
33
+ }
34
+
35
+ console.log('Setting up web dashboard files...');
36
+
37
+ // Clean previous cache
38
+ if (fs.existsSync(cacheDir)) {
39
+ fs.rmSync(cacheDir, { recursive: true, force: true });
40
+ }
41
+ fs.mkdirSync(cacheDir, { recursive: true });
42
+
43
+ // Copy source directories
44
+ for (const dir of WEB_COPY_DIRS) {
45
+ const srcDir = path.join(PACKAGE_ROOT, dir);
46
+ const destDir = path.join(cacheDir, dir.replace(/^web\//, ''));
47
+ if (fs.existsSync(srcDir)) {
48
+ fs.cpSync(srcDir, destDir, { recursive: true });
49
+ }
50
+ }
51
+
52
+ // Copy individual files
53
+ for (const file of WEB_COPY_FILES) {
54
+ const srcFile = path.join(PACKAGE_ROOT, file);
55
+ const destFile = path.join(cacheDir, file.replace(/^web\//, ''));
56
+ if (fs.existsSync(srcFile)) {
57
+ const destDir = path.dirname(destFile);
58
+ fs.mkdirSync(destDir, { recursive: true });
59
+ fs.copyFileSync(srcFile, destFile);
60
+ }
61
+ }
62
+
63
+ // Write version marker
64
+ fs.writeFileSync(versionFile, VERSION);
65
+
66
+ return cacheDir;
67
+ }
5
68
 
6
69
  /**
7
70
  * devflow web - Start the DevFlow Web Dashboard
8
71
  */
9
72
  async function webCommand(options) {
10
73
  const port = options.port || '3000';
11
- const webDir = path.join(PACKAGE_ROOT, 'web');
74
+ const webDir = resolveWebDir();
12
75
 
13
76
  // 1. Verify web/ directory exists
14
- if (!fs.existsSync(webDir)) {
77
+ if (!webDir) {
15
78
  console.error('Error: Web IDE files not found.');
16
79
  console.error('Re-install devflow with: npm install -g @evolve.labs/devflow');
17
80
  process.exit(1);
@@ -49,15 +112,27 @@ async function webCommand(options) {
49
112
  process.exit(1);
50
113
  }
51
114
 
52
- // 5. Start the web server
115
+ // 5. Validate port
116
+ const portNum = parseInt(port, 10);
117
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
118
+ console.error(`Invalid port: ${port}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ // 6. Start the web server
53
123
  const isDev = options.dev || false;
54
124
  const cmd = isDev ? 'dev' : 'start';
55
125
 
56
- // Build first if not dev mode and .next doesn't exist
126
+ // Build first if not dev mode and no valid production build exists
57
127
  if (!isDev) {
58
- const nextDir = path.join(webDir, '.next');
59
- if (!fs.existsSync(nextDir)) {
60
- console.log('Building web dashboard...');
128
+ const buildIdPath = path.join(webDir, '.next', 'BUILD_ID');
129
+ if (!fs.existsSync(buildIdPath)) {
130
+ console.log('Building web dashboard (first run, may take a minute)...');
131
+ // Remove stale .next directory if it exists without BUILD_ID
132
+ const nextDir = path.join(webDir, '.next');
133
+ if (fs.existsSync(nextDir)) {
134
+ fs.rmSync(nextDir, { recursive: true, force: true });
135
+ }
61
136
  try {
62
137
  execSync('npx next build --webpack', {
63
138
  cwd: webDir,
@@ -91,12 +166,12 @@ async function webCommand(options) {
91
166
  // Open browser after a short delay (unless --no-open)
92
167
  if (options.open !== false) {
93
168
  setTimeout(() => {
94
- const url = `http://localhost:${port}`;
169
+ const url = `http://localhost:${portNum}`;
95
170
  try {
96
171
  const openCmd = process.platform === 'darwin' ? 'open'
97
172
  : process.platform === 'win32' ? 'start'
98
173
  : 'xdg-open';
99
- execSync(`${openCmd} ${url}`, { stdio: 'ignore' });
174
+ execSync(`${openCmd} ${JSON.stringify(url)}`, { stdio: 'ignore' });
100
175
  } catch {
101
176
  console.log(`Open your browser at: ${url}`);
102
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolve.labs/devflow",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Multi-agent system for software development with Claude Code. 6 specialized agents (Strategist, Architect, System Designer, Builder, Guardian, Chronicler) as slash commands.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -42,6 +42,12 @@ export async function POST(req: NextRequest) {
42
42
  return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
43
43
  }
44
44
 
45
+ // Sanitize sessionId to prevent path traversal
46
+ const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
47
+ if (!safeSessionId || safeSessionId !== sessionId) {
48
+ return NextResponse.json({ error: 'Invalid sessionId format' }, { status: 400 });
49
+ }
50
+
45
51
  if (!isValidAgent(agent)) {
46
52
  return NextResponse.json({ error: `Unknown agent: ${agent}` }, { status: 400 });
47
53
  }
@@ -66,10 +72,10 @@ export async function POST(req: NextRequest) {
66
72
 
67
73
  // Write prompt to temp file (avoid shell escaping issues)
68
74
  const tmpDir = os.tmpdir();
69
- const tmpFile = path.join(tmpDir, `devflow-autopilot-${sessionId}-${agent}-${Date.now()}.md`);
75
+ const tmpFile = path.join(tmpDir, `devflow-autopilot-${safeSessionId}-${agent}-${Date.now()}.md`);
70
76
 
71
77
  try {
72
- await fs.writeFile(tmpFile, fullPrompt, 'utf-8');
78
+ await fs.writeFile(tmpFile, fullPrompt, { encoding: 'utf-8', mode: 0o600 });
73
79
 
74
80
  // Arm the collector before writing the command
75
81
  const collectorPromise = ptyManager.armAutopilotCollector(sessionId, timeoutMs + 5000);
@@ -1,5 +1,6 @@
1
1
  import * as pty from 'node-pty';
2
2
  import { EventEmitter } from 'events';
3
+ import { existsSync } from 'fs';
3
4
  import { PHASE_DONE_REGEX } from '@/lib/autopilotConstants';
4
5
 
5
6
  interface TerminalSession {
@@ -16,21 +17,42 @@ interface AutopilotCollector {
16
17
  timeout: NodeJS.Timeout;
17
18
  }
18
19
 
20
+ /**
21
+ * Detect the best available shell for the current platform.
22
+ */
23
+ function detectShell(): string {
24
+ if (process.platform === 'win32') return 'powershell.exe';
25
+
26
+ // Try SHELL env var first
27
+ const envShell = process.env.SHELL;
28
+ if (envShell && existsSync(envShell)) return envShell;
29
+
30
+ // Fallback chain for Unix-like systems
31
+ const candidates = ['/bin/zsh', '/bin/bash', '/bin/sh'];
32
+ for (const candidate of candidates) {
33
+ if (existsSync(candidate)) return candidate;
34
+ }
35
+
36
+ return '/bin/sh';
37
+ }
38
+
19
39
  class PtyManager extends EventEmitter {
20
40
  private sessions: Map<string, TerminalSession> = new Map();
21
41
  private outputBuffers: Map<string, string[]> = new Map();
22
42
  private autopilotCollectors: Map<string, AutopilotCollector> = new Map();
23
43
 
24
44
  createSession(id: string, cwd: string, cols: number = 80, rows: number = 24): TerminalSession {
25
- // Determine shell based on platform
26
- const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/zsh';
45
+ const shell = detectShell();
27
46
  const shellArgs = process.platform === 'win32' ? [] : ['-l'];
28
47
 
48
+ // Validate cwd exists, fallback to home directory
49
+ const safeCwd = existsSync(cwd) ? cwd : process.env.HOME || '/tmp';
50
+
29
51
  const ptyProcess = pty.spawn(shell, shellArgs, {
30
52
  name: 'xterm-256color',
31
53
  cols,
32
54
  rows,
33
- cwd,
55
+ cwd: safeCwd,
34
56
  env: {
35
57
  ...process.env,
36
58
  TERM: 'xterm-256color',
@@ -1,3 +1,5 @@
1
+ const path = require('path');
2
+
1
3
  /** @type {import('next').NextConfig} */
2
4
  const nextConfig = {
3
5
  // Enable React strict mode for better development experience
@@ -14,8 +16,14 @@ const nextConfig = {
14
16
  // Exclude native modules from client-side bundling
15
17
  serverExternalPackages: ['node-pty'],
16
18
 
17
- // Webpack config for native modules
19
+ // Webpack config for native modules and path resolution
18
20
  webpack: (config, { isServer }) => {
21
+ // Ensure @/ alias resolves correctly even when running from node_modules
22
+ config.resolve.alias = {
23
+ ...config.resolve.alias,
24
+ '@': path.resolve(__dirname),
25
+ };
26
+
19
27
  if (!isServer) {
20
28
  // Don't bundle node-pty on client side
21
29
  config.resolve.fallback = {
package/web/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-ide",
3
- "version": "0.7.0",
3
+ "version": "0.9.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "./node_modules/.bin/next dev",
@@ -36,8 +36,6 @@
36
36
  "simple-git": "^3.30.0",
37
37
  "sonner": "^2.0.7",
38
38
  "tailwind-merge": "^2.5.5",
39
- "xterm": "^5.3.0",
40
- "xterm-addon-fit": "^0.8.0",
41
39
  "zustand": "^5.0.2"
42
40
  },
43
41
  "devDependencies": {