fifony 0.1.11
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/FIFONY.md +173 -0
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +175 -0
- package/app/dist/assets/index-BE3a-eEo.js +13 -0
- package/app/dist/icon-maskable.svg +8 -0
- package/app/dist/icon.svg +7 -0
- package/app/dist/index.html +24 -0
- package/app/dist/manifest.webmanifest +49 -0
- package/app/dist/offline.html +86 -0
- package/app/dist/service-worker.js +100 -0
- package/app/public/icon-maskable.svg +8 -0
- package/app/public/icon.svg +7 -0
- package/app/public/manifest.webmanifest +49 -0
- package/app/public/offline.html +86 -0
- package/app/public/service-worker.js +100 -0
- package/bin/fifony.js +54 -0
- package/dist/chunk-LH5V2WV2.js +389 -0
- package/dist/chunk-LH5V2WV2.js.map +1 -0
- package/dist/cli.js +204 -0
- package/dist/cli.js.map +1 -0
- package/dist/mcp/server.js +747 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/runtime/run-local.js +6569 -0
- package/dist/runtime/run-local.js.map +1 -0
- package/package.json +69 -0
- package/src/fixtures/agent-catalog.json +208 -0
- package/src/fixtures/skill-catalog.json +67 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
+
<title id="title">Fifony</title>
|
|
3
|
+
<rect width="512" height="512" fill="#0f172a"/>
|
|
4
|
+
<rect x="88" y="88" width="336" height="336" rx="72" fill="#13223d"/>
|
|
5
|
+
<path fill="#4ade80" d="M154 160h204c11 0 20 9 20 20v26H134v-26c0-11 9-20 20-20z"/>
|
|
6
|
+
<path fill="#22d3ee" d="M130 212h252l-24 138c-3 15-16 26-31 26H185c-15 0-28-11-31-26L130 212z"/>
|
|
7
|
+
<circle cx="256" cy="326" r="24" fill="#0f172a"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
+
<title id="title">Fifony</title>
|
|
3
|
+
<rect width="512" height="512" rx="96" fill="#0f172a"/>
|
|
4
|
+
<path fill="#4ade80" d="M130 132h252c13 0 24 11 24 24v32H106v-32c0-13 11-24 24-24z"/>
|
|
5
|
+
<path fill="#22d3ee" d="M102 192h308l-30 170c-3 18-19 32-38 32H170c-19 0-35-14-38-32L102 192z"/>
|
|
6
|
+
<circle cx="256" cy="332" r="26" fill="#0f172a"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#0f172a" />
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
8
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
9
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
10
|
+
<meta name="format-detection" content="telephone=no" />
|
|
11
|
+
<meta name="description" content="Fifony orchestrator dashboard" />
|
|
12
|
+
<title>Fifony</title>
|
|
13
|
+
<link rel="manifest" href="/assets/manifest.webmanifest" />
|
|
14
|
+
<link rel="icon" href="/assets/icon.svg" type="image/svg+xml" />
|
|
15
|
+
<link rel="apple-touch-icon" href="/assets/icon.svg" />
|
|
16
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
|
17
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
19
|
+
<script type="module" crossorigin src="/assets/assets/index-BE3a-eEo.js"></script>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="root"></div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Fifony Dashboard",
|
|
4
|
+
"short_name": "Fifony",
|
|
5
|
+
"description": "Fifony local orchestrator dashboard (React + PWA)",
|
|
6
|
+
"lang": "en-US",
|
|
7
|
+
"start_url": "/kanban",
|
|
8
|
+
"scope": "/",
|
|
9
|
+
"display": "standalone",
|
|
10
|
+
"display_override": ["window-controls-overlay", "standalone", "browser"],
|
|
11
|
+
"background_color": "#020617",
|
|
12
|
+
"theme_color": "#0f172a",
|
|
13
|
+
"orientation": "portrait-primary",
|
|
14
|
+
"prefer_related_applications": false,
|
|
15
|
+
"categories": ["productivity", "developer", "business"],
|
|
16
|
+
"shortcuts": [
|
|
17
|
+
{
|
|
18
|
+
"name": "Kanban",
|
|
19
|
+
"short_name": "Kanban",
|
|
20
|
+
"url": "/kanban",
|
|
21
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Issues",
|
|
25
|
+
"short_name": "Issues",
|
|
26
|
+
"url": "/issues",
|
|
27
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "Agents",
|
|
31
|
+
"short_name": "Agents",
|
|
32
|
+
"url": "/agents",
|
|
33
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"icons": [
|
|
37
|
+
{
|
|
38
|
+
"src": "/icon.svg",
|
|
39
|
+
"type": "image/svg+xml",
|
|
40
|
+
"sizes": "any"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"src": "/icon-maskable.svg",
|
|
44
|
+
"type": "image/svg+xml",
|
|
45
|
+
"sizes": "any",
|
|
46
|
+
"purpose": "any maskable"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#0f172a" />
|
|
7
|
+
<title>Offline · Fifony</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #020617;
|
|
12
|
+
--panel: #0f172a;
|
|
13
|
+
--line: rgba(148, 163, 184, 0.18);
|
|
14
|
+
--text: #e2e8f0;
|
|
15
|
+
--muted: #94a3b8;
|
|
16
|
+
--accent: #22d3ee;
|
|
17
|
+
--accent-2: #4ade80;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
display: grid;
|
|
24
|
+
place-items: center;
|
|
25
|
+
background:
|
|
26
|
+
radial-gradient(circle at top, rgba(34, 211, 238, 0.14), transparent 42%),
|
|
27
|
+
radial-gradient(circle at bottom, rgba(74, 222, 128, 0.12), transparent 36%),
|
|
28
|
+
var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font: 16px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
31
|
+
padding: 24px;
|
|
32
|
+
}
|
|
33
|
+
main {
|
|
34
|
+
width: min(100%, 520px);
|
|
35
|
+
padding: 28px;
|
|
36
|
+
border-radius: 24px;
|
|
37
|
+
border: 1px solid var(--line);
|
|
38
|
+
background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.84));
|
|
39
|
+
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
|
|
40
|
+
}
|
|
41
|
+
.eyebrow {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
padding: 6px 10px;
|
|
44
|
+
border-radius: 999px;
|
|
45
|
+
border: 1px solid var(--line);
|
|
46
|
+
color: var(--muted);
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
letter-spacing: 0.08em;
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
}
|
|
51
|
+
h1 { margin: 16px 0 8px; font-size: clamp(28px, 5vw, 40px); line-height: 1.05; }
|
|
52
|
+
p { margin: 0; color: var(--muted); }
|
|
53
|
+
.actions { display: flex; gap: 12px; margin-top: 24px; flex-wrap: wrap; }
|
|
54
|
+
a, button {
|
|
55
|
+
appearance: none;
|
|
56
|
+
border: 0;
|
|
57
|
+
border-radius: 999px;
|
|
58
|
+
padding: 12px 18px;
|
|
59
|
+
font: inherit;
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
a {
|
|
64
|
+
color: #082f49;
|
|
65
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
66
|
+
font-weight: 700;
|
|
67
|
+
}
|
|
68
|
+
button {
|
|
69
|
+
color: var(--text);
|
|
70
|
+
background: rgba(148, 163, 184, 0.12);
|
|
71
|
+
border: 1px solid var(--line);
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<main>
|
|
77
|
+
<div class="eyebrow">Offline</div>
|
|
78
|
+
<h1>Fifony is temporarily disconnected.</h1>
|
|
79
|
+
<p>The dashboard shell is available, but live runtime data needs the local server connection to come back.</p>
|
|
80
|
+
<div class="actions">
|
|
81
|
+
<a href="/kanban">Try again</a>
|
|
82
|
+
<button type="button" onclick="location.reload()">Reload</button>
|
|
83
|
+
</div>
|
|
84
|
+
</main>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const CORE_CACHE = "fifony-core-v2";
|
|
2
|
+
const ASSET_CACHE = "fifony-assets-v2";
|
|
3
|
+
const APP_SHELL_ROUTES = ["/kanban", "/issues", "/agents", "/providers", "/settings"];
|
|
4
|
+
const APP_SHELL_FILES = ["/offline.html", "/manifest.webmanifest", "/icon.svg", "/icon-maskable.svg"];
|
|
5
|
+
const API_PREFIXES = ["/api/", "/docs", "/ws"];
|
|
6
|
+
|
|
7
|
+
self.addEventListener("install", (event) => {
|
|
8
|
+
event.waitUntil((async () => {
|
|
9
|
+
const cache = await caches.open(CORE_CACHE);
|
|
10
|
+
await cache.addAll([...APP_SHELL_ROUTES, ...APP_SHELL_FILES]);
|
|
11
|
+
await self.skipWaiting();
|
|
12
|
+
})());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
self.addEventListener("activate", (event) => {
|
|
16
|
+
event.waitUntil((async () => {
|
|
17
|
+
const names = await caches.keys();
|
|
18
|
+
await Promise.all(
|
|
19
|
+
names
|
|
20
|
+
.filter((name) => ![CORE_CACHE, ASSET_CACHE].includes(name))
|
|
21
|
+
.map((name) => caches.delete(name)),
|
|
22
|
+
);
|
|
23
|
+
await self.clients.claim();
|
|
24
|
+
})());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
self.addEventListener("message", (event) => {
|
|
28
|
+
if (event.data?.type === "SKIP_WAITING") {
|
|
29
|
+
self.skipWaiting();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
self.addEventListener("fetch", (event) => {
|
|
34
|
+
const request = event.request;
|
|
35
|
+
if (request.method !== "GET") return;
|
|
36
|
+
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
if (url.origin !== self.location.origin) return;
|
|
39
|
+
if (API_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) return;
|
|
40
|
+
|
|
41
|
+
if (request.mode === "navigate") {
|
|
42
|
+
event.respondWith((async () => {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(request);
|
|
45
|
+
const cache = await caches.open(CORE_CACHE);
|
|
46
|
+
await cache.put(request, response.clone());
|
|
47
|
+
return response;
|
|
48
|
+
} catch {
|
|
49
|
+
const cache = await caches.open(CORE_CACHE);
|
|
50
|
+
return (
|
|
51
|
+
await cache.match(request) ||
|
|
52
|
+
await cache.match("/kanban") ||
|
|
53
|
+
await cache.match("/offline.html")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
})());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isStaticAsset =
|
|
61
|
+
url.pathname.startsWith("/assets/") ||
|
|
62
|
+
url.pathname.endsWith(".css") ||
|
|
63
|
+
url.pathname.endsWith(".js") ||
|
|
64
|
+
url.pathname.endsWith(".svg") ||
|
|
65
|
+
url.pathname.endsWith(".webmanifest");
|
|
66
|
+
|
|
67
|
+
if (!isStaticAsset) return;
|
|
68
|
+
|
|
69
|
+
event.respondWith((async () => {
|
|
70
|
+
const cache = await caches.open(ASSET_CACHE);
|
|
71
|
+
const cached = await cache.match(request);
|
|
72
|
+
|
|
73
|
+
if (cached) {
|
|
74
|
+
event.waitUntil((async () => {
|
|
75
|
+
try {
|
|
76
|
+
const fresh = await fetch(request);
|
|
77
|
+
if (fresh?.ok) {
|
|
78
|
+
await cache.put(request, fresh.clone());
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
})());
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(request);
|
|
87
|
+
if (response?.ok) {
|
|
88
|
+
await cache.put(request, response.clone());
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
91
|
+
} catch {
|
|
92
|
+
if (cached) return cached;
|
|
93
|
+
return new Response("Offline", {
|
|
94
|
+
status: 503,
|
|
95
|
+
statusText: "Service Unavailable",
|
|
96
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
})());
|
|
100
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
+
<title id="title">Fifony</title>
|
|
3
|
+
<rect width="512" height="512" fill="#0f172a"/>
|
|
4
|
+
<rect x="88" y="88" width="336" height="336" rx="72" fill="#13223d"/>
|
|
5
|
+
<path fill="#4ade80" d="M154 160h204c11 0 20 9 20 20v26H134v-26c0-11 9-20 20-20z"/>
|
|
6
|
+
<path fill="#22d3ee" d="M130 212h252l-24 138c-3 15-16 26-31 26H185c-15 0-28-11-31-26L130 212z"/>
|
|
7
|
+
<circle cx="256" cy="326" r="24" fill="#0f172a"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title">
|
|
2
|
+
<title id="title">Fifony</title>
|
|
3
|
+
<rect width="512" height="512" rx="96" fill="#0f172a"/>
|
|
4
|
+
<path fill="#4ade80" d="M130 132h252c13 0 24 11 24 24v32H106v-32c0-13 11-24 24-24z"/>
|
|
5
|
+
<path fill="#22d3ee" d="M102 192h308l-30 170c-3 18-19 32-38 32H170c-19 0-35-14-38-32L102 192z"/>
|
|
6
|
+
<circle cx="256" cy="332" r="26" fill="#0f172a"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Fifony Dashboard",
|
|
4
|
+
"short_name": "Fifony",
|
|
5
|
+
"description": "Fifony local orchestrator dashboard (React + PWA)",
|
|
6
|
+
"lang": "en-US",
|
|
7
|
+
"start_url": "/kanban",
|
|
8
|
+
"scope": "/",
|
|
9
|
+
"display": "standalone",
|
|
10
|
+
"display_override": ["window-controls-overlay", "standalone", "browser"],
|
|
11
|
+
"background_color": "#020617",
|
|
12
|
+
"theme_color": "#0f172a",
|
|
13
|
+
"orientation": "portrait-primary",
|
|
14
|
+
"prefer_related_applications": false,
|
|
15
|
+
"categories": ["productivity", "developer", "business"],
|
|
16
|
+
"shortcuts": [
|
|
17
|
+
{
|
|
18
|
+
"name": "Kanban",
|
|
19
|
+
"short_name": "Kanban",
|
|
20
|
+
"url": "/kanban",
|
|
21
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Issues",
|
|
25
|
+
"short_name": "Issues",
|
|
26
|
+
"url": "/issues",
|
|
27
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "Agents",
|
|
31
|
+
"short_name": "Agents",
|
|
32
|
+
"url": "/agents",
|
|
33
|
+
"icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"icons": [
|
|
37
|
+
{
|
|
38
|
+
"src": "/icon.svg",
|
|
39
|
+
"type": "image/svg+xml",
|
|
40
|
+
"sizes": "any"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"src": "/icon-maskable.svg",
|
|
44
|
+
"type": "image/svg+xml",
|
|
45
|
+
"sizes": "any",
|
|
46
|
+
"purpose": "any maskable"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#0f172a" />
|
|
7
|
+
<title>Offline · Fifony</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #020617;
|
|
12
|
+
--panel: #0f172a;
|
|
13
|
+
--line: rgba(148, 163, 184, 0.18);
|
|
14
|
+
--text: #e2e8f0;
|
|
15
|
+
--muted: #94a3b8;
|
|
16
|
+
--accent: #22d3ee;
|
|
17
|
+
--accent-2: #4ade80;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
display: grid;
|
|
24
|
+
place-items: center;
|
|
25
|
+
background:
|
|
26
|
+
radial-gradient(circle at top, rgba(34, 211, 238, 0.14), transparent 42%),
|
|
27
|
+
radial-gradient(circle at bottom, rgba(74, 222, 128, 0.12), transparent 36%),
|
|
28
|
+
var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font: 16px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
31
|
+
padding: 24px;
|
|
32
|
+
}
|
|
33
|
+
main {
|
|
34
|
+
width: min(100%, 520px);
|
|
35
|
+
padding: 28px;
|
|
36
|
+
border-radius: 24px;
|
|
37
|
+
border: 1px solid var(--line);
|
|
38
|
+
background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.84));
|
|
39
|
+
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
|
|
40
|
+
}
|
|
41
|
+
.eyebrow {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
padding: 6px 10px;
|
|
44
|
+
border-radius: 999px;
|
|
45
|
+
border: 1px solid var(--line);
|
|
46
|
+
color: var(--muted);
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
letter-spacing: 0.08em;
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
}
|
|
51
|
+
h1 { margin: 16px 0 8px; font-size: clamp(28px, 5vw, 40px); line-height: 1.05; }
|
|
52
|
+
p { margin: 0; color: var(--muted); }
|
|
53
|
+
.actions { display: flex; gap: 12px; margin-top: 24px; flex-wrap: wrap; }
|
|
54
|
+
a, button {
|
|
55
|
+
appearance: none;
|
|
56
|
+
border: 0;
|
|
57
|
+
border-radius: 999px;
|
|
58
|
+
padding: 12px 18px;
|
|
59
|
+
font: inherit;
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
a {
|
|
64
|
+
color: #082f49;
|
|
65
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
66
|
+
font-weight: 700;
|
|
67
|
+
}
|
|
68
|
+
button {
|
|
69
|
+
color: var(--text);
|
|
70
|
+
background: rgba(148, 163, 184, 0.12);
|
|
71
|
+
border: 1px solid var(--line);
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<main>
|
|
77
|
+
<div class="eyebrow">Offline</div>
|
|
78
|
+
<h1>Fifony is temporarily disconnected.</h1>
|
|
79
|
+
<p>The dashboard shell is available, but live runtime data needs the local server connection to come back.</p>
|
|
80
|
+
<div class="actions">
|
|
81
|
+
<a href="/kanban">Try again</a>
|
|
82
|
+
<button type="button" onclick="location.reload()">Reload</button>
|
|
83
|
+
</div>
|
|
84
|
+
</main>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const CORE_CACHE = "fifony-core-v2";
|
|
2
|
+
const ASSET_CACHE = "fifony-assets-v2";
|
|
3
|
+
const APP_SHELL_ROUTES = ["/kanban", "/issues", "/agents", "/providers", "/settings"];
|
|
4
|
+
const APP_SHELL_FILES = ["/offline.html", "/manifest.webmanifest", "/icon.svg", "/icon-maskable.svg"];
|
|
5
|
+
const API_PREFIXES = ["/api/", "/docs", "/ws"];
|
|
6
|
+
|
|
7
|
+
self.addEventListener("install", (event) => {
|
|
8
|
+
event.waitUntil((async () => {
|
|
9
|
+
const cache = await caches.open(CORE_CACHE);
|
|
10
|
+
await cache.addAll([...APP_SHELL_ROUTES, ...APP_SHELL_FILES]);
|
|
11
|
+
await self.skipWaiting();
|
|
12
|
+
})());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
self.addEventListener("activate", (event) => {
|
|
16
|
+
event.waitUntil((async () => {
|
|
17
|
+
const names = await caches.keys();
|
|
18
|
+
await Promise.all(
|
|
19
|
+
names
|
|
20
|
+
.filter((name) => ![CORE_CACHE, ASSET_CACHE].includes(name))
|
|
21
|
+
.map((name) => caches.delete(name)),
|
|
22
|
+
);
|
|
23
|
+
await self.clients.claim();
|
|
24
|
+
})());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
self.addEventListener("message", (event) => {
|
|
28
|
+
if (event.data?.type === "SKIP_WAITING") {
|
|
29
|
+
self.skipWaiting();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
self.addEventListener("fetch", (event) => {
|
|
34
|
+
const request = event.request;
|
|
35
|
+
if (request.method !== "GET") return;
|
|
36
|
+
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
if (url.origin !== self.location.origin) return;
|
|
39
|
+
if (API_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) return;
|
|
40
|
+
|
|
41
|
+
if (request.mode === "navigate") {
|
|
42
|
+
event.respondWith((async () => {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(request);
|
|
45
|
+
const cache = await caches.open(CORE_CACHE);
|
|
46
|
+
await cache.put(request, response.clone());
|
|
47
|
+
return response;
|
|
48
|
+
} catch {
|
|
49
|
+
const cache = await caches.open(CORE_CACHE);
|
|
50
|
+
return (
|
|
51
|
+
await cache.match(request) ||
|
|
52
|
+
await cache.match("/kanban") ||
|
|
53
|
+
await cache.match("/offline.html")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
})());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isStaticAsset =
|
|
61
|
+
url.pathname.startsWith("/assets/") ||
|
|
62
|
+
url.pathname.endsWith(".css") ||
|
|
63
|
+
url.pathname.endsWith(".js") ||
|
|
64
|
+
url.pathname.endsWith(".svg") ||
|
|
65
|
+
url.pathname.endsWith(".webmanifest");
|
|
66
|
+
|
|
67
|
+
if (!isStaticAsset) return;
|
|
68
|
+
|
|
69
|
+
event.respondWith((async () => {
|
|
70
|
+
const cache = await caches.open(ASSET_CACHE);
|
|
71
|
+
const cached = await cache.match(request);
|
|
72
|
+
|
|
73
|
+
if (cached) {
|
|
74
|
+
event.waitUntil((async () => {
|
|
75
|
+
try {
|
|
76
|
+
const fresh = await fetch(request);
|
|
77
|
+
if (fresh?.ok) {
|
|
78
|
+
await cache.put(request, fresh.clone());
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
})());
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(request);
|
|
87
|
+
if (response?.ok) {
|
|
88
|
+
await cache.put(request, response.clone());
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
91
|
+
} catch {
|
|
92
|
+
if (cached) return cached;
|
|
93
|
+
return new Response("Offline", {
|
|
94
|
+
status: 503,
|
|
95
|
+
statusText: "Service Unavailable",
|
|
96
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
})());
|
|
100
|
+
});
|
package/bin/fifony.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { cwd, env, exit, argv } from "node:process";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const packageRoot = resolve(__dirname, "..");
|
|
10
|
+
const workspaceRoot = env.FIFONY_WORKSPACE_ROOT ?? cwd();
|
|
11
|
+
|
|
12
|
+
const distCli = resolve(packageRoot, "dist", "cli.js");
|
|
13
|
+
const srcCli = resolve(packageRoot, "src", "cli.ts");
|
|
14
|
+
const forceSource = argv.includes("--dev") || env.NODE_ENV === "development";
|
|
15
|
+
const useCompiled = !forceSource && existsSync(distCli);
|
|
16
|
+
|
|
17
|
+
if (useCompiled) {
|
|
18
|
+
// Production: run compiled JS directly
|
|
19
|
+
process.env.FIFONY_WORKSPACE_ROOT = workspaceRoot;
|
|
20
|
+
import(distCli).catch((error) => {
|
|
21
|
+
console.error(`Failed to start fifony: ${String(error)}`);
|
|
22
|
+
exit(1);
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
// Development: use tsx to run TypeScript source
|
|
26
|
+
const { spawn } = await import("node:child_process");
|
|
27
|
+
const { createRequire } = await import("node:module");
|
|
28
|
+
const { execPath } = await import("node:process");
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
let tsxCli;
|
|
32
|
+
try {
|
|
33
|
+
tsxCli = require.resolve("tsx/cli");
|
|
34
|
+
} catch {
|
|
35
|
+
console.error("No compiled dist/ found and tsx is not installed. Run 'pnpm build' first.");
|
|
36
|
+
exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const child = spawn(execPath, [tsxCli, srcCli, ...argv.slice(2)], {
|
|
40
|
+
cwd: workspaceRoot,
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env: { ...env, FIFONY_WORKSPACE_ROOT: workspaceRoot },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on("exit", (code, signal) => {
|
|
46
|
+
if (signal) { process.kill(process.pid, signal); return; }
|
|
47
|
+
exit(code ?? 1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.on("error", (error) => {
|
|
51
|
+
console.error(`Failed to start fifony CLI: ${String(error)}`);
|
|
52
|
+
exit(1);
|
|
53
|
+
});
|
|
54
|
+
}
|