kcg-code 0.0.0-stage → 0.1.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/README.md +228 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +83 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/dashboard.test.ts +62 -0
- package/src/cli/args.ts +123 -0
- package/src/cli/commands.ts +174 -0
- package/src/cli/dashboard.tsx +343 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +142 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +250 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/PanelSection.tsx +102 -0
- package/src/components/projects/ProjectInstructions.tsx +229 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +448 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +123 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
- package/src/components/sessions/ContextPanel.tsx +561 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +209 -0
- package/src/components/sessions/ModelSearchList.tsx +151 -0
- package/src/components/sessions/NewSessionDialog.tsx +189 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +640 -0
- package/src/components/sessions/SessionHeader.tsx +175 -0
- package/src/components/sessions/SessionList.tsx +396 -0
- package/src/components/sessions/SessionRow.tsx +137 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +236 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +262 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +265 -0
- package/src/components/skills/InstallNotifications.tsx +325 -0
- package/src/components/skills/InstallSkillDialog.tsx +233 -0
- package/src/components/skills/SkillCards.tsx +210 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +85 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +97 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +234 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +52 -0
- package/src/lib/__tests__/project-overview.test.ts +173 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +134 -0
- package/src/lib/__tests__/skills-catalog.test.ts +96 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +74 -0
- package/src/lib/project-overview.ts +117 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +115 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +360 -0
- package/src/lib/skills-catalog.ts +99 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +130 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +299 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
- package/src/pages/skills/SkillsPage.tsx +701 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1031 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +415 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +157 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +117 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3469 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
- package/src/server/services/__tests__/skills-registry.test.ts +359 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1374 -0
- package/src/server/services/skill-install-jobs.ts +262 -0
- package/src/server/services/skills-registry.ts +468 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill_Install_Jobs — instalasi skill sebagai job latar dengan log live.
|
|
3
|
+
*
|
|
4
|
+
* `POST /api/projects/:id/skills/install` membuat job lalu langsung kembali
|
|
5
|
+
* (202); CLI `skills` berjalan di belakang dan setiap baris output-nya
|
|
6
|
+
* dicatat. Client mengambil log bertahap lewat `GET /api/skills/installs/:id
|
|
7
|
+
* ?from=<n>` sehingga output tampil hampir real-time dan instalasi tetap
|
|
8
|
+
* berjalan walau user pindah halaman / menutup dialog.
|
|
9
|
+
*
|
|
10
|
+
* Job disimpan di memori (hilang saat server restart — wajar untuk proses
|
|
11
|
+
* yang memang ikut mati). Log dibatasi `MAX_LOG_LINES`; job selesai dibuang
|
|
12
|
+
* setelah `FINISHED_TTL_MS` agar memori tidak tumbuh terus.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import type { Result } from "../result";
|
|
16
|
+
import type { InstallResult, SkillsRegistry } from "./skills-registry";
|
|
17
|
+
|
|
18
|
+
export const MAX_LOG_LINES = 500;
|
|
19
|
+
const MAX_LINE_LENGTH = 1000;
|
|
20
|
+
/** Job selesai tetap bisa dibaca selama ini (UI sempat menampilkan hasil). */
|
|
21
|
+
export const FINISHED_TTL_MS = 10 * 60_000;
|
|
22
|
+
|
|
23
|
+
export type InstallJobStatus = "running" | "succeeded" | "failed" | "cancelled";
|
|
24
|
+
|
|
25
|
+
export interface InstallJob {
|
|
26
|
+
id: string;
|
|
27
|
+
projectId: string;
|
|
28
|
+
projectName: string;
|
|
29
|
+
source: string;
|
|
30
|
+
skillId: string;
|
|
31
|
+
status: InstallJobStatus;
|
|
32
|
+
/** Kode error domain bila `failed`. */
|
|
33
|
+
error: string | null;
|
|
34
|
+
/** `false` = terpasang tapi server opencode belum di-refresh (ada chat aktif). */
|
|
35
|
+
refreshed: boolean | null;
|
|
36
|
+
installedPath: string | null;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
finishedAt: number | null;
|
|
39
|
+
/** Waktu baris output terakhir (untuk petunjuk "tidak ada output"). */
|
|
40
|
+
lastOutputAt: number;
|
|
41
|
+
/** Jumlah total baris log sejak awal (termasuk yang sudah terbuang). */
|
|
42
|
+
lineCount: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Potongan log mulai indeks `from` (indeks absolut sejak awal job). */
|
|
46
|
+
export interface InstallJobLog {
|
|
47
|
+
job: InstallJob;
|
|
48
|
+
/** Indeks absolut baris pertama pada `lines`. */
|
|
49
|
+
from: number;
|
|
50
|
+
lines: string[];
|
|
51
|
+
/** Ada baris lama yang sudah terbuang sebelum `from` yang diminta. */
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StartInstallInput {
|
|
56
|
+
projectId: string;
|
|
57
|
+
projectName: string;
|
|
58
|
+
projectPath: string;
|
|
59
|
+
source: string;
|
|
60
|
+
skillId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SkillInstallJobs {
|
|
64
|
+
/**
|
|
65
|
+
* Mulai job. Validasi & cek "sedang berjalan" dilakukan sinkron agar
|
|
66
|
+
* pemanggil bisa membalas 400/409 sebelum job dibuat.
|
|
67
|
+
*/
|
|
68
|
+
start(input: StartInstallInput): Result<InstallJob>;
|
|
69
|
+
get(jobId: string, from?: number): InstallJobLog | null;
|
|
70
|
+
/** Job berjalan + yang baru selesai (terbaru dulu). */
|
|
71
|
+
list(): InstallJob[];
|
|
72
|
+
/** Batalkan job berjalan (membunuh proses CLI). `false` bila tidak berjalan. */
|
|
73
|
+
cancel(jobId: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Refresh skill yang tertunda untuk Project baru saja berjalan otomatis:
|
|
76
|
+
* job sukses dengan `refreshed: false` ditandai aktif + dicatat di log.
|
|
77
|
+
*/
|
|
78
|
+
markRefreshed(projectId: string): void;
|
|
79
|
+
/** Batalkan semua job berjalan lalu tunggu selesai (shutdown server). */
|
|
80
|
+
cancelAll(): Promise<void>;
|
|
81
|
+
/** Menunggu job selesai (untuk test & shutdown). */
|
|
82
|
+
settled(jobId: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SkillInstallJobsOptions {
|
|
86
|
+
registry: SkillsRegistry;
|
|
87
|
+
/** Refresh skill server opencode Project setelah terpasang. */
|
|
88
|
+
refreshSkills: (projectId: string) => Promise<Result<{ refreshed: boolean }>>;
|
|
89
|
+
now?: () => number;
|
|
90
|
+
/** Validasi source/skill sebelum job dibuat. */
|
|
91
|
+
validate: (source: string, skillId: string) => boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface JobState {
|
|
95
|
+
job: InstallJob;
|
|
96
|
+
lines: string[];
|
|
97
|
+
/** Indeks absolut baris pertama yang masih disimpan di `lines`. */
|
|
98
|
+
offset: number;
|
|
99
|
+
done: Promise<void>;
|
|
100
|
+
abort: AbortController;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createSkillInstallJobs(opts: SkillInstallJobsOptions): SkillInstallJobs {
|
|
104
|
+
const now = opts.now ?? Date.now;
|
|
105
|
+
const jobs = new Map<string, JobState>();
|
|
106
|
+
|
|
107
|
+
function prune(): void {
|
|
108
|
+
const cutoff = now() - FINISHED_TTL_MS;
|
|
109
|
+
for (const [id, st] of jobs) {
|
|
110
|
+
if (st.job.finishedAt !== null && st.job.finishedAt < cutoff) jobs.delete(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function push(st: JobState, raw: string): void {
|
|
115
|
+
const line = raw.length > MAX_LINE_LENGTH ? `${raw.slice(0, MAX_LINE_LENGTH)}…` : raw;
|
|
116
|
+
st.lines.push(line);
|
|
117
|
+
st.job.lineCount += 1;
|
|
118
|
+
st.job.lastOutputAt = now();
|
|
119
|
+
if (st.lines.length > MAX_LOG_LINES) {
|
|
120
|
+
const drop = st.lines.length - MAX_LOG_LINES;
|
|
121
|
+
st.lines.splice(0, drop);
|
|
122
|
+
st.offset += drop;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function finish(st: JobState, status: InstallJobStatus, extra: Partial<InstallJob>): void {
|
|
127
|
+
Object.assign(st.job, extra, { status, finishedAt: now() });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function run(st: JobState, projectPath: string): Promise<void> {
|
|
131
|
+
const { job } = st;
|
|
132
|
+
const signal = st.abort.signal;
|
|
133
|
+
let res: Result<InstallResult>;
|
|
134
|
+
try {
|
|
135
|
+
res = await opts.registry.install(
|
|
136
|
+
projectPath,
|
|
137
|
+
job.source,
|
|
138
|
+
job.skillId,
|
|
139
|
+
(l) => push(st, l),
|
|
140
|
+
signal,
|
|
141
|
+
);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
res = { ok: false, error: "SKILL_INSTALL_FAILED" };
|
|
144
|
+
push(st, `Error: ${(e as Error).message}`);
|
|
145
|
+
}
|
|
146
|
+
if (signal.aborted) {
|
|
147
|
+
push(st, "✗ Installation cancelled.");
|
|
148
|
+
finish(st, "cancelled", { error: "SKILL_INSTALL_CANCELLED" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
push(st, "✗ Installation failed.");
|
|
153
|
+
finish(st, "failed", { error: res.error });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
push(st, "Reloading the agent's skill list…");
|
|
157
|
+
let refreshed = false;
|
|
158
|
+
try {
|
|
159
|
+
const r = await opts.refreshSkills(job.projectId);
|
|
160
|
+
refreshed = r.ok ? r.data.refreshed : false;
|
|
161
|
+
} catch {
|
|
162
|
+
refreshed = false;
|
|
163
|
+
}
|
|
164
|
+
push(
|
|
165
|
+
st,
|
|
166
|
+
refreshed
|
|
167
|
+
? `✓ Installed ${res.data.name}.`
|
|
168
|
+
: `✓ Installed ${res.data.name}. A chat is running in this project — the agent reloads skills automatically when it finishes.`,
|
|
169
|
+
);
|
|
170
|
+
finish(st, "succeeded", { refreshed, installedPath: res.data.path });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function start(input: StartInstallInput): Result<InstallJob> {
|
|
174
|
+
prune();
|
|
175
|
+
if (!opts.validate(input.source, input.skillId)) {
|
|
176
|
+
return { ok: false, error: "INVALID_SKILL_ID" };
|
|
177
|
+
}
|
|
178
|
+
for (const st of jobs.values()) {
|
|
179
|
+
if (st.job.projectId === input.projectId && st.job.status === "running") {
|
|
180
|
+
return { ok: false, error: "SKILL_INSTALL_IN_PROGRESS" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const job: InstallJob = {
|
|
184
|
+
id: randomUUID(),
|
|
185
|
+
projectId: input.projectId,
|
|
186
|
+
projectName: input.projectName,
|
|
187
|
+
source: input.source,
|
|
188
|
+
skillId: input.skillId,
|
|
189
|
+
status: "running",
|
|
190
|
+
error: null,
|
|
191
|
+
refreshed: null,
|
|
192
|
+
installedPath: null,
|
|
193
|
+
startedAt: now(),
|
|
194
|
+
finishedAt: null,
|
|
195
|
+
lastOutputAt: now(),
|
|
196
|
+
lineCount: 0,
|
|
197
|
+
};
|
|
198
|
+
const st: JobState = {
|
|
199
|
+
job,
|
|
200
|
+
lines: [],
|
|
201
|
+
offset: 0,
|
|
202
|
+
done: Promise.resolve(),
|
|
203
|
+
abort: new AbortController(),
|
|
204
|
+
};
|
|
205
|
+
jobs.set(job.id, st);
|
|
206
|
+
st.done = run(st, input.projectPath);
|
|
207
|
+
return { ok: true, data: { ...job } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function get(jobId: string, from = 0): InstallJobLog | null {
|
|
211
|
+
const st = jobs.get(jobId);
|
|
212
|
+
if (!st) return null;
|
|
213
|
+
const start = Math.max(0, Math.floor(from));
|
|
214
|
+
const idx = Math.max(0, start - st.offset);
|
|
215
|
+
return {
|
|
216
|
+
job: { ...st.job },
|
|
217
|
+
from: Math.max(start, st.offset),
|
|
218
|
+
lines: st.lines.slice(idx),
|
|
219
|
+
truncated: start < st.offset,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function list(): InstallJob[] {
|
|
224
|
+
prune();
|
|
225
|
+
return [...jobs.values()]
|
|
226
|
+
.map((st) => ({ ...st.job }))
|
|
227
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cancel(jobId: string): boolean {
|
|
231
|
+
const st = jobs.get(jobId);
|
|
232
|
+
if (st?.job.status !== "running" || st.abort.signal.aborted) return false;
|
|
233
|
+
push(st, "Cancelling…");
|
|
234
|
+
st.abort.abort();
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function markRefreshed(projectId: string): void {
|
|
239
|
+
for (const st of jobs.values()) {
|
|
240
|
+
if (
|
|
241
|
+
st.job.projectId === projectId &&
|
|
242
|
+
st.job.status === "succeeded" &&
|
|
243
|
+
st.job.refreshed === false
|
|
244
|
+
) {
|
|
245
|
+
st.job.refreshed = true;
|
|
246
|
+
push(st, `✓ Chat finished — ${st.job.skillId} is now active.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function cancelAll(): Promise<void> {
|
|
252
|
+
const running = [...jobs.values()].filter((st) => st.job.status === "running");
|
|
253
|
+
for (const st of running) st.abort.abort();
|
|
254
|
+
await Promise.all(running.map((st) => st.done));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function settled(jobId: string): Promise<void> {
|
|
258
|
+
await jobs.get(jobId)?.done;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { start, get, list, cancel, markRefreshed, cancelAll, settled };
|
|
262
|
+
}
|