@programinglive/dev-workflow-mcp-server 1.8.1 → 1.10.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/.env.example +21 -1
- package/AGENTS.md +25 -0
- package/README.md +372 -649
- package/factory-client.js +554 -0
- package/git-facade.js +35 -0
- package/index.js +9 -34
- package/package.json +21 -20
- package/scripts/bootstrap-unix.sh +55 -0
- package/scripts/bootstrap-windows.ps1 +75 -0
- package/scripts/install-husky.js +43 -0
- package/scripts/setup-stack.js +436 -0
- package/scripts/setup-test-dbs.sh +61 -0
- package/scripts/test-db-adapters.sh +71 -0
- package/scripts/verify-bundle.mjs +92 -0
- package/tools/definitions.js +143 -0
- package/tools/handler-modules/commit-handlers.js +2 -1
- package/tools/handler-modules/factory-handlers.js +276 -0
- package/tools/handler-modules/index.js +24 -7
- package/tools/handler-modules/shared.js +7 -41
- package/tools/handler-modules/status-handlers.js +25 -3
- package/tools/handler-modules/workflow-handlers.js +57 -10
- package/vite.config.js +6 -0
- package/workflow-state.js +176 -2
- package/.agent/workflows/dev-workflow-bugfix.md +0 -250
- package/.agent/workflows/dev-workflow-feature.md +0 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
- package/.agent/workflows/dev-workflow-refactor.md +0 -238
- package/.env.production +0 -16
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/pull_request_template.md +0 -22
- package/.github/workflows/github-release.yml +0 -131
- package/.husky/commit-msg +0 -3
- package/.husky/pre-commit +0 -2
- package/CHANGELOG.md +0 -1145
- package/docs/GCP_DEPLOYMENT.md +0 -298
- package/docs/NETLIFY_DEPLOYMENT.md +0 -125
- package/docs/PLESK_DEPLOYMENT.md +0 -170
- package/docs/PRD.md +0 -86
- package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
- package/docs/antigravity/EXAMPLES.md +0 -535
- package/docs/antigravity/GETTING-STARTED.md +0 -292
- package/docs/antigravity/TROUBLESHOOTING.md +0 -742
- package/docs/antigravity/WALKTHROUGH.md +0 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
- package/docs/release-notes/RELEASE_NOTES.md +0 -524
- package/docs/web-dashboard.md +0 -175
- package/scripts/test-auth-flow.js +0 -75
- package/tailwind.config.js +0 -8
- package/tests/build.test.js +0 -74
- package/tests/db-adapters.test.js +0 -108
- package/tests/env-support.test.js +0 -46
- package/tests/exec.test.js +0 -35
- package/tests/handlers-commit.test.js +0 -174
- package/tests/handlers-release.test.js +0 -212
- package/tests/handlers-workflow.test.js +0 -68
- package/tests/handlers.test.js +0 -1142
- package/tests/hello-world.test.js +0 -13
- package/tests/mcpRegistry.test.js +0 -65
- package/tests/patch-release.test.js +0 -7
- package/tests/release-wrapper.test.js +0 -107
- package/tests/startup-resilience.test.js +0 -73
- package/tests/test-helpers.js +0 -50
- package/tests/workflowState.test.js +0 -520
- package/web/Dockerfile.api +0 -25
- package/web/README.md +0 -133
- package/web/__tests__/api/workflow-history.test.js +0 -63
- package/web/api-server.js +0 -211
- package/web/app/docs/page.tsx +0 -202
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +0 -91
- package/web/app/history/page.tsx +0 -220
- package/web/app/layout.tsx +0 -39
- package/web/app/login/page.tsx +0 -142
- package/web/app/page.tsx +0 -13
- package/web/components/ApiReference.tsx +0 -20
- package/web/components/Features.tsx +0 -61
- package/web/components/Footer.tsx +0 -9
- package/web/components/Hero.tsx +0 -96
- package/web/components/History.tsx +0 -175
- package/web/components/Navbar.tsx +0 -208
- package/web/components/Terminal.tsx +0 -114
- package/web/components/ThemeProvider.tsx +0 -58
- package/web/eslint.config.mjs +0 -18
- package/web/lib/api-client.ts +0 -123
- package/web/lib/auth.js +0 -75
- package/web/lib/db.js +0 -131
- package/web/lib/docs.js +0 -73
- package/web/middleware/auth.js +0 -26
- package/web/netlify.toml +0 -8
- package/web/next.config.ts +0 -19
- package/web/package-lock.json +0 -10911
- package/web/package.json +0 -42
- package/web/postcss.config.mjs +0 -9
- package/web/public/assets/android-chrome-192x192.png +0 -0
- package/web/public/assets/android-chrome-512x512.png +0 -0
- package/web/public/assets/apple-touch-icon.png +0 -0
- package/web/public/assets/favicon-16x16.png +0 -0
- package/web/public/assets/favicon-32x32.png +0 -0
- package/web/public/assets/favicon.ico +0 -0
- package/web/public/assets/favicon.png +0 -0
- package/web/public/assets/logo.png +0 -0
- package/web/public/assets/og-preview.png +0 -0
- package/web/public/assets/site.webmanifest +0 -1
- package/web/public/file.svg +0 -1
- package/web/public/globe.svg +0 -1
- package/web/public/next.svg +0 -1
- package/web/public/vercel.svg +0 -1
- package/web/public/window.svg +0 -1
- package/web/scripts/db-query-history.js +0 -120
- package/web/scripts/smoke-test.js +0 -30
- package/web/server.js +0 -184
- package/web/tailwind.config.ts +0 -20
- package/web/tsconfig.json +0 -42
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
mkdtemp,
|
|
5
|
-
rm,
|
|
6
|
-
mkdir,
|
|
7
|
-
realpath,
|
|
8
|
-
readFile,
|
|
9
|
-
writeFile,
|
|
10
|
-
access,
|
|
11
|
-
copyFile,
|
|
12
|
-
} from "node:fs/promises";
|
|
13
|
-
import { constants as fsConstants } from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
-
|
|
18
|
-
// Isolate these tests from the real database configuration
|
|
19
|
-
delete process.env.DEV_WORKFLOW_DB_TYPE;
|
|
20
|
-
delete process.env.DEV_WORKFLOW_DB_URL;
|
|
21
|
-
delete process.env.TEST_MYSQL_URL;
|
|
22
|
-
delete process.env.TEST_POSTGRES_URL;
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
WorkflowState,
|
|
26
|
-
getNextStep,
|
|
27
|
-
containsTestFilesInStatus,
|
|
28
|
-
createCommitMessageParts,
|
|
29
|
-
determineReleaseTypeFromCommit,
|
|
30
|
-
workingTreeSummary,
|
|
31
|
-
} from "../index.js";
|
|
32
|
-
|
|
33
|
-
async function withWorkflowState(callback) {
|
|
34
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
|
|
35
|
-
const stateFile = path.join(tempDir, "state.json");
|
|
36
|
-
const state = new WorkflowState(stateFile);
|
|
37
|
-
await state.load();
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await callback(state);
|
|
41
|
-
} finally {
|
|
42
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
test("resolveStateFile prefers current working directory when multiple project markers exist", async () => {
|
|
47
|
-
const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-prefers-cwd-"));
|
|
48
|
-
const moduleDir = path.join(tempRoot, "module");
|
|
49
|
-
const projectA = path.join(tempRoot, "project-a");
|
|
50
|
-
const projectB = path.join(tempRoot, "project-b");
|
|
51
|
-
const projectBNested = path.join(projectB, "sub", "app");
|
|
52
|
-
|
|
53
|
-
await mkdir(moduleDir, { recursive: true });
|
|
54
|
-
await mkdir(projectA, { recursive: true });
|
|
55
|
-
await mkdir(projectBNested, { recursive: true });
|
|
56
|
-
|
|
57
|
-
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
58
|
-
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
59
|
-
|
|
60
|
-
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
61
|
-
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
62
|
-
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
63
|
-
await writeFile(path.join(projectA, "package.json"), JSON.stringify({ name: "project-a" }));
|
|
64
|
-
await writeFile(path.join(projectB, "package.json"), JSON.stringify({ name: "project-b" }));
|
|
65
|
-
|
|
66
|
-
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
67
|
-
const { WorkflowState: TempWorkflowState } = await import(`${moduleUrl.href}?preferCwd=${Date.now()}`);
|
|
68
|
-
|
|
69
|
-
const originalCwd = process.cwd();
|
|
70
|
-
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
71
|
-
const originalInitCwd = process.env.INIT_CWD;
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
process.chdir(projectBNested);
|
|
75
|
-
process.env.INIT_CWD = projectA;
|
|
76
|
-
|
|
77
|
-
const state = new TempWorkflowState();
|
|
78
|
-
const resolvedStateFile = state.stateFile;
|
|
79
|
-
const resolvedProjectA = await realpath(projectA);
|
|
80
|
-
const resolvedProjectB = await realpath(projectB);
|
|
81
|
-
|
|
82
|
-
assert.ok(
|
|
83
|
-
resolvedStateFile.startsWith(path.join(resolvedProjectB, ".state", "users")),
|
|
84
|
-
"State file should resolve under the current working directory's project"
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
assert.ok(
|
|
88
|
-
!resolvedStateFile.startsWith(path.join(resolvedProjectA, ".state")),
|
|
89
|
-
"State file should not use INIT_CWD project when cwd has its own markers"
|
|
90
|
-
);
|
|
91
|
-
} finally {
|
|
92
|
-
process.chdir(originalCwd);
|
|
93
|
-
if (initWasSet) {
|
|
94
|
-
process.env.INIT_CWD = originalInitCwd;
|
|
95
|
-
} else {
|
|
96
|
-
delete process.env.INIT_CWD;
|
|
97
|
-
}
|
|
98
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("WorkflowState mirrors state file to compatibility locations", async () => {
|
|
103
|
-
const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-compat-"));
|
|
104
|
-
const moduleDir = path.join(tempRoot, "module");
|
|
105
|
-
const projectRoot = path.join(tempRoot, "project");
|
|
106
|
-
const stateDir = path.join(projectRoot, ".state");
|
|
107
|
-
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
108
|
-
await mkdir(moduleDir, { recursive: true });
|
|
109
|
-
await mkdir(path.join(moduleDir, "dist"), { recursive: true });
|
|
110
|
-
await mkdir(path.join(tempRoot, "dist"), { recursive: true });
|
|
111
|
-
await mkdir(stateDir, { recursive: true });
|
|
112
|
-
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
113
|
-
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
114
|
-
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
115
|
-
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
116
|
-
await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
|
|
117
|
-
|
|
118
|
-
const stateFile = path.join(stateDir, "workflow-state.json");
|
|
119
|
-
await writeFile(stateFile, JSON.stringify({ currentPhase: "testing" }, null, 2));
|
|
120
|
-
|
|
121
|
-
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
122
|
-
const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
|
|
123
|
-
|
|
124
|
-
const state = new TempWorkflowState(stateFile);
|
|
125
|
-
await state.load();
|
|
126
|
-
state.state.currentPhase = "commit";
|
|
127
|
-
await state.save();
|
|
128
|
-
|
|
129
|
-
// Ensure primary state file was written correctly
|
|
130
|
-
const primaryContent = await readFile(stateFile, "utf-8");
|
|
131
|
-
const primaryParsed = JSON.parse(primaryContent);
|
|
132
|
-
assert.equal(primaryParsed.currentPhase, "commit", "primary state file should be updated");
|
|
133
|
-
|
|
134
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("WorkflowState avoids filesystem root when no project markers found", async () => {
|
|
138
|
-
const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-rootless-"));
|
|
139
|
-
const moduleDir = path.join(tempRoot, "module");
|
|
140
|
-
await mkdir(moduleDir, { recursive: true });
|
|
141
|
-
|
|
142
|
-
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
143
|
-
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
144
|
-
await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module", type: "module" }));
|
|
145
|
-
await mkdir(path.join(moduleDir, "db"), { recursive: true });
|
|
146
|
-
await writeFile(path.join(moduleDir, "db", "index.js"), 'export function getDbAdapter() { return { connect: async () => {}, saveState: async () => {}, getState: async () => null }; } export async function insertHistoryEntry() {} export async function updateSummaryForUser() {} export async function saveState() {} export async function getState() {} export async function getSummaryForUser() { return { totalTasks: 0, taskTypes: {} }; } export async function getHistoryForUser() { return { entries: [], total: 0 }; }');
|
|
147
|
-
|
|
148
|
-
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
149
|
-
const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
|
|
150
|
-
|
|
151
|
-
const originalCwd = process.cwd();
|
|
152
|
-
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
153
|
-
const originalInitCwd = process.env.INIT_CWD;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const cwdWithoutMarkers = path.join(tempRoot, "random", "deep");
|
|
157
|
-
await mkdir(cwdWithoutMarkers, { recursive: true });
|
|
158
|
-
|
|
159
|
-
process.chdir(cwdWithoutMarkers);
|
|
160
|
-
process.env.INIT_CWD = cwdWithoutMarkers;
|
|
161
|
-
|
|
162
|
-
const state = new TempWorkflowState();
|
|
163
|
-
const resolvedTempRoot = await realpath(tempRoot);
|
|
164
|
-
|
|
165
|
-
assert.ok(
|
|
166
|
-
state.stateFile.startsWith(resolvedTempRoot),
|
|
167
|
-
"state file should remain inside the temporary root when no project markers exist"
|
|
168
|
-
);
|
|
169
|
-
assert.ok(
|
|
170
|
-
!state.stateFile.startsWith(path.join(path.sep, ".state")),
|
|
171
|
-
"state file should not resolve to filesystem root"
|
|
172
|
-
);
|
|
173
|
-
} finally {
|
|
174
|
-
process.chdir(originalCwd);
|
|
175
|
-
if (initWasSet) {
|
|
176
|
-
process.env.INIT_CWD = originalInitCwd;
|
|
177
|
-
} else {
|
|
178
|
-
delete process.env.INIT_CWD;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("resolveUserId persists generated ID", async () => {
|
|
186
|
-
const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-userid-"));
|
|
187
|
-
await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
|
|
188
|
-
|
|
189
|
-
const originalCwd = process.cwd();
|
|
190
|
-
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
191
|
-
const originalInitCwd = process.env.INIT_CWD;
|
|
192
|
-
const userEnvWasSet = Object.prototype.hasOwnProperty.call(process.env, "DEV_WORKFLOW_USER_ID");
|
|
193
|
-
const originalUserEnv = process.env.DEV_WORKFLOW_USER_ID;
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
process.chdir(projectRoot);
|
|
197
|
-
process.env.INIT_CWD = projectRoot;
|
|
198
|
-
if (userEnvWasSet) {
|
|
199
|
-
delete process.env.DEV_WORKFLOW_USER_ID;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const firstModuleHref = new URL("../workflow-state.js?resolveUserId1", import.meta.url).href;
|
|
203
|
-
const { WorkflowState: FirstWorkflowState } = await import(firstModuleHref);
|
|
204
|
-
|
|
205
|
-
const state = new FirstWorkflowState();
|
|
206
|
-
await state.ensurePrimaryFile();
|
|
207
|
-
|
|
208
|
-
const userDir = path.dirname(state.stateFile);
|
|
209
|
-
const userId = path.basename(userDir);
|
|
210
|
-
assert.ok(userId.startsWith("user-"), "Generated user ID should use user- prefix");
|
|
211
|
-
|
|
212
|
-
const userIdFile = path.join(projectRoot, ".state", "user-id");
|
|
213
|
-
const persistedId = (await readFile(userIdFile, "utf-8")).trim();
|
|
214
|
-
assert.equal(persistedId, userId, "user-id file should match generated user ID");
|
|
215
|
-
|
|
216
|
-
const secondModuleHref = new URL("../workflow-state.js?resolveUserId2", import.meta.url).href;
|
|
217
|
-
const { WorkflowState: SecondWorkflowState } = await import(secondModuleHref);
|
|
218
|
-
|
|
219
|
-
const reloadedState = new SecondWorkflowState();
|
|
220
|
-
await reloadedState.ensurePrimaryFile();
|
|
221
|
-
|
|
222
|
-
assert.equal(reloadedState.stateFile, state.stateFile, "State file path should persist across reloads");
|
|
223
|
-
const reloadedUserId = path.basename(path.dirname(reloadedState.stateFile));
|
|
224
|
-
assert.equal(reloadedUserId, userId, "Reloaded module should reuse persisted user ID");
|
|
225
|
-
} finally {
|
|
226
|
-
process.chdir(originalCwd);
|
|
227
|
-
if (initWasSet) {
|
|
228
|
-
process.env.INIT_CWD = originalInitCwd;
|
|
229
|
-
} else {
|
|
230
|
-
delete process.env.INIT_CWD;
|
|
231
|
-
}
|
|
232
|
-
if (userEnvWasSet) {
|
|
233
|
-
process.env.DEV_WORKFLOW_USER_ID = originalUserEnv;
|
|
234
|
-
} else {
|
|
235
|
-
delete process.env.DEV_WORKFLOW_USER_ID;
|
|
236
|
-
}
|
|
237
|
-
await rm(projectRoot, { recursive: true, force: true });
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("WorkflowState generates project summary on load and updates on save", async () => {
|
|
242
|
-
await withWorkflowState(async (state) => {
|
|
243
|
-
const summaryPath = path.join(path.dirname(state.stateFile), "project-summary.json");
|
|
244
|
-
|
|
245
|
-
const initialSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
|
|
246
|
-
assert.equal(initialSummary.totalTasks, 0);
|
|
247
|
-
assert.deepEqual(initialSummary.taskTypes, {});
|
|
248
|
-
|
|
249
|
-
state.addToHistory({
|
|
250
|
-
taskDescription: "Seed summary",
|
|
251
|
-
taskType: "feature",
|
|
252
|
-
commitMessage: "feat: seed",
|
|
253
|
-
});
|
|
254
|
-
await state.save();
|
|
255
|
-
|
|
256
|
-
const updatedSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
|
|
257
|
-
assert.equal(updatedSummary.totalTasks, 1);
|
|
258
|
-
assert.equal(updatedSummary.taskTypes.feature, 1);
|
|
259
|
-
assert.equal(updatedSummary.recentTasks[0].description, "Seed summary");
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("WorkflowState persists data across save/load", async () => {
|
|
264
|
-
await withWorkflowState(async (state) => {
|
|
265
|
-
state.state.currentPhase = "testing";
|
|
266
|
-
state.state.taskDescription = "Test persistence";
|
|
267
|
-
state.state.bugFixed = true;
|
|
268
|
-
state.state.testsCreated = true;
|
|
269
|
-
state.state.readyCheckCompleted = true;
|
|
270
|
-
state.state.released = true;
|
|
271
|
-
state.state.releaseCommand = "npm run release";
|
|
272
|
-
state.state.releaseNotes = "Automated release";
|
|
273
|
-
state.state.commitAndPushCompleted = true;
|
|
274
|
-
state.state.lastCommitMessage = "test: persistence";
|
|
275
|
-
state.state.lastPushBranch = "main";
|
|
276
|
-
state.state.testsSkipped = true;
|
|
277
|
-
state.state.testsSkippedReason = "Legacy app uses manual QA";
|
|
278
|
-
await state.save();
|
|
279
|
-
|
|
280
|
-
const reloaded = new WorkflowState(state.stateFile);
|
|
281
|
-
await reloaded.load();
|
|
282
|
-
|
|
283
|
-
assert.equal(reloaded.state.currentPhase, "testing");
|
|
284
|
-
assert.equal(reloaded.state.taskDescription, "Test persistence");
|
|
285
|
-
assert.equal(reloaded.state.bugFixed, true);
|
|
286
|
-
assert.equal(reloaded.state.testsCreated, true);
|
|
287
|
-
assert.equal(reloaded.state.readyCheckCompleted, true);
|
|
288
|
-
assert.equal(reloaded.state.released, true);
|
|
289
|
-
assert.equal(reloaded.state.releaseCommand, "npm run release");
|
|
290
|
-
assert.equal(reloaded.state.releaseNotes, "Automated release");
|
|
291
|
-
assert.equal(reloaded.state.commitAndPushCompleted, true);
|
|
292
|
-
assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
|
|
293
|
-
assert.equal(reloaded.state.lastPushBranch, "main");
|
|
294
|
-
assert.equal(reloaded.state.testsSkipped, true);
|
|
295
|
-
assert.equal(
|
|
296
|
-
reloaded.state.testsSkippedReason,
|
|
297
|
-
"Legacy app uses manual QA"
|
|
298
|
-
);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("WorkflowState resolves state file relative to project root", async () => {
|
|
303
|
-
const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-project-"));
|
|
304
|
-
const originalCwd = process.cwd();
|
|
305
|
-
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
306
|
-
const originalInitCwd = process.env.INIT_CWD;
|
|
307
|
-
const devStateWasSet = Object.prototype.hasOwnProperty.call(
|
|
308
|
-
process.env,
|
|
309
|
-
"DEV_WORKFLOW_STATE_FILE"
|
|
310
|
-
);
|
|
311
|
-
const originalDevState = process.env.DEV_WORKFLOW_STATE_FILE;
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
await mkdir(path.join(projectRoot, ".git"));
|
|
315
|
-
const nestedDir = path.join(projectRoot, "dist", "server");
|
|
316
|
-
await mkdir(nestedDir, { recursive: true });
|
|
317
|
-
|
|
318
|
-
process.chdir(projectRoot);
|
|
319
|
-
process.env.INIT_CWD = projectRoot;
|
|
320
|
-
if (devStateWasSet) {
|
|
321
|
-
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
322
|
-
}
|
|
323
|
-
const resolvedProjectRoot = await realpath(projectRoot);
|
|
324
|
-
|
|
325
|
-
const state = new WorkflowState();
|
|
326
|
-
assert.ok(
|
|
327
|
-
state.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
|
|
328
|
-
"state file should resolve under project .state/users directory"
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
process.chdir(nestedDir);
|
|
332
|
-
delete process.env.INIT_CWD;
|
|
333
|
-
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
334
|
-
|
|
335
|
-
const state2 = new WorkflowState();
|
|
336
|
-
assert.ok(
|
|
337
|
-
state2.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
|
|
338
|
-
"state file should resolve to project root"
|
|
339
|
-
);
|
|
340
|
-
} finally {
|
|
341
|
-
process.chdir(originalCwd);
|
|
342
|
-
if (initWasSet) {
|
|
343
|
-
process.env.INIT_CWD = originalInitCwd;
|
|
344
|
-
} else {
|
|
345
|
-
delete process.env.INIT_CWD;
|
|
346
|
-
}
|
|
347
|
-
if (devStateWasSet) {
|
|
348
|
-
process.env.DEV_WORKFLOW_STATE_FILE = originalDevState;
|
|
349
|
-
} else {
|
|
350
|
-
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
351
|
-
}
|
|
352
|
-
await rm(projectRoot, { recursive: true, force: true });
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("WorkflowState reset keeps history but clears progress", async () => {
|
|
357
|
-
await withWorkflowState(async (state) => {
|
|
358
|
-
state.state.currentPhase = "ready";
|
|
359
|
-
state.state.testsCreated = true;
|
|
360
|
-
state.state.testsPassed = true;
|
|
361
|
-
state.state.documentationCreated = true;
|
|
362
|
-
state.state.readyCheckCompleted = true;
|
|
363
|
-
state.state.released = true;
|
|
364
|
-
state.state.commitAndPushCompleted = true;
|
|
365
|
-
state.state.testsSkipped = true;
|
|
366
|
-
state.state.testsSkippedReason = "Manual QA";
|
|
367
|
-
state.addToHistory({ taskDescription: "Sample task" });
|
|
368
|
-
|
|
369
|
-
state.reset();
|
|
370
|
-
|
|
371
|
-
assert.equal(state.state.currentPhase, "idle");
|
|
372
|
-
assert.equal(state.state.testsCreated, false);
|
|
373
|
-
assert.equal(state.state.testsPassed, false);
|
|
374
|
-
assert.equal(state.state.documentationCreated, false);
|
|
375
|
-
assert.equal(state.state.readyCheckCompleted, false);
|
|
376
|
-
assert.equal(state.state.released, false);
|
|
377
|
-
assert.equal(state.state.commitAndPushCompleted, false);
|
|
378
|
-
assert.equal(state.state.testsSkipped, false);
|
|
379
|
-
assert.equal(state.state.testsSkippedReason, "");
|
|
380
|
-
assert.equal(state.state.history.length, 1);
|
|
381
|
-
assert.equal(state.state.history[0].taskDescription, "Sample task");
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
test("getNextStep treats skipped tests as satisfied", () => {
|
|
386
|
-
const skippedState = {
|
|
387
|
-
bugFixed: true,
|
|
388
|
-
testsCreated: true,
|
|
389
|
-
testsPassed: true,
|
|
390
|
-
testsSkipped: true,
|
|
391
|
-
testsSkippedReason: "Legacy system",
|
|
392
|
-
documentationCreated: false,
|
|
393
|
-
readyCheckCompleted: false,
|
|
394
|
-
commitAndPushCompleted: false,
|
|
395
|
-
released: false,
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
assert.equal(getNextStep(skippedState), "Create documentation");
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test("getNextStep guides workflow progression", () => {
|
|
402
|
-
const baseState = {
|
|
403
|
-
bugFixed: false,
|
|
404
|
-
testsCreated: false,
|
|
405
|
-
testsPassed: false,
|
|
406
|
-
documentationCreated: false,
|
|
407
|
-
readyCheckCompleted: false,
|
|
408
|
-
commitAndPushCompleted: false,
|
|
409
|
-
released: false,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
assert.equal(
|
|
413
|
-
getNextStep(baseState),
|
|
414
|
-
"Mark feature/bug as fixed"
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const afterFix = { ...baseState, bugFixed: true };
|
|
418
|
-
assert.equal(
|
|
419
|
-
getNextStep(afterFix),
|
|
420
|
-
"Create tests"
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
const afterTestsCreated = { ...afterFix, testsCreated: true };
|
|
424
|
-
assert.equal(getNextStep(afterTestsCreated), "Run tests");
|
|
425
|
-
|
|
426
|
-
const afterTests = { ...afterTestsCreated, testsPassed: true };
|
|
427
|
-
assert.equal(
|
|
428
|
-
getNextStep(afterTests),
|
|
429
|
-
"Create documentation"
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
const afterDocs = { ...afterTests, documentationCreated: true };
|
|
433
|
-
assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
|
|
434
|
-
|
|
435
|
-
const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
|
|
436
|
-
assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
|
|
437
|
-
|
|
438
|
-
const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
|
|
439
|
-
assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
|
|
440
|
-
|
|
441
|
-
const afterRelease = { ...afterCommit, released: true };
|
|
442
|
-
assert.equal(getNextStep(afterRelease), "Complete the task");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("containsTestFilesInStatus detects modified test files", () => {
|
|
446
|
-
const statusOutput = [
|
|
447
|
-
" M src/app.test.js",
|
|
448
|
-
"A tests/helpers/util.js",
|
|
449
|
-
"R src/old.js -> src/new.spec.ts",
|
|
450
|
-
].join("\n");
|
|
451
|
-
|
|
452
|
-
assert.equal(containsTestFilesInStatus(statusOutput), true);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test("containsTestFilesInStatus returns false when no test files present", () => {
|
|
456
|
-
const statusOutput = [
|
|
457
|
-
" M src/app.js",
|
|
458
|
-
"A docs/README.md",
|
|
459
|
-
"?? assets/logo.png",
|
|
460
|
-
].join("\n");
|
|
461
|
-
|
|
462
|
-
assert.equal(containsTestFilesInStatus(statusOutput), false);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
test("createCommitMessageParts generates test prefixed summary with details", () => {
|
|
466
|
-
const changes = [
|
|
467
|
-
{ status: "M", path: "src/app.test.js" },
|
|
468
|
-
{ status: "A", path: "tests/helpers/util.js" },
|
|
469
|
-
];
|
|
470
|
-
|
|
471
|
-
const { summary, body } = createCommitMessageParts(changes);
|
|
472
|
-
|
|
473
|
-
assert.equal(summary.startsWith("test:"), true);
|
|
474
|
-
assert.equal(body.includes("- modified src/app.test.js"), true);
|
|
475
|
-
assert.equal(body.includes("- added tests/helpers/util.js"), true);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("createCommitMessageParts respects provided summary when present", () => {
|
|
479
|
-
const changes = [{ status: "M", path: "docs/guide.md" }];
|
|
480
|
-
const provided = "docs: update guide";
|
|
481
|
-
|
|
482
|
-
const { summary, body } = createCommitMessageParts(changes, provided);
|
|
483
|
-
|
|
484
|
-
assert.equal(summary, provided);
|
|
485
|
-
assert.equal(body.includes("- modified docs/guide.md"), true);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test("determineReleaseTypeFromCommit returns major for breaking change", () => {
|
|
489
|
-
const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
|
|
490
|
-
assert.equal(determineReleaseTypeFromCommit(message), "major");
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test("determineReleaseTypeFromCommit returns minor for feat", () => {
|
|
494
|
-
const message = "feat: add dashboard";
|
|
495
|
-
assert.equal(determineReleaseTypeFromCommit(message), "minor");
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
test("determineReleaseTypeFromCommit returns patch for fix", () => {
|
|
499
|
-
const message = "fix: resolve login issue";
|
|
500
|
-
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
|
|
504
|
-
const message = "chore: update dependencies";
|
|
505
|
-
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
test("workingTreeSummary reports no changes for empty output", () => {
|
|
509
|
-
const summary = workingTreeSummary("\n \n");
|
|
510
|
-
assert.equal(summary.hasChanges, false);
|
|
511
|
-
assert.deepEqual(summary.lines, []);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("workingTreeSummary lists trimmed status lines", () => {
|
|
515
|
-
const statusOutput = " M src/app.js\nA tests/new.test.js\n";
|
|
516
|
-
const summary = workingTreeSummary(statusOutput);
|
|
517
|
-
|
|
518
|
-
assert.equal(summary.hasChanges, true);
|
|
519
|
-
assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
|
|
520
|
-
});
|
package/web/Dockerfile.api
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
FROM node:20-alpine
|
|
2
|
-
|
|
3
|
-
# Install dependencies for native modules
|
|
4
|
-
RUN apk add --no-cache python3 make g++ postgresql-client
|
|
5
|
-
|
|
6
|
-
WORKDIR /app
|
|
7
|
-
|
|
8
|
-
# Copy package files
|
|
9
|
-
COPY package*.json ./
|
|
10
|
-
|
|
11
|
-
# Install dependencies
|
|
12
|
-
RUN npm install --only=production
|
|
13
|
-
|
|
14
|
-
# Copy application code
|
|
15
|
-
COPY . .
|
|
16
|
-
|
|
17
|
-
# Expose API port
|
|
18
|
-
EXPOSE 8080
|
|
19
|
-
|
|
20
|
-
# Set environment
|
|
21
|
-
ENV NODE_ENV=production
|
|
22
|
-
ENV API_PORT=8080
|
|
23
|
-
|
|
24
|
-
# Run API server
|
|
25
|
-
CMD ["node", "api-server.js"]
|
package/web/README.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# Dev Workflow Website
|
|
2
|
-
|
|
3
|
-
A modern, responsive website for the Dev Workflow MCP Server built with Next.js, React, and Tailwind CSS.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- ✅ **Responsive Design** - Optimized for all screen sizes including vertical/portrait monitors
|
|
8
|
-
- 🌓 **Dark/Light Mode Toggle** - User preference persisted to localStorage with system preference detection
|
|
9
|
-
- 📱 **Mobile First** - Fully responsive with adaptive layouts for sm, md, lg, and xl breakpoints
|
|
10
|
-
- ⚡ **Static Export** - Pre-rendered static HTML for fast Netlify deployment
|
|
11
|
-
- 🎨 **Modern UI** - Built with Tailwind CSS and Lucide React icons
|
|
12
|
-
- ♿ **Accessible** - Semantic HTML and proper ARIA labels
|
|
13
|
-
|
|
14
|
-
## Getting Started
|
|
15
|
-
|
|
16
|
-
### Development
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm run dev
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Open [http://localhost:3000](http://localhost:3000) to see the site.
|
|
23
|
-
|
|
24
|
-
### Build
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm run build
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Generates static HTML in the `out/` directory.
|
|
31
|
-
|
|
32
|
-
## Features Overview
|
|
33
|
-
|
|
34
|
-
### Responsive Vertical Monitor Support
|
|
35
|
-
- Grid layout switches from 2-column to single-column on smaller screens
|
|
36
|
-
- Terminal visualization hidden on screens smaller than xl breakpoint
|
|
37
|
-
- Responsive padding and text sizes scale appropriately
|
|
38
|
-
|
|
39
|
-
### Dark/Light Mode
|
|
40
|
-
- Toggle button in navbar (Sun/Moon icons)
|
|
41
|
-
- Theme preference stored in localStorage
|
|
42
|
-
- Respects system color scheme preference on first visit
|
|
43
|
-
- Smooth transitions between modes
|
|
44
|
-
|
|
45
|
-
## Deployment
|
|
46
|
-
|
|
47
|
-
### Netlify
|
|
48
|
-
|
|
49
|
-
1. **Prerequisites**: Install Netlify CLI
|
|
50
|
-
```bash
|
|
51
|
-
npm install -g netlify-cli
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
2. **Build the site**
|
|
55
|
-
```bash
|
|
56
|
-
npm run build
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
3. **Deploy**
|
|
60
|
-
```bash
|
|
61
|
-
netlify deploy --prod --dir=out
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
4. **Or connect GitHub for automatic deployments**
|
|
65
|
-
- Push to GitHub
|
|
66
|
-
- Connect repo to Netlify
|
|
67
|
-
- Netlify automatically builds and deploys on push
|
|
68
|
-
|
|
69
|
-
### Environment Setup
|
|
70
|
-
|
|
71
|
-
Create a `.env.local` file if needed for environment variables (currently not required).
|
|
72
|
-
|
|
73
|
-
## Testing
|
|
74
|
-
|
|
75
|
-
Run the build verification tests:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
node verify-build.js
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Tests verify:
|
|
82
|
-
- Build completes successfully
|
|
83
|
-
- All pages generate correctly
|
|
84
|
-
- Dark mode classes are included
|
|
85
|
-
- Responsive design classes present
|
|
86
|
-
- Theme toggle functionality
|
|
87
|
-
- Viewport meta tags for mobile
|
|
88
|
-
- Vertical monitor optimization
|
|
89
|
-
|
|
90
|
-
## Project Structure
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
web/
|
|
94
|
-
├── app/
|
|
95
|
-
│ ├── layout.tsx # Root layout with ThemeProvider
|
|
96
|
-
│ ├── page.tsx # Homepage
|
|
97
|
-
│ ├── globals.css # Global styles
|
|
98
|
-
│ └── docs/ # Documentation pages
|
|
99
|
-
├── components/
|
|
100
|
-
│ ├── Navbar.tsx # Navigation with theme toggle
|
|
101
|
-
│ ├── Hero.tsx # Hero section
|
|
102
|
-
│ ├── Features.tsx # Features grid
|
|
103
|
-
│ ├── History.tsx # Workflow history section
|
|
104
|
-
│ ├── Terminal.tsx # Animated terminal demo
|
|
105
|
-
│ ├── Footer.tsx # Footer
|
|
106
|
-
│ └── ThemeProvider.tsx # Theme context provider
|
|
107
|
-
├── public/ # Static assets
|
|
108
|
-
├── tailwind.config.ts # Tailwind configuration
|
|
109
|
-
├── next.config.ts # Next.js configuration
|
|
110
|
-
└── package.json
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Technologies
|
|
114
|
-
|
|
115
|
-
- **Framework**: Next.js 16 with Turbopack
|
|
116
|
-
- **Styling**: Tailwind CSS 3
|
|
117
|
-
- **Icons**: Lucide React
|
|
118
|
-
- **Language**: TypeScript
|
|
119
|
-
- **Deployment**: Netlify (static export)
|
|
120
|
-
|
|
121
|
-
## Browser Support
|
|
122
|
-
|
|
123
|
-
- Chrome/Edge (latest)
|
|
124
|
-
- Firefox (latest)
|
|
125
|
-
- Safari (latest)
|
|
126
|
-
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
127
|
-
|
|
128
|
-
## Performance
|
|
129
|
-
|
|
130
|
-
- Static HTML export for instant page loads
|
|
131
|
-
- Optimized images with Next.js Image component
|
|
132
|
-
- Minimal JavaScript bundle
|
|
133
|
-
- CSS-in-JS with Tailwind for efficient styling
|