@shykaruu/jarvis-brain 0.4.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/LICENSE +153 -0
- package/README.md +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +261 -0
- package/src/actions/browser/session.ts +506 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +363 -0
- package/src/actions/tools/builtin.ts +950 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +301 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/dashboard-auth.ts +75 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/dashboard-auth.test.ts +170 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +407 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +120 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +218 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +195 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Builder — Project Manager
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for projects, file system access, project discovery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Project, ProjectMeta, FileEntry, SiteBuilderConfig } from './types.ts';
|
|
8
|
+
import { GitManager } from './git-manager.ts';
|
|
9
|
+
import { TEMPLATES, generateMakefile, scaffoldBunReact } from './templates.ts';
|
|
10
|
+
import { join, relative, resolve } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { readdirSync, statSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const META_FILE = '.jarvis-project.json';
|
|
15
|
+
|
|
16
|
+
// Directories to exclude from file tree
|
|
17
|
+
const IGNORED_DIRS = new Set([
|
|
18
|
+
'node_modules', '.git', '.next', '.vite', 'dist', 'build',
|
|
19
|
+
'.cache', '.turbo', '.output', '.nuxt', '.svelte-kit',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db']);
|
|
23
|
+
|
|
24
|
+
export class ProjectManager {
|
|
25
|
+
private projectsDir: string;
|
|
26
|
+
private gitManager: GitManager;
|
|
27
|
+
|
|
28
|
+
constructor(config: SiteBuilderConfig, gitManager?: GitManager) {
|
|
29
|
+
this.projectsDir = config.projects_dir.replace(/^~/, homedir());
|
|
30
|
+
this.gitManager = gitManager ?? new GitManager();
|
|
31
|
+
|
|
32
|
+
// Ensure projects directory exists
|
|
33
|
+
mkdirSync(this.projectsDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover all projects by scanning the projects directory.
|
|
38
|
+
*/
|
|
39
|
+
async listProjects(): Promise<Project[]> {
|
|
40
|
+
if (!existsSync(this.projectsDir)) return [];
|
|
41
|
+
|
|
42
|
+
const entries = readdirSync(this.projectsDir, { withFileTypes: true });
|
|
43
|
+
const projects: Project[] = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory()) continue;
|
|
47
|
+
if (entry.name.startsWith('.')) continue;
|
|
48
|
+
|
|
49
|
+
const projectPath = join(this.projectsDir, entry.name);
|
|
50
|
+
|
|
51
|
+
// Must have a Makefile to be considered a project
|
|
52
|
+
if (!existsSync(join(projectPath, 'Makefile'))) continue;
|
|
53
|
+
|
|
54
|
+
const meta = this.readMeta(projectPath);
|
|
55
|
+
let gitBranch: string | null = null;
|
|
56
|
+
let gitDirty = false;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
gitBranch = await this.gitManager.getCurrentBranch(projectPath);
|
|
60
|
+
gitDirty = await this.gitManager.isDirty(projectPath);
|
|
61
|
+
} catch { /* not a git repo */ }
|
|
62
|
+
|
|
63
|
+
projects.push({
|
|
64
|
+
id: entry.name,
|
|
65
|
+
name: meta?.name ?? entry.name,
|
|
66
|
+
path: projectPath,
|
|
67
|
+
framework: meta?.framework ?? 'custom',
|
|
68
|
+
devPort: null,
|
|
69
|
+
devServerPid: null,
|
|
70
|
+
status: 'stopped',
|
|
71
|
+
gitBranch,
|
|
72
|
+
gitDirty,
|
|
73
|
+
createdAt: meta?.createdAt ?? statSync(projectPath).birthtimeMs,
|
|
74
|
+
lastOpenedAt: meta?.lastOpenedAt ?? Date.now(),
|
|
75
|
+
githubUrl: meta?.github ? `https://github.com/${meta.github.owner}/${meta.github.repo}` : null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return projects.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a single project by ID.
|
|
84
|
+
*/
|
|
85
|
+
async getProject(id: string): Promise<Project | null> {
|
|
86
|
+
const projectPath = this.resolveProjectPath(id);
|
|
87
|
+
if (!projectPath || !existsSync(join(projectPath, 'Makefile'))) return null;
|
|
88
|
+
|
|
89
|
+
const meta = this.readMeta(projectPath);
|
|
90
|
+
let gitBranch: string | null = null;
|
|
91
|
+
let gitDirty = false;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
gitBranch = await this.gitManager.getCurrentBranch(projectPath);
|
|
95
|
+
gitDirty = await this.gitManager.isDirty(projectPath);
|
|
96
|
+
} catch { /* not a git repo */ }
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
name: meta?.name ?? id,
|
|
101
|
+
path: projectPath,
|
|
102
|
+
framework: meta?.framework ?? 'custom',
|
|
103
|
+
devPort: null,
|
|
104
|
+
devServerPid: null,
|
|
105
|
+
status: 'stopped',
|
|
106
|
+
gitBranch,
|
|
107
|
+
gitDirty,
|
|
108
|
+
createdAt: meta?.createdAt ?? statSync(projectPath).birthtimeMs,
|
|
109
|
+
lastOpenedAt: meta?.lastOpenedAt ?? Date.now(),
|
|
110
|
+
githubUrl: meta?.github ? `https://github.com/${meta.github.owner}/${meta.github.repo}` : null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a new project from a template.
|
|
116
|
+
*/
|
|
117
|
+
async createProject(name: string, templateId: string, gitAuthor?: { name: string; email: string; global: boolean }): Promise<Project> {
|
|
118
|
+
const id = this.sanitizeId(name);
|
|
119
|
+
const projectPath = join(this.projectsDir, id);
|
|
120
|
+
|
|
121
|
+
if (existsSync(projectPath)) {
|
|
122
|
+
throw new Error(`Project "${id}" already exists`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const template = TEMPLATES.find(t => t.id === templateId);
|
|
126
|
+
if (!template) {
|
|
127
|
+
throw new Error(`Unknown template: ${templateId}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
mkdirSync(projectPath, { recursive: true });
|
|
131
|
+
|
|
132
|
+
// Scaffold the project
|
|
133
|
+
if (template.command === 'scaffold') {
|
|
134
|
+
// Internal scaffolding
|
|
135
|
+
if (template.framework === 'bun-react') {
|
|
136
|
+
scaffoldBunReact(projectPath);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// Use CLI tool (bunx create-vite, etc.)
|
|
140
|
+
const args = [...template.args, id];
|
|
141
|
+
const proc = Bun.spawn([template.command, ...args], {
|
|
142
|
+
cwd: this.projectsDir,
|
|
143
|
+
stdout: 'pipe',
|
|
144
|
+
stderr: 'pipe',
|
|
145
|
+
});
|
|
146
|
+
const exitCode = await proc.exited;
|
|
147
|
+
if (exitCode !== 0) {
|
|
148
|
+
const stderr = await new Response(proc.stderr).text();
|
|
149
|
+
// Clean up failed scaffold
|
|
150
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
151
|
+
throw new Error(`Template scaffolding failed: ${stderr}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Generate Makefile
|
|
156
|
+
const makefile = generateMakefile(template.framework);
|
|
157
|
+
await Bun.write(join(projectPath, 'Makefile'), makefile);
|
|
158
|
+
|
|
159
|
+
// Write project metadata
|
|
160
|
+
const meta: ProjectMeta = {
|
|
161
|
+
name,
|
|
162
|
+
framework: template.framework,
|
|
163
|
+
createdAt: Date.now(),
|
|
164
|
+
lastOpenedAt: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
await Bun.write(join(projectPath, META_FILE), JSON.stringify(meta, null, 2));
|
|
167
|
+
|
|
168
|
+
// Install dependencies
|
|
169
|
+
const installProc = Bun.spawn(['make', 'install'], {
|
|
170
|
+
cwd: projectPath,
|
|
171
|
+
stdout: 'pipe',
|
|
172
|
+
stderr: 'pipe',
|
|
173
|
+
});
|
|
174
|
+
await installProc.exited;
|
|
175
|
+
|
|
176
|
+
// Initialize git
|
|
177
|
+
await this.gitManager.init(projectPath, gitAuthor);
|
|
178
|
+
|
|
179
|
+
console.log(`[SiteBuilder] Created project "${id}" with template "${templateId}"`);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
name,
|
|
184
|
+
path: projectPath,
|
|
185
|
+
framework: template.framework,
|
|
186
|
+
devPort: null,
|
|
187
|
+
devServerPid: null,
|
|
188
|
+
status: 'stopped',
|
|
189
|
+
gitBranch: 'main',
|
|
190
|
+
gitDirty: false,
|
|
191
|
+
createdAt: meta.createdAt,
|
|
192
|
+
lastOpenedAt: meta.lastOpenedAt,
|
|
193
|
+
githubUrl: null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Delete a project and its directory.
|
|
199
|
+
*/
|
|
200
|
+
async deleteProject(id: string): Promise<void> {
|
|
201
|
+
const projectPath = this.resolveProjectPath(id);
|
|
202
|
+
if (!projectPath) throw new Error(`Project "${id}" not found`);
|
|
203
|
+
|
|
204
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
205
|
+
console.log(`[SiteBuilder] Deleted project "${id}"`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the file tree for a project.
|
|
210
|
+
*/
|
|
211
|
+
getFileTree(projectId: string, maxDepth: number = 5): FileEntry {
|
|
212
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
213
|
+
if (!projectPath) throw new Error(`Project "${projectId}" not found`);
|
|
214
|
+
|
|
215
|
+
return this.buildFileTree(projectPath, projectPath, 0, maxDepth);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Read a file from a project.
|
|
220
|
+
*/
|
|
221
|
+
async readFile(projectId: string, relativePath: string): Promise<string> {
|
|
222
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
223
|
+
if (!projectPath) throw new Error(`Project "${projectId}" not found`);
|
|
224
|
+
|
|
225
|
+
const filePath = this.safeJoin(projectPath, relativePath);
|
|
226
|
+
const file = Bun.file(filePath);
|
|
227
|
+
if (!await file.exists()) throw new Error(`File not found: ${relativePath}`);
|
|
228
|
+
|
|
229
|
+
return file.text();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Write a file to a project.
|
|
234
|
+
*/
|
|
235
|
+
async writeFile(projectId: string, relativePath: string, content: string): Promise<void> {
|
|
236
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
237
|
+
if (!projectPath) throw new Error(`Project "${projectId}" not found`);
|
|
238
|
+
|
|
239
|
+
const filePath = this.safeJoin(projectPath, relativePath);
|
|
240
|
+
|
|
241
|
+
// Ensure parent directory exists
|
|
242
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
243
|
+
mkdirSync(dir, { recursive: true });
|
|
244
|
+
|
|
245
|
+
await Bun.write(filePath, content);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Delete a file from a project.
|
|
250
|
+
*/
|
|
251
|
+
async deleteFile(projectId: string, relativePath: string): Promise<void> {
|
|
252
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
253
|
+
if (!projectPath) throw new Error(`Project "${projectId}" not found`);
|
|
254
|
+
|
|
255
|
+
const filePath = this.safeJoin(projectPath, relativePath);
|
|
256
|
+
rmSync(filePath, { force: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Update last opened timestamp.
|
|
261
|
+
*/
|
|
262
|
+
async touchProject(projectId: string): Promise<void> {
|
|
263
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
264
|
+
if (!projectPath) return;
|
|
265
|
+
|
|
266
|
+
const meta = this.readMeta(projectPath) ?? {
|
|
267
|
+
name: projectId,
|
|
268
|
+
framework: 'custom',
|
|
269
|
+
createdAt: Date.now(),
|
|
270
|
+
lastOpenedAt: Date.now(),
|
|
271
|
+
};
|
|
272
|
+
meta.lastOpenedAt = Date.now();
|
|
273
|
+
await Bun.write(join(projectPath, META_FILE), JSON.stringify(meta, null, 2));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update the GitHub metadata for a project (or clear it with null).
|
|
278
|
+
*/
|
|
279
|
+
async updateGitHubMeta(projectId: string, github: ProjectMeta['github'] | null): Promise<void> {
|
|
280
|
+
const projectPath = this.resolveProjectPath(projectId);
|
|
281
|
+
if (!projectPath) throw new Error(`Project "${projectId}" not found`);
|
|
282
|
+
|
|
283
|
+
const meta = this.readMeta(projectPath) ?? {
|
|
284
|
+
name: projectId,
|
|
285
|
+
framework: 'custom',
|
|
286
|
+
createdAt: Date.now(),
|
|
287
|
+
lastOpenedAt: Date.now(),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (github) {
|
|
291
|
+
meta.github = github;
|
|
292
|
+
} else {
|
|
293
|
+
delete meta.github;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await Bun.write(join(projectPath, META_FILE), JSON.stringify(meta, null, 2));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get the resolved absolute path for a project.
|
|
301
|
+
*/
|
|
302
|
+
getProjectPath(projectId: string): string | null {
|
|
303
|
+
return this.resolveProjectPath(projectId);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Private Helpers ──
|
|
307
|
+
|
|
308
|
+
private resolveProjectPath(id: string): string | null {
|
|
309
|
+
const projectPath = join(this.projectsDir, id);
|
|
310
|
+
// Prevent path traversal
|
|
311
|
+
const resolved = resolve(projectPath);
|
|
312
|
+
if (!resolved.startsWith(resolve(this.projectsDir))) return null;
|
|
313
|
+
if (!existsSync(resolved)) return null;
|
|
314
|
+
return resolved;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private safeJoin(projectPath: string, relativePath: string): string {
|
|
318
|
+
const resolved = resolve(join(projectPath, relativePath));
|
|
319
|
+
if (!resolved.startsWith(resolve(projectPath))) {
|
|
320
|
+
throw new Error('Path traversal attempt blocked');
|
|
321
|
+
}
|
|
322
|
+
return resolved;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private sanitizeId(name: string): string {
|
|
326
|
+
return name
|
|
327
|
+
.toLowerCase()
|
|
328
|
+
.replace(/[^a-z0-9-_]/g, '-')
|
|
329
|
+
.replace(/-+/g, '-')
|
|
330
|
+
.replace(/^-|-$/g, '')
|
|
331
|
+
.slice(0, 64) || 'project';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private readMeta(projectPath: string): ProjectMeta | null {
|
|
335
|
+
const metaPath = join(projectPath, META_FILE);
|
|
336
|
+
if (!existsSync(metaPath)) return null;
|
|
337
|
+
try {
|
|
338
|
+
const text = require('node:fs').readFileSync(metaPath, 'utf-8');
|
|
339
|
+
return JSON.parse(text) as ProjectMeta;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private buildFileTree(basePath: string, currentPath: string, depth: number, maxDepth: number): FileEntry {
|
|
346
|
+
const name = currentPath === basePath ? '.' : currentPath.split('/').pop()!;
|
|
347
|
+
const rel = relative(basePath, currentPath) || '.';
|
|
348
|
+
|
|
349
|
+
const stat = statSync(currentPath);
|
|
350
|
+
|
|
351
|
+
if (!stat.isDirectory()) {
|
|
352
|
+
return {
|
|
353
|
+
name,
|
|
354
|
+
path: rel,
|
|
355
|
+
type: 'file',
|
|
356
|
+
size: stat.size,
|
|
357
|
+
modified: stat.mtimeMs,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const entry: FileEntry = {
|
|
362
|
+
name,
|
|
363
|
+
path: rel,
|
|
364
|
+
type: 'directory',
|
|
365
|
+
children: [],
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
if (depth >= maxDepth) return entry;
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
372
|
+
const sorted = entries
|
|
373
|
+
.filter(e => !IGNORED_DIRS.has(e.name) && !IGNORED_FILES.has(e.name) && !e.name.startsWith('.'))
|
|
374
|
+
.sort((a, b) => {
|
|
375
|
+
// Directories first, then alphabetical
|
|
376
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
377
|
+
return a.name.localeCompare(b.name);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
for (const child of sorted) {
|
|
381
|
+
entry.children!.push(
|
|
382
|
+
this.buildFileTree(basePath, join(currentPath, child.name), depth + 1, maxDepth)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
} catch { /* permission error */ }
|
|
386
|
+
|
|
387
|
+
return entry;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Builder — HTTP/WebSocket Proxy
|
|
3
|
+
*
|
|
4
|
+
* Proxies requests to project dev servers running on localhost.
|
|
5
|
+
*
|
|
6
|
+
* Two routing modes on the same port:
|
|
7
|
+
* 1. Explicit: /api/sites/:id/proxy/* → sets __proj cookie, proxies to dev server
|
|
8
|
+
* 2. Catch-all: any unmatched path → reads __proj cookie, proxies to dev server
|
|
9
|
+
*
|
|
10
|
+
* Because the iframe uses allow-same-origin, absolute paths emitted by
|
|
11
|
+
* frameworks (e.g. /src/main.tsx) naturally hit the main server. The
|
|
12
|
+
* catch-all picks them up via the cookie — zero URL rewriting needed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DevServerManager } from './dev-server-manager.ts';
|
|
16
|
+
|
|
17
|
+
const PROXY_PATH_REGEX = /^\/api\/sites\/([^/]+)\/proxy(\/.*)?$/;
|
|
18
|
+
const COOKIE_NAME = '__proj';
|
|
19
|
+
|
|
20
|
+
export class SiteProxy {
|
|
21
|
+
constructor(private devServerManager: DevServerManager) {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a pathname matches the explicit proxy pattern.
|
|
25
|
+
*/
|
|
26
|
+
matchProxy(pathname: string): { projectId: string; subPath: string } | null {
|
|
27
|
+
const match = pathname.match(PROXY_PATH_REGEX);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
return {
|
|
30
|
+
projectId: match[1]!,
|
|
31
|
+
subPath: match[2] || '/',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Proxy an HTTP request to a project's dev server (explicit route).
|
|
37
|
+
* Sets the __proj cookie so the catch-all can route subsequent requests.
|
|
38
|
+
*/
|
|
39
|
+
async proxyHttp(req: Request, projectId: string, subPath: string): Promise<Response> {
|
|
40
|
+
const port = this.devServerManager.getPort(projectId);
|
|
41
|
+
if (port === null) {
|
|
42
|
+
return Response.json({ error: `Dev server for "${projectId}" is not running` }, { status: 502 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resp = await this.forward(req, port, subPath);
|
|
46
|
+
// Set cookie so the catch-all knows which project subsequent requests belong to
|
|
47
|
+
resp.headers.append('set-cookie', `${COOKIE_NAME}=${projectId}; Path=/; SameSite=Lax`);
|
|
48
|
+
return resp;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Proxy an HTTP request using the __proj cookie (catch-all route).
|
|
53
|
+
* Returns null if no cookie or project isn't running.
|
|
54
|
+
*/
|
|
55
|
+
async proxyCatchAll(req: Request, pathname: string): Promise<Response | null> {
|
|
56
|
+
const projectId = this.projectFromCookie(req);
|
|
57
|
+
if (!projectId) return null;
|
|
58
|
+
|
|
59
|
+
const port = this.devServerManager.getPort(projectId);
|
|
60
|
+
if (port === null) return null;
|
|
61
|
+
|
|
62
|
+
return this.forward(req, port, pathname);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the WebSocket target URL for a proxied connection.
|
|
67
|
+
*/
|
|
68
|
+
getWebSocketTarget(projectId: string, subPath: string): string | null {
|
|
69
|
+
const port = this.devServerManager.getPort(projectId);
|
|
70
|
+
if (port === null) return null;
|
|
71
|
+
return `ws://127.0.0.1:${port}${subPath}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the WebSocket target URL using the __proj cookie (catch-all).
|
|
76
|
+
*/
|
|
77
|
+
getWebSocketTargetFromCookie(req: Request, pathname: string): string | null {
|
|
78
|
+
const projectId = this.projectFromCookie(req);
|
|
79
|
+
if (!projectId) return null;
|
|
80
|
+
const port = this.devServerManager.getPort(projectId);
|
|
81
|
+
if (port === null) return null;
|
|
82
|
+
return `ws://127.0.0.1:${port}${pathname}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Internal ──
|
|
86
|
+
|
|
87
|
+
private projectFromCookie(req: Request): string | null {
|
|
88
|
+
const cookies = req.headers.get('cookie') || '';
|
|
89
|
+
const m = cookies.match(/__proj=([^;]+)/);
|
|
90
|
+
return m?.[1] ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async forward(req: Request, targetPort: number, path: string): Promise<Response> {
|
|
94
|
+
const targetUrl = `http://127.0.0.1:${targetPort}${path}`;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const headers = new Headers(req.headers);
|
|
98
|
+
headers.delete('host');
|
|
99
|
+
headers.set('host', `127.0.0.1:${targetPort}`);
|
|
100
|
+
|
|
101
|
+
const clientIp = req.headers.get('x-forwarded-for')
|
|
102
|
+
|| req.headers.get('x-real-ip')
|
|
103
|
+
|| '127.0.0.1';
|
|
104
|
+
headers.set('x-forwarded-for', clientIp);
|
|
105
|
+
headers.set('x-forwarded-proto', 'http');
|
|
106
|
+
|
|
107
|
+
const init: RequestInit = {
|
|
108
|
+
method: req.method,
|
|
109
|
+
headers,
|
|
110
|
+
redirect: 'manual',
|
|
111
|
+
};
|
|
112
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
113
|
+
init.body = req.body;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resp = await fetch(targetUrl, init);
|
|
117
|
+
const respHeaders = new Headers(resp.headers);
|
|
118
|
+
|
|
119
|
+
return new Response(resp.body, {
|
|
120
|
+
status: resp.status,
|
|
121
|
+
statusText: resp.statusText,
|
|
122
|
+
headers: respHeaders,
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
126
|
+
const safeMsg = rawMsg
|
|
127
|
+
.replace(/127\.0\.0\.1:\d+/g, '<dev-server>')
|
|
128
|
+
.replace(/\/home\/[^\s"']*/g, '<path>');
|
|
129
|
+
return Response.json({ error: `Proxy error: ${safeMsg}` }, { status: 502 });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Builder Service — Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages the lifecycle of the site builder feature.
|
|
5
|
+
* Implements the Service interface for daemon integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Service, ServiceStatus } from '../daemon/services.ts';
|
|
9
|
+
import type { SiteBuilderConfig, Project } from './types.ts';
|
|
10
|
+
import { ProjectManager } from './project-manager.ts';
|
|
11
|
+
import { GitManager } from './git-manager.ts';
|
|
12
|
+
import { DevServerManager } from './dev-server-manager.ts';
|
|
13
|
+
import { SiteProxy } from './proxy.ts';
|
|
14
|
+
import { GitHubManager } from './github-manager.ts';
|
|
15
|
+
|
|
16
|
+
export class SiteBuilderService implements Service {
|
|
17
|
+
name = 'site-builder';
|
|
18
|
+
private _status: ServiceStatus = 'stopped';
|
|
19
|
+
|
|
20
|
+
readonly projectManager: ProjectManager;
|
|
21
|
+
readonly gitManager: GitManager;
|
|
22
|
+
readonly githubManager: GitHubManager;
|
|
23
|
+
readonly devServerManager: DevServerManager;
|
|
24
|
+
readonly proxy: SiteProxy;
|
|
25
|
+
|
|
26
|
+
constructor(private config: SiteBuilderConfig) {
|
|
27
|
+
this.gitManager = new GitManager();
|
|
28
|
+
this.githubManager = new GitHubManager();
|
|
29
|
+
this.devServerManager = new DevServerManager(config);
|
|
30
|
+
this.projectManager = new ProjectManager(config, this.gitManager);
|
|
31
|
+
this.proxy = new SiteProxy(this.devServerManager);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async start(): Promise<void> {
|
|
35
|
+
if (!this.config.enabled) {
|
|
36
|
+
console.log('[SiteBuilder] Disabled by config');
|
|
37
|
+
this._status = 'stopped';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this._status = 'starting';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Clean up any orphaned dev server processes from a previous crash
|
|
45
|
+
await this.devServerManager.cleanupOrphans();
|
|
46
|
+
|
|
47
|
+
this._status = 'running';
|
|
48
|
+
console.log('[SiteBuilder] Service started');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this._status = 'error';
|
|
51
|
+
console.error('[SiteBuilder] Failed to start:', err instanceof Error ? err.message : err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async stop(): Promise<void> {
|
|
56
|
+
this._status = 'stopping';
|
|
57
|
+
|
|
58
|
+
// Kill all running dev servers
|
|
59
|
+
await this.devServerManager.stopAll();
|
|
60
|
+
|
|
61
|
+
this._status = 'stopped';
|
|
62
|
+
console.log('[SiteBuilder] Service stopped');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
status(): ServiceStatus {
|
|
66
|
+
return this._status;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start a project's dev server and return enriched project info.
|
|
71
|
+
*/
|
|
72
|
+
async startProject(projectId: string): Promise<Project> {
|
|
73
|
+
const project = await this.projectManager.getProject(projectId);
|
|
74
|
+
if (!project) throw new Error(`Project "${projectId}" not found`);
|
|
75
|
+
|
|
76
|
+
if (this.devServerManager.isRunning(projectId)) {
|
|
77
|
+
const port = this.devServerManager.getPort(projectId)!;
|
|
78
|
+
return { ...project, devPort: port, status: 'running' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { port, pid } = await this.devServerManager.start(projectId, project.path);
|
|
82
|
+
|
|
83
|
+
// Update last opened time
|
|
84
|
+
await this.projectManager.touchProject(projectId);
|
|
85
|
+
|
|
86
|
+
// Wait for server to be ready
|
|
87
|
+
const ready = await this.devServerManager.waitForReady(port);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...project,
|
|
91
|
+
devPort: port,
|
|
92
|
+
devServerPid: pid,
|
|
93
|
+
status: ready ? 'running' : 'starting',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stop a project's dev server.
|
|
99
|
+
*/
|
|
100
|
+
async stopProject(projectId: string): Promise<void> {
|
|
101
|
+
await this.devServerManager.stop(projectId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get project with live status info.
|
|
106
|
+
*/
|
|
107
|
+
async getProjectWithStatus(projectId: string): Promise<Project | null> {
|
|
108
|
+
const project = await this.projectManager.getProject(projectId);
|
|
109
|
+
if (!project) return null;
|
|
110
|
+
|
|
111
|
+
const running = this.devServerManager.isRunning(projectId);
|
|
112
|
+
const port = this.devServerManager.getPort(projectId);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...project,
|
|
116
|
+
devPort: port,
|
|
117
|
+
status: running ? 'running' : 'stopped',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* List all projects with live status.
|
|
123
|
+
*/
|
|
124
|
+
async listProjectsWithStatus(): Promise<Project[]> {
|
|
125
|
+
const projects = await this.projectManager.listProjects();
|
|
126
|
+
return projects.map(p => {
|
|
127
|
+
const running = this.devServerManager.isRunning(p.id);
|
|
128
|
+
const port = this.devServerManager.getPort(p.id);
|
|
129
|
+
return {
|
|
130
|
+
...p,
|
|
131
|
+
devPort: port,
|
|
132
|
+
status: running ? 'running' as const : 'stopped' as const,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|