@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.
Files changed (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. 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
+ }