@programinglive/dev-workflow-mcp-server 1.5.0 → 1.5.2
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/.agent/workflows/dev-workflow-bugfix.md +250 -250
- package/.agent/workflows/dev-workflow-feature.md +201 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
- package/.agent/workflows/dev-workflow-refactor.md +238 -238
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
- package/.github/pull_request_template.md +22 -22
- package/.github/workflows/npm-publish.yml +50 -47
- package/.husky/commit-msg +0 -0
- package/CHANGELOG.md +750 -491
- package/CODE_OF_CONDUCT.md +79 -79
- package/CONTRIBUTING.md +60 -60
- package/LICENSE +21 -21
- package/README.md +632 -609
- package/SECURITY.md +20 -20
- package/db/index.js +204 -204
- package/docs/NETLIFY_DEPLOYMENT.md +125 -0
- package/docs/PLESK_DEPLOYMENT.md +170 -0
- package/docs/WORKFLOW-BEST-PRACTICES.md +58 -0
- package/docs/antigravity/EXAMPLES.md +535 -535
- package/docs/antigravity/GETTING-STARTED.md +292 -292
- package/docs/antigravity/TROUBLESHOOTING.md +742 -742
- package/docs/antigravity/WALKTHROUGH.md +25 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
- package/docs/product/PRD.md +67 -64
- package/docs/release-notes/RELEASE_NOTES.md +226 -132
- package/docs/web-dashboard.md +175 -163
- package/exec.js +49 -49
- package/index.js +279 -279
- package/install.js +73 -73
- package/package.json +11 -9
- package/release-wrapper.js +192 -157
- package/scripts/build-docs.js +63 -63
- package/scripts/hello-world.js +1 -1
- package/scripts/release.cjs +252 -252
- package/scripts/update-release-notes.cjs +178 -182
- package/server.json +23 -0
- package/tailwind.config.js +8 -10
- package/tests/build.test.js +83 -48
- package/tests/exec.test.js +35 -35
- package/tests/handlers.test.js +1058 -980
- package/tests/hello-world.test.js +13 -13
- package/tests/mcpRegistry.test.js +65 -0
- package/tests/release-wrapper.test.js +107 -105
- package/tests/workflowState.test.js +505 -502
- package/tools/definitions.js +332 -332
- package/tools/handlers.js +1019 -1019
- package/vite.config.js +43 -43
- package/vite.web.config.js +23 -0
- package/web/README.md +36 -0
- package/web/app/docs/page.tsx +202 -0
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +91 -0
- package/web/app/layout.tsx +36 -0
- package/web/app/page.tsx +15 -0
- package/web/components/ApiReference.tsx +20 -0
- package/web/components/Features.tsx +61 -0
- package/web/components/Footer.tsx +9 -0
- package/web/components/Hero.tsx +96 -0
- package/web/components/History.tsx +143 -0
- package/web/components/Navbar.tsx +64 -0
- package/web/components/Terminal.tsx +114 -0
- package/web/eslint.config.mjs +18 -0
- package/web/lib/db.js +131 -0
- package/web/lib/docs.js +73 -0
- package/web/netlify.toml +3 -0
- package/web/next.config.ts +12 -0
- package/web/package-lock.json +6560 -0
- package/web/package.json +28 -0
- package/web/postcss.config.mjs +9 -0
- 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 +1 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/server.js +31 -159
- package/web/tailwind.config.ts +19 -0
- package/web/tsconfig.json +42 -0
- package/workflow-state.js +612 -626
- package/dist/docs/EXAMPLES.html +0 -411
- package/dist/docs/GETTING-STARTED.html +0 -233
- package/dist/docs/README.html +0 -535
- package/dist/docs/RELEASE_NOTES.html +0 -167
- package/dist/docs/TROUBLESHOOTING.html +0 -542
- package/dist/docs/WORKFLOW-TEMPLATES.html +0 -591
- package/dist/docs/web-dashboard.html +0 -158
- package/web/public/index.html +0 -1046
- package/web/public/input.css +0 -3
- package/web/public/output.css +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { ArrowRight } from "lucide-react";
|
|
5
|
+
import Terminal from "./Terminal";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
export default function Hero() {
|
|
9
|
+
const [downloads, setDownloads] = useState<number | null>(null);
|
|
10
|
+
const [version, setVersion] = useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Fetch npm downloads directly from npm API
|
|
14
|
+
fetch("https://api.npmjs.org/downloads/point/last-month/@programinglive/dev-workflow-mcp-server")
|
|
15
|
+
.then(res => res.json())
|
|
16
|
+
.then(data => setDownloads(data.downloads || 0))
|
|
17
|
+
.catch(() => setDownloads(null));
|
|
18
|
+
|
|
19
|
+
// Version is hardcoded for static export
|
|
20
|
+
setVersion("1.5.1");
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const formatDownloads = (num: number) => {
|
|
24
|
+
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
|
25
|
+
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
|
26
|
+
return num.toString();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section className="min-h-screen flex items-center justify-center px-4 pt-16 relative overflow-hidden">
|
|
31
|
+
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-purple-500/10 to-pink-500/10"></div>
|
|
32
|
+
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
|
33
|
+
{/* Left Column - Content */}
|
|
34
|
+
<div>
|
|
35
|
+
<div className="animate-fade-in-up" style={{ animationDelay: "0.1s" }}>
|
|
36
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm mb-8">
|
|
37
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
38
|
+
<span>v{version ?? "..."} - Active</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight animate-fade-in-up" style={{ animationDelay: "0.2s" }}>
|
|
43
|
+
Master Your<br />
|
|
44
|
+
<span className="bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">Workflow</span>
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="text-lg md:text-xl text-gray-400 mb-8 max-w-xl animate-fade-in-up" style={{ animationDelay: "0.3s" }}>
|
|
47
|
+
An MCP server that acts as your coding conscience. Enforce test-driven development, ensure documentation, and never skip a step in your release process.
|
|
48
|
+
</p>
|
|
49
|
+
<div className="flex flex-col sm:flex-row gap-4 mb-8 animate-fade-in-up" style={{ animationDelay: "0.4s" }}>
|
|
50
|
+
<Link href="#history" className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-lg font-semibold hover:shadow-lg hover:shadow-indigo-500/50 transition inline-flex items-center justify-center gap-2">
|
|
51
|
+
View History <ArrowRight className="w-4 h-4" />
|
|
52
|
+
</Link>
|
|
53
|
+
<Link href="/docs" className="px-6 py-3 bg-white/5 border border-white/10 rounded-lg font-semibold hover:bg-white/10 transition text-center">
|
|
54
|
+
Documentation
|
|
55
|
+
</Link>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex flex-wrap gap-6 sm:gap-8 text-sm animate-fade-in-up" style={{ animationDelay: "0.5s" }}>
|
|
58
|
+
<div>
|
|
59
|
+
<div className="text-2xl font-bold">{version ?? "..."}</div>
|
|
60
|
+
<div className="text-gray-500">Latest Version</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<div className="text-2xl font-bold">MIT</div>
|
|
64
|
+
<div className="text-gray-500">License</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<div className="text-2xl font-bold">Node 18+</div>
|
|
68
|
+
<div className="text-gray-500">Compatibility</div>
|
|
69
|
+
</div>
|
|
70
|
+
{downloads !== null && (
|
|
71
|
+
<div>
|
|
72
|
+
<div className="text-2xl font-bold">{formatDownloads(downloads)}</div>
|
|
73
|
+
<div className="text-gray-500">Downloads/mo</div>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{/* Right Column - Terminal */}
|
|
79
|
+
<div className="hidden lg:block">
|
|
80
|
+
<div className="relative">
|
|
81
|
+
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500/20 to-purple-500/20 blur-3xl"></div>
|
|
82
|
+
<div className="relative bg-[#1a1a24] rounded-xl border border-white/10 p-6 shadow-2xl">
|
|
83
|
+
<div className="flex items-center gap-2 mb-4">
|
|
84
|
+
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
|
85
|
+
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
|
86
|
+
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
|
87
|
+
<span className="ml-auto text-xs text-gray-500">terminal</span>
|
|
88
|
+
</div>
|
|
89
|
+
<Terminal />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
type HistoryItem = {
|
|
6
|
+
task_description: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SummaryData = {
|
|
11
|
+
summary: {
|
|
12
|
+
totalTasks: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function History() {
|
|
17
|
+
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
18
|
+
const [summary, setSummary] = useState<SummaryData | null>(null);
|
|
19
|
+
const [page, setPage] = useState(1);
|
|
20
|
+
const [totalPages, setTotalPages] = useState(1);
|
|
21
|
+
const [filters, setFilters] = useState({
|
|
22
|
+
userId: "",
|
|
23
|
+
startDate: "",
|
|
24
|
+
endDate: ""
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const loadData = async () => {
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
page: page.toString(),
|
|
30
|
+
pageSize: "20",
|
|
31
|
+
...(filters.userId && { user: filters.userId }),
|
|
32
|
+
...(filters.startDate && { startDate: filters.startDate }),
|
|
33
|
+
...(filters.endDate && { endDate: filters.endDate }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`/api/history?${params}`);
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
setHistory(data.data || []);
|
|
40
|
+
setTotalPages(data.totalPages || 1);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error("Failed to load history", err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`/api/summary?${params}`);
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
setSummary(data);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error("Failed to load summary", err);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
loadData();
|
|
56
|
+
}, [page]); // Reload when page changes
|
|
57
|
+
|
|
58
|
+
const handleApplyFilters = () => {
|
|
59
|
+
setPage(1);
|
|
60
|
+
loadData();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section id="history" className="py-20 px-4 bg-white/5">
|
|
65
|
+
<div className="max-w-7xl mx-auto">
|
|
66
|
+
<h2 className="text-3xl md:text-4xl font-bold mb-8">Workflow History</h2>
|
|
67
|
+
<p className="text-gray-400 mb-12">Track your development progress and analyze your workflow patterns.</p>
|
|
68
|
+
<div className="bg-[#0a0a0f] rounded-xl border border-white/10 p-6">
|
|
69
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
placeholder="User ID"
|
|
73
|
+
value={filters.userId}
|
|
74
|
+
onChange={(e) => setFilters({ ...filters, userId: e.target.value })}
|
|
75
|
+
className="px-4 py-2 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
|
76
|
+
/>
|
|
77
|
+
<input
|
|
78
|
+
type="date"
|
|
79
|
+
value={filters.startDate}
|
|
80
|
+
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
|
|
81
|
+
className="px-4 py-2 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 [color-scheme:dark]"
|
|
82
|
+
/>
|
|
83
|
+
<input
|
|
84
|
+
type="date"
|
|
85
|
+
value={filters.endDate}
|
|
86
|
+
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
|
|
87
|
+
className="px-4 py-2 bg-white/5 border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 [color-scheme:dark]"
|
|
88
|
+
/>
|
|
89
|
+
<button
|
|
90
|
+
onClick={handleApplyFilters}
|
|
91
|
+
className="px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-lg font-semibold hover:shadow-lg hover:shadow-indigo-500/50 transition"
|
|
92
|
+
>
|
|
93
|
+
Apply Filters
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{summary && (
|
|
98
|
+
<div className="mb-6">
|
|
99
|
+
<h3 className="text-lg font-semibold mb-4">Project Summary</h3>
|
|
100
|
+
<div className="text-gray-400">
|
|
101
|
+
<p>Total tasks: {summary.summary?.totalTasks || 0}</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<h3 className="text-lg font-semibold mb-4">Recent History</h3>
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
{history.length > 0 ? (
|
|
110
|
+
history.map((row, i) => (
|
|
111
|
+
<div key={i} className="p-4 rounded-lg bg-white/5 border border-white/10">
|
|
112
|
+
<div className="font-semibold">{row.task_description}</div>
|
|
113
|
+
<div className="text-sm text-gray-400 mt-1">{new Date(row.created_at).toLocaleString()}</div>
|
|
114
|
+
</div>
|
|
115
|
+
))
|
|
116
|
+
) : (
|
|
117
|
+
<p className="text-gray-500">No history yet.</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="flex items-center justify-between mt-6">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
124
|
+
disabled={page <= 1}
|
|
125
|
+
className="px-4 py-2 bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 disabled:opacity-50"
|
|
126
|
+
>
|
|
127
|
+
Previous
|
|
128
|
+
</button>
|
|
129
|
+
<span className="text-sm text-gray-400">Page {page} of {totalPages}</span>
|
|
130
|
+
<button
|
|
131
|
+
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
|
132
|
+
disabled={page >= totalPages}
|
|
133
|
+
className="px-4 py-2 bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 disabled:opacity-50"
|
|
134
|
+
>
|
|
135
|
+
Next
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Github, Menu, X } from "lucide-react";
|
|
6
|
+
import Image from "next/image";
|
|
7
|
+
|
|
8
|
+
export default function Navbar() {
|
|
9
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<nav className="fixed top-0 left-0 right-0 z-50 bg-[#0a0a0f]/80 backdrop-blur-lg border-b border-white/10">
|
|
13
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
14
|
+
<div className="flex items-center justify-between h-16">
|
|
15
|
+
<Link href="/" className="flex items-center gap-2">
|
|
16
|
+
<Image src="/assets/logo.png" alt="Dev Workflow" width={32} height={32} className="rounded-lg" />
|
|
17
|
+
<span className="text-lg font-bold">Dev Workflow</span>
|
|
18
|
+
</Link>
|
|
19
|
+
<div className="hidden md:flex items-center gap-6">
|
|
20
|
+
<Link href="/#features" className="text-gray-300 hover:text-white transition">Features</Link>
|
|
21
|
+
<Link href="/#history" className="text-gray-300 hover:text-white transition">History</Link>
|
|
22
|
+
<Link href="/#api" className="text-gray-300 hover:text-white transition">API</Link>
|
|
23
|
+
<Link href="/docs" className="text-gray-300 hover:text-white transition">Docs</Link>
|
|
24
|
+
<a
|
|
25
|
+
href="https://github.com/programinglive/dev-workflow-mcp-server"
|
|
26
|
+
target="_blank"
|
|
27
|
+
rel="noopener noreferrer"
|
|
28
|
+
className="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 rounded-lg transition"
|
|
29
|
+
>
|
|
30
|
+
<Github className="w-4 h-4" />
|
|
31
|
+
GitHub
|
|
32
|
+
</a>
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
className="md:hidden p-2 rounded-lg hover:bg-white/5"
|
|
36
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
37
|
+
>
|
|
38
|
+
{isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
{/* Mobile Menu */}
|
|
43
|
+
{isOpen && (
|
|
44
|
+
<div className="md:hidden border-t border-white/10 bg-[#0a0a0f]">
|
|
45
|
+
<div className="px-4 py-4 space-y-3">
|
|
46
|
+
<Link href="/#features" className="block text-gray-300 hover:text-white transition" onClick={() => setIsOpen(false)}>Features</Link>
|
|
47
|
+
<Link href="/#history" className="block text-gray-300 hover:text-white transition" onClick={() => setIsOpen(false)}>History</Link>
|
|
48
|
+
<Link href="/#api" className="block text-gray-300 hover:text-white transition" onClick={() => setIsOpen(false)}>API</Link>
|
|
49
|
+
<Link href="/docs" className="block text-gray-300 hover:text-white transition" onClick={() => setIsOpen(false)}>Docs</Link>
|
|
50
|
+
<a
|
|
51
|
+
href="https://github.com/programinglive/dev-workflow-mcp-server"
|
|
52
|
+
target="_blank"
|
|
53
|
+
rel="noopener noreferrer"
|
|
54
|
+
className="flex items-center gap-2 text-gray-300 hover:text-white transition"
|
|
55
|
+
>
|
|
56
|
+
<Github className="w-4 h-4" />
|
|
57
|
+
GitHub
|
|
58
|
+
</a>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</nav>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
import { Rocket } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
type TerminalLine = {
|
|
7
|
+
type: 'command' | 'success' | 'comment' | 'info' | 'final';
|
|
8
|
+
text: string;
|
|
9
|
+
delay: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const terminalLines: TerminalLine[] = [
|
|
13
|
+
{ type: 'command', text: 'start_task', delay: 500 },
|
|
14
|
+
{ type: 'success', text: '✓ Task started: Implement user auth', delay: 1500 },
|
|
15
|
+
{ type: 'comment', text: '# Write code...', delay: 2500 },
|
|
16
|
+
{ type: 'command', text: 'mark_bug_fixed', delay: 4000 },
|
|
17
|
+
{ type: 'info', text: '✨ Feature complete! Now create tests...', delay: 5000 },
|
|
18
|
+
{ type: 'command', text: 'create_tests', delay: 6500 },
|
|
19
|
+
{ type: 'success', text: '✓ Tests created', delay: 7500 },
|
|
20
|
+
{ type: 'command', text: 'run_tests', delay: 8500 },
|
|
21
|
+
{ type: 'success', text: '✓ All tests passed!', delay: 9500 },
|
|
22
|
+
{ type: 'command', text: 'create_documentation', delay: 10500 },
|
|
23
|
+
{ type: 'success', text: '✓ Documentation updated', delay: 11500 },
|
|
24
|
+
{ type: 'command', text: 'commit_and_push', delay: 12500 },
|
|
25
|
+
{ type: 'final', text: 'Ready to release!', delay: 13500 }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export default function Terminal() {
|
|
29
|
+
const [lines, setLines] = useState<React.ReactElement[]>([]);
|
|
30
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const hasStarted = useRef(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const observer = new IntersectionObserver((entries) => {
|
|
35
|
+
entries.forEach(entry => {
|
|
36
|
+
if (entry.isIntersecting && !hasStarted.current) {
|
|
37
|
+
hasStarted.current = true;
|
|
38
|
+
runAnimation();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (containerRef.current) {
|
|
44
|
+
observer.observe(containerRef.current);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return () => observer.disconnect();
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const runAnimation = async () => {
|
|
51
|
+
for (const line of terminalLines) {
|
|
52
|
+
await new Promise(r => setTimeout(r, line.delay - (terminalLines[terminalLines.indexOf(line) - 1]?.delay || 0)));
|
|
53
|
+
|
|
54
|
+
if (line.type === 'command') {
|
|
55
|
+
await typeCommand(line.text);
|
|
56
|
+
} else {
|
|
57
|
+
addLine(line);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const typeCommand = async (text: string) => {
|
|
63
|
+
const id = Date.now();
|
|
64
|
+
setLines(prev => [...prev, (
|
|
65
|
+
<div key={id} className="terminal-line visible">
|
|
66
|
+
<span className="text-gray-500 mr-2">$</span>
|
|
67
|
+
<span className="text-white typing-cursor" id={`cmd-${id}`}></span>
|
|
68
|
+
</div>
|
|
69
|
+
)]);
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
|
|
73
|
+
const el = document.getElementById(`cmd-${id}`);
|
|
74
|
+
if (el) el.textContent += text[i];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const el = document.getElementById(`cmd-${id}`);
|
|
78
|
+
if (el) el.classList.remove('typing-cursor');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const addLine = (line: TerminalLine) => {
|
|
82
|
+
let content: React.ReactElement;
|
|
83
|
+
let className = "terminal-line visible";
|
|
84
|
+
|
|
85
|
+
switch (line.type) {
|
|
86
|
+
case 'success':
|
|
87
|
+
className += " text-emerald-400";
|
|
88
|
+
content = <>{line.text}</>;
|
|
89
|
+
break;
|
|
90
|
+
case 'comment':
|
|
91
|
+
className += " mt-3";
|
|
92
|
+
content = <><span className="text-gray-500 mr-2">$</span><span className="text-white">{line.text}</span></>;
|
|
93
|
+
break;
|
|
94
|
+
case 'info':
|
|
95
|
+
className += " text-purple-400";
|
|
96
|
+
content = <>{line.text}</>;
|
|
97
|
+
break;
|
|
98
|
+
case 'final':
|
|
99
|
+
className += " text-pink-400 flex items-center gap-2";
|
|
100
|
+
content = <><Rocket className="w-4 h-4" />{line.text}</>;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
content = <>{line.text}</>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setLines(prev => [...prev, <div key={Date.now()} className={className}>{content}</div>]);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div ref={containerRef} className="font-mono text-sm space-y-2 min-h-[400px]">
|
|
111
|
+
{lines}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
package/web/lib/db.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const DB_FILENAME = "dev-workflow.db";
|
|
6
|
+
const FREQUENCY_FORMATS = {
|
|
7
|
+
daily: "%Y-%m-%d",
|
|
8
|
+
monthly: "%Y-%m",
|
|
9
|
+
yearly: "%Y",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getDbPath() {
|
|
13
|
+
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
14
|
+
let projectRoot = initCwd;
|
|
15
|
+
|
|
16
|
+
// Look for .state folder specifically.
|
|
17
|
+
while (projectRoot !== path.dirname(projectRoot)) {
|
|
18
|
+
if (existsSync(path.join(projectRoot, ".state"))) return path.join(projectRoot, ".state", DB_FILENAME);
|
|
19
|
+
projectRoot = path.dirname(projectRoot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback to relative path from this file if possible
|
|
23
|
+
return path.join(process.cwd(), "..", ".state", DB_FILENAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let db;
|
|
27
|
+
export function getDb() {
|
|
28
|
+
if (!db) {
|
|
29
|
+
const dbPath = getDbPath();
|
|
30
|
+
db = new Database(dbPath);
|
|
31
|
+
db.pragma("journal_mode = WAL");
|
|
32
|
+
migrate();
|
|
33
|
+
}
|
|
34
|
+
return db;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function migrate() {
|
|
38
|
+
const createHistory = `
|
|
39
|
+
CREATE TABLE IF NOT EXISTS workflow_history (
|
|
40
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
+
user_id TEXT NOT NULL,
|
|
42
|
+
task_description TEXT NOT NULL,
|
|
43
|
+
task_type TEXT NOT NULL,
|
|
44
|
+
commit_message TEXT,
|
|
45
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
forced INTEGER DEFAULT 0,
|
|
47
|
+
force_reason TEXT,
|
|
48
|
+
dropped INTEGER DEFAULT 0,
|
|
49
|
+
drop_reason TEXT,
|
|
50
|
+
UNIQUE(user_id, timestamp, task_description)
|
|
51
|
+
)
|
|
52
|
+
`;
|
|
53
|
+
const createSummary = `
|
|
54
|
+
CREATE TABLE IF NOT EXISTS project_summary (
|
|
55
|
+
user_id TEXT PRIMARY KEY,
|
|
56
|
+
total_tasks INTEGER DEFAULT 0,
|
|
57
|
+
task_types TEXT DEFAULT '{}',
|
|
58
|
+
last_active DATETIME,
|
|
59
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
recent_tasks TEXT DEFAULT '[]'
|
|
61
|
+
)
|
|
62
|
+
`;
|
|
63
|
+
const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
|
|
64
|
+
const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
|
|
65
|
+
|
|
66
|
+
db.exec(createHistory);
|
|
67
|
+
db.exec(createSummary);
|
|
68
|
+
db.exec(idxHistory);
|
|
69
|
+
db.exec(idxSummary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getSummaryForUser(userId) {
|
|
73
|
+
const stmt = getDb().prepare(`
|
|
74
|
+
SELECT * FROM project_summary WHERE user_id = ?
|
|
75
|
+
`);
|
|
76
|
+
const row = stmt.get(userId);
|
|
77
|
+
if (!row) return null;
|
|
78
|
+
return {
|
|
79
|
+
totalTasks: row.total_tasks,
|
|
80
|
+
taskTypes: JSON.parse(row.task_types),
|
|
81
|
+
lastActive: row.last_active,
|
|
82
|
+
recentTasks: JSON.parse(row.recent_tasks),
|
|
83
|
+
updatedAt: row.updated_at,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildHistoryFilters({ startDate, endDate }) {
|
|
88
|
+
const clauses = [];
|
|
89
|
+
const params = [];
|
|
90
|
+
if (startDate) {
|
|
91
|
+
clauses.push("DATE(timestamp) >= DATE(?)");
|
|
92
|
+
params.push(startDate);
|
|
93
|
+
}
|
|
94
|
+
if (endDate) {
|
|
95
|
+
clauses.push("DATE(timestamp) <= DATE(?)");
|
|
96
|
+
params.push(endDate);
|
|
97
|
+
}
|
|
98
|
+
return { clauses, params };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getHistoryForUser(userId, options = {}) {
|
|
102
|
+
const { page = 1, pageSize = 20, startDate, endDate } = options;
|
|
103
|
+
const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
|
|
104
|
+
const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
|
|
105
|
+
const offset = (sanitizedPage - 1) * sanitizedPageSize;
|
|
106
|
+
|
|
107
|
+
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
108
|
+
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
109
|
+
const dbInstance = getDb();
|
|
110
|
+
|
|
111
|
+
const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
|
|
112
|
+
const total = countStmt.get(userId, ...params).total;
|
|
113
|
+
|
|
114
|
+
const rowsStmt = dbInstance.prepare(`
|
|
115
|
+
SELECT *
|
|
116
|
+
FROM workflow_history
|
|
117
|
+
WHERE ${where}
|
|
118
|
+
ORDER BY timestamp DESC
|
|
119
|
+
LIMIT ?
|
|
120
|
+
OFFSET ?
|
|
121
|
+
`);
|
|
122
|
+
const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
data: entries,
|
|
126
|
+
total,
|
|
127
|
+
page: sanitizedPage,
|
|
128
|
+
pageSize: sanitizedPageSize,
|
|
129
|
+
totalPages: Math.ceil(total / sanitizedPageSize)
|
|
130
|
+
};
|
|
131
|
+
}
|
package/web/lib/docs.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile, access } from "fs/promises";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
function getProjectRoot() {
|
|
7
|
+
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
8
|
+
let projectRoot = initCwd;
|
|
9
|
+
while (projectRoot !== path.dirname(projectRoot)) {
|
|
10
|
+
if (existsSync(path.join(projectRoot, "package.json")) && existsSync(path.join(projectRoot, "docs"))) return projectRoot;
|
|
11
|
+
projectRoot = path.dirname(projectRoot);
|
|
12
|
+
}
|
|
13
|
+
return path.join(process.cwd(), "..");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const projectRoot = getProjectRoot();
|
|
17
|
+
|
|
18
|
+
const docSources = {
|
|
19
|
+
"README.md": {
|
|
20
|
+
markdown: join(projectRoot, "README.md"),
|
|
21
|
+
html: join(projectRoot, "dist", "docs", "README.html"),
|
|
22
|
+
},
|
|
23
|
+
"web-dashboard.md": {
|
|
24
|
+
markdown: join(projectRoot, "docs", "web-dashboard.md"),
|
|
25
|
+
html: join(projectRoot, "dist", "docs", "web-dashboard.html"),
|
|
26
|
+
},
|
|
27
|
+
"GETTING-STARTED.md": {
|
|
28
|
+
markdown: join(projectRoot, "docs", "antigravity", "GETTING-STARTED.md"),
|
|
29
|
+
html: join(projectRoot, "dist", "docs", "GETTING-STARTED.html"),
|
|
30
|
+
},
|
|
31
|
+
"EXAMPLES.md": {
|
|
32
|
+
markdown: join(projectRoot, "docs", "antigravity", "EXAMPLES.md"),
|
|
33
|
+
html: join(projectRoot, "dist", "docs", "EXAMPLES.html"),
|
|
34
|
+
},
|
|
35
|
+
"WORKFLOW-TEMPLATES.md": {
|
|
36
|
+
markdown: join(projectRoot, "docs", "antigravity", "WORKFLOW-TEMPLATES.md"),
|
|
37
|
+
html: join(projectRoot, "dist", "docs", "WORKFLOW-TEMPLATES.html"),
|
|
38
|
+
},
|
|
39
|
+
"TROUBLESHOOTING.md": {
|
|
40
|
+
markdown: join(projectRoot, "docs", "antigravity", "TROUBLESHOOTING.md"),
|
|
41
|
+
html: join(projectRoot, "dist", "docs", "TROUBLESHOOTING.html"),
|
|
42
|
+
},
|
|
43
|
+
"RELEASE_NOTES.md": {
|
|
44
|
+
markdown: join(projectRoot, "docs", "release-notes", "RELEASE_NOTES.md"),
|
|
45
|
+
html: join(projectRoot, "dist", "docs", "RELEASE_NOTES.html"),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export async function resolveDocContent(docKey) {
|
|
50
|
+
const entry = docSources[docKey];
|
|
51
|
+
if (!entry) {
|
|
52
|
+
return { exists: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try HTML first, fall back to Markdown in development or if HTML missing.
|
|
56
|
+
const candidates = [entry.html, entry.markdown];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
try {
|
|
59
|
+
await access(candidate);
|
|
60
|
+
const content = await readFile(candidate, "utf-8");
|
|
61
|
+
const format = candidate.endsWith(".html") ? "html" : "markdown";
|
|
62
|
+
return { exists: true, content, format };
|
|
63
|
+
} catch {
|
|
64
|
+
// try next candidate
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { exists: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getAvailableDocs() {
|
|
72
|
+
return Object.keys(docSources);
|
|
73
|
+
}
|
package/web/netlify.toml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
// Static export for Netlify deployment
|
|
5
|
+
output: 'export',
|
|
6
|
+
// Disable image optimization for static export
|
|
7
|
+
images: {
|
|
8
|
+
unoptimized: true,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default nextConfig;
|