@programinglive/dev-workflow-mcp-server 1.7.11 ā 1.7.13
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/.dockerignore +60 -0
- package/.env.example +1 -0
- package/.env.production +16 -0
- package/CHANGELOG.md +35 -0
- package/Dockerfile +32 -0
- package/README.md +20 -0
- package/db/migrations/002_create_users_table.sql +22 -0
- package/db/migrations/003_create_workflow_history.sql +14 -0
- package/db/migrations/004_fix_workflow_history.sql +3 -0
- package/docker-compose.yml +24 -0
- package/docs/GCP_DEPLOYMENT.md +298 -0
- package/docs/product/PRD.md +1 -1
- package/docs/release-notes/RELEASE_NOTES.md +34 -0
- package/package.json +7 -2
- package/scripts/add-user.js +52 -0
- package/scripts/create-admin.js +47 -0
- package/scripts/deploy-gcloud.ps1 +160 -0
- package/scripts/deploy-gcloud.sh +154 -0
- package/scripts/health-check.js +50 -0
- package/scripts/test-history.js +56 -0
- package/web/__tests__/api/workflow-history.test.js +61 -0
- package/web/app/api/history-fallback/route.ts +56 -0
- package/web/app/docs/page.tsx +1 -1
- package/web/app/history/page.tsx +213 -0
- package/web/app/layout.tsx +1 -1
- package/web/app/login/page.tsx +144 -0
- package/web/app/page.tsx +0 -2
- package/web/components/Hero.tsx +1 -1
- package/web/components/History.tsx +63 -16
- package/web/components/Navbar.tsx +111 -9
- package/web/lib/auth.js +75 -0
- package/web/middleware/auth.js +26 -0
- package/web/next.config.ts +8 -1
- package/web/package-lock.json +239 -54
- package/web/package.json +10 -5
- package/web/scripts/db-query-history.js +120 -0
- package/web/scripts/smoke-test.js +30 -0
- package/web/server.js +171 -18
- package/verify-db-mysql.js +0 -92
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { Pool } = pg;
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Silence stdout during dotenv loading to prevent noise in API output
|
|
15
|
+
const originalWrite = process.stdout.write;
|
|
16
|
+
process.stdout.write = () => true; // No-op
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Try web/.env
|
|
20
|
+
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
21
|
+
// Try root .env
|
|
22
|
+
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// dotenv might not be available
|
|
25
|
+
} finally {
|
|
26
|
+
// Restore stdout
|
|
27
|
+
process.stdout.write = originalWrite;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultUrl = 'postgres://devworkflow:devworkflow_secure_password_change_me@localhost:5432/devworkflow';
|
|
31
|
+
const connectionString = process.env.DEV_WORKFLOW_DB_URL || defaultUrl;
|
|
32
|
+
const rawSid = process.argv[2]; // Passed from API
|
|
33
|
+
|
|
34
|
+
// DEBUG: output connection details to stderr (so it doesn't break JSON on stdout)
|
|
35
|
+
try {
|
|
36
|
+
const urlStr = connectionString.startsWith('postgres') ? connectionString : `postgres://${connectionString}`; // handle simpler strings if needed
|
|
37
|
+
// Simple heuristic parsing or URL object
|
|
38
|
+
const u = new URL(urlStr);
|
|
39
|
+
console.error(`DEBUG: Connecting to Host: ${u.hostname}, Port: ${u.port}, User: ${u.username}`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('DEBUG: Could not parse connection string for logging');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!connectionString) {
|
|
45
|
+
// Should be unreachable with defaultUrl, but good for safety
|
|
46
|
+
console.error(JSON.stringify({ error: 'Database configuration missing' }));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pool = new Pool({ connectionString });
|
|
51
|
+
|
|
52
|
+
async function run() {
|
|
53
|
+
try {
|
|
54
|
+
let userId = null;
|
|
55
|
+
|
|
56
|
+
// 1. Validate Session if provided
|
|
57
|
+
if (rawSid) {
|
|
58
|
+
// Parse 's:session_id.signature' -> 'session_id'
|
|
59
|
+
// Simplified logic: between 's:' and '.'
|
|
60
|
+
let sid = rawSid;
|
|
61
|
+
if (rawSid.startsWith('s:')) {
|
|
62
|
+
sid = rawSid.slice(2, rawSid.lastIndexOf('.'));
|
|
63
|
+
} else if (rawSid.includes('.')) {
|
|
64
|
+
// heuristic for signed cookie without s: prefix?
|
|
65
|
+
sid = rawSid.slice(0, rawSid.lastIndexOf('.'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try lookup
|
|
69
|
+
const sessionRes = await pool.query("SELECT sess FROM session WHERE sid = $1", [sid]);
|
|
70
|
+
if (sessionRes.rows.length > 0) {
|
|
71
|
+
const sessionData = sessionRes.rows[0].sess;
|
|
72
|
+
if (sessionData && sessionData.userId) {
|
|
73
|
+
userId = sessionData.userId;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Fallback if no valid session found (for dev/recovery comfort)
|
|
79
|
+
if (!userId) {
|
|
80
|
+
// Try admin
|
|
81
|
+
const userRes = await pool.query("SELECT id FROM users WHERE LOWER(username) = 'admin'");
|
|
82
|
+
if (userRes.rows.length > 0) {
|
|
83
|
+
userId = userRes.rows[0].id;
|
|
84
|
+
} else {
|
|
85
|
+
const anyUser = await pool.query("SELECT id FROM users LIMIT 1");
|
|
86
|
+
if (anyUser.rows.length > 0) {
|
|
87
|
+
userId = anyUser.rows[0].id;
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error('No user found');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. (Self-healing removed as it was causing schema errors)
|
|
95
|
+
|
|
96
|
+
const result = await pool.query(`
|
|
97
|
+
SELECT
|
|
98
|
+
id,
|
|
99
|
+
task_type,
|
|
100
|
+
description,
|
|
101
|
+
commit_message,
|
|
102
|
+
completed_at,
|
|
103
|
+
tests_passed,
|
|
104
|
+
documentation_type
|
|
105
|
+
FROM workflow_history
|
|
106
|
+
WHERE user_id = $1
|
|
107
|
+
ORDER BY completed_at DESC LIMIT 100
|
|
108
|
+
`, [userId]);
|
|
109
|
+
|
|
110
|
+
console.log(JSON.stringify({ history: result.rows }));
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(JSON.stringify({ error: error.message, stack: error.stack }));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
} finally {
|
|
116
|
+
await pool.end();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
run();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const rootDir = path.join(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
console.log('Running smoke tests...');
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// 1. Verify package.json type: module
|
|
13
|
+
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
14
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
15
|
+
if (packageJson.type !== 'module') {
|
|
16
|
+
throw new Error('FAIL: package.json must have "type": "module"');
|
|
17
|
+
}
|
|
18
|
+
console.log('ā
package.json has "type": "module"');
|
|
19
|
+
|
|
20
|
+
// 2. Syntax check server.js
|
|
21
|
+
const serverJsPath = path.join(rootDir, 'server.js');
|
|
22
|
+
execSync(`node -c "${serverJsPath}"`, { stdio: 'inherit' });
|
|
23
|
+
console.log('ā
server.js syntax check passed');
|
|
24
|
+
|
|
25
|
+
console.log('All smoke tests passed!');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('ā Smoke test failed:', error.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
package/web/server.js
CHANGED
|
@@ -1,31 +1,184 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { parse } from 'url';
|
|
3
|
+
import next from 'next';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import session from 'express-session';
|
|
6
|
+
import connectPgSimple from 'connect-pg-simple';
|
|
7
|
+
import pg from 'pg';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// Load env from root
|
|
16
|
+
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
|
17
|
+
|
|
18
|
+
const pgSession = connectPgSimple(session);
|
|
19
|
+
const { Pool } = pg;
|
|
4
20
|
|
|
5
21
|
const dev = process.env.NODE_ENV !== 'production';
|
|
6
22
|
const hostname = 'localhost';
|
|
7
23
|
const port = process.env.PORT || 3000;
|
|
8
24
|
|
|
25
|
+
// Initialize PostgreSQL pool for sessions
|
|
26
|
+
const dbUrl = process.env.DEV_WORKFLOW_DB_URL;
|
|
27
|
+
const pool = dbUrl ? new Pool({ connectionString: dbUrl }) : null;
|
|
28
|
+
|
|
9
29
|
// Initialize Next.js app
|
|
10
30
|
const app = next({ dev, hostname, port });
|
|
11
31
|
const handle = app.getRequestHandler();
|
|
12
32
|
|
|
33
|
+
// Import auth utilities
|
|
34
|
+
let authenticateUser, getUserById;
|
|
35
|
+
if (pool) {
|
|
36
|
+
import('./lib/auth.js').then(module => {
|
|
37
|
+
authenticateUser = module.authenticateUser;
|
|
38
|
+
getUserById = module.getUserById;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
app.prepare().then(() => {
|
|
14
|
-
|
|
43
|
+
const server = express();
|
|
44
|
+
|
|
45
|
+
server.use(express.json());
|
|
46
|
+
server.use(express.urlencoded({ extended: true }));
|
|
47
|
+
|
|
48
|
+
// Session middleware
|
|
49
|
+
if (pool) {
|
|
50
|
+
server.use(
|
|
51
|
+
session({
|
|
52
|
+
store: new pgSession({
|
|
53
|
+
pool,
|
|
54
|
+
tableName: 'session'
|
|
55
|
+
}),
|
|
56
|
+
secret: process.env.SESSION_SECRET || 'dev-workflow-secret-change-in-production',
|
|
57
|
+
resave: false,
|
|
58
|
+
saveUninitialized: false,
|
|
59
|
+
cookie: {
|
|
60
|
+
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
61
|
+
httpOnly: true,
|
|
62
|
+
secure: process.env.NODE_ENV === 'production'
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Auth API routes
|
|
69
|
+
server.post('/api/auth/login', async (req, res) => {
|
|
70
|
+
if (!pool || !authenticateUser) {
|
|
71
|
+
return res.status(500).json({ error: 'Database not configured' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { username, password } = req.body;
|
|
75
|
+
|
|
76
|
+
if (!username || !password) {
|
|
77
|
+
return res.status(400).json({ error: 'Username and password required' });
|
|
78
|
+
}
|
|
79
|
+
|
|
15
80
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
const user = await authenticateUser(pool, username, password);
|
|
82
|
+
|
|
83
|
+
if (!user) {
|
|
84
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
req.session.userId = user.id;
|
|
88
|
+
req.session.username = user.username;
|
|
89
|
+
|
|
90
|
+
res.json({ success: true, user: { username: user.username, email: user.email } });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Login error:', error);
|
|
93
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
server.post('/api/auth/logout', (req, res) => {
|
|
98
|
+
req.session.destroy((err) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
return res.status(500).json({ error: 'Logout failed' });
|
|
101
|
+
}
|
|
102
|
+
res.json({ success: true });
|
|
30
103
|
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.get('/api/auth/me', async (req, res) => {
|
|
107
|
+
if (!req.session || !req.session.userId) {
|
|
108
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!pool || !getUserById) {
|
|
112
|
+
return res.status(500).json({ error: 'Database not configured' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const user = await getUserById(pool, req.session.userId);
|
|
117
|
+
|
|
118
|
+
if (!user) {
|
|
119
|
+
return res.status(404).json({ error: 'User not found' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.json({ user: { username: user.username, email: user.email } });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Get user error:', error);
|
|
125
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Get workflow history
|
|
130
|
+
server.get('/api/workflow/history', async (req, res) => {
|
|
131
|
+
if (!req.session || !req.session.userId) {
|
|
132
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!pool) {
|
|
136
|
+
return res.status(500).json({ error: 'Database not configured' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Get user details to filter history
|
|
141
|
+
// We assume workflow_history.user_id stores the username (based on schema)
|
|
142
|
+
// We use userId (integer) to query the foreign key column
|
|
143
|
+
const userId = req.session.userId;
|
|
144
|
+
|
|
145
|
+
let query = `
|
|
146
|
+
SELECT
|
|
147
|
+
id,
|
|
148
|
+
task_type,
|
|
149
|
+
task_description as description,
|
|
150
|
+
commit_message,
|
|
151
|
+
timestamp as completed_at,
|
|
152
|
+
tests_passed,
|
|
153
|
+
documentation_type
|
|
154
|
+
FROM workflow_history
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const params = [];
|
|
158
|
+
|
|
159
|
+
if (userId) {
|
|
160
|
+
query += ` WHERE user_id = $1 `;
|
|
161
|
+
params.push(userId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
query += ` ORDER BY timestamp DESC LIMIT 100`;
|
|
165
|
+
|
|
166
|
+
const result = await pool.query(query, params);
|
|
167
|
+
|
|
168
|
+
res.json({ history: result.rows });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Get history error:', error);
|
|
171
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Next.js handler for all other routes
|
|
176
|
+
server.all('*', (req, res) => {
|
|
177
|
+
return handle(req, res);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
server.listen(port, (err) => {
|
|
181
|
+
if (err) throw err;
|
|
182
|
+
console.log(`> Ready on http://${hostname}:${port}`);
|
|
183
|
+
});
|
|
31
184
|
});
|
package/verify-db-mysql.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { MysqlAdapter } from "./db/adapters/mysql.js";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
|
|
5
|
-
const CONNECTION_URL = process.env.DEV_WORKFLOW_DB_URL;
|
|
6
|
-
|
|
7
|
-
if (!CONNECTION_URL) {
|
|
8
|
-
console.error("ā Error: DEV_WORKFLOW_DB_URL environment variable is required.");
|
|
9
|
-
console.error("Usage: DEV_WORKFLOW_DB_URL='mysql://user:password@host:port/database' node verify-db-mysql.js");
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const TEST_USER = "test-verification-user";
|
|
14
|
-
const TEST_PROJECT = resolve(process.cwd());
|
|
15
|
-
|
|
16
|
-
async function runVerification() {
|
|
17
|
-
console.log("š Starting MySQL Verification...");
|
|
18
|
-
console.log(` User ID: ${TEST_USER}`);
|
|
19
|
-
console.log(` Project: ${TEST_PROJECT}`);
|
|
20
|
-
console.log(` URL: ${CONNECTION_URL.replace(/:[^:@]+@/, ":****@")}`);
|
|
21
|
-
|
|
22
|
-
const adapter = new MysqlAdapter({
|
|
23
|
-
connectionUrl: CONNECTION_URL
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// 1. Connect & Migrate
|
|
28
|
-
console.log("\n1. Connecting and Migrating...");
|
|
29
|
-
await adapter.connect();
|
|
30
|
-
console.log(" ā
Connected to MySQL");
|
|
31
|
-
|
|
32
|
-
// 2. Clear previous test data (optional, but good for cleanliness)
|
|
33
|
-
// We won't strictly clear to avoid wiping valid data if user reuses IDs,
|
|
34
|
-
// but the test user should be unique enough.
|
|
35
|
-
|
|
36
|
-
// 3. Insert History
|
|
37
|
-
console.log("\n3. Testing History Insertion...");
|
|
38
|
-
const entry = {
|
|
39
|
-
taskDescription: "Verification Task",
|
|
40
|
-
taskType: "test",
|
|
41
|
-
timestamp: new Date().toISOString(),
|
|
42
|
-
commitMessage: "test commit"
|
|
43
|
-
};
|
|
44
|
-
await adapter.insertHistoryEntry(TEST_USER, TEST_PROJECT, entry);
|
|
45
|
-
console.log(" ā
Inserted history entry");
|
|
46
|
-
|
|
47
|
-
// 4. Verify History Retrieval
|
|
48
|
-
console.log("\n4. Verifying History Retrieval...");
|
|
49
|
-
const history = await adapter.getHistoryForUser(TEST_USER, TEST_PROJECT, { pageSize: 1 });
|
|
50
|
-
const savedEntry = history.entries[0];
|
|
51
|
-
|
|
52
|
-
if (savedEntry && savedEntry.task_description === entry.taskDescription) {
|
|
53
|
-
console.log(" ā
Retrieved history entry matches");
|
|
54
|
-
} else {
|
|
55
|
-
console.error(" ā Retrieved entry mismatch:", savedEntry);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 5. Test State Persistence
|
|
60
|
-
console.log("\n5. Testing State Persistence...");
|
|
61
|
-
const state = {
|
|
62
|
-
currentPhase: "verification",
|
|
63
|
-
verified: true,
|
|
64
|
-
random: Math.random()
|
|
65
|
-
};
|
|
66
|
-
await adapter.saveState(TEST_USER, TEST_PROJECT, state);
|
|
67
|
-
console.log(" ā
Saved state");
|
|
68
|
-
|
|
69
|
-
// 6. Verify State Retrieval
|
|
70
|
-
console.log("\n6. Verifying State Retrieval...");
|
|
71
|
-
const retrievedState = await adapter.getState(TEST_USER, TEST_PROJECT);
|
|
72
|
-
if (retrievedState && retrievedState.random === state.random) {
|
|
73
|
-
console.log(" ā
Retrieved state matches");
|
|
74
|
-
} else {
|
|
75
|
-
console.error(" ā Retrieved state mismatch:", retrievedState);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
console.log("\nš MySQL Verification SUCCESS!");
|
|
80
|
-
process.exit(0);
|
|
81
|
-
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error("\nā Verification FAILED:", error);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
} finally {
|
|
86
|
-
if (adapter.pool) {
|
|
87
|
-
await adapter.pool.end();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
runVerification();
|