owflow 0.1.4 → 0.1.6

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 (42) hide show
  1. package/README.md +34 -21
  2. package/dist/__tests__/verify_template.test.js +102 -0
  3. package/dist/agents/task-classifier.md +7 -19
  4. package/dist/commands/diagrams-mermaid.md +37 -0
  5. package/dist/commands/html-renderer.md +37 -0
  6. package/dist/commands/quick-dev.md +48 -6
  7. package/dist/commands/quick-plan.md +39 -4
  8. package/dist/commands/work.md +13 -17
  9. package/dist/index.js +4 -0
  10. package/dist/skills/development/SKILL.md +98 -115
  11. package/dist/skills/diagrams-mermaid/SKILL.md +156 -0
  12. package/dist/skills/diagrams-mermaid/references/reference.md +148 -0
  13. package/dist/skills/flow-init/SKILL.md +15 -0
  14. package/dist/skills/html-renderer/SKILL.md +159 -0
  15. package/dist/skills/html-renderer/references/index.html +318 -0
  16. package/dist/skills/html-renderer/references/styles.css +388 -0
  17. package/dist/skills/html-renderer/references/tokens.json +104 -0
  18. package/dist/skills/migration/SKILL.md +14 -33
  19. package/dist/skills/orchestrator-framework/references/orchestrator-creation-checklist.md +1 -0
  20. package/dist/skills/orchestrator-framework/references/orchestrator-patterns.md +4 -59
  21. package/dist/skills/performance/SKILL.md +14 -30
  22. package/dist/skills/quick-bugfix/SKILL.md +96 -26
  23. package/dist/skills/research/SKILL.md +11 -42
  24. package/dist/templates/orchestrator-state-base.yml +17 -0
  25. package/dist/templates/orchestrator-state-development.yml +94 -0
  26. package/dist/templates/orchestrator-state-migration.yml +55 -0
  27. package/dist/templates/orchestrator-state-performance.yml +53 -0
  28. package/dist/templates/orchestrator-state-research.yml +61 -0
  29. package/dist/templates/quick-bugfix-task.yml +10 -0
  30. package/dist/templates/quick-dev-task.yml +10 -0
  31. package/dist/templates/quick-plan-task.yml +11 -0
  32. package/dist/tools/verify_template.js +61 -0
  33. package/package.json +4 -3
  34. package/dist/agents/ui-mockup-generator.md +0 -369
  35. package/dist/commands/product-design.md +0 -37
  36. package/dist/skills/product-design/SKILL.md +0 -843
  37. package/dist/skills/product-design/references/characteristic-detection.md +0 -92
  38. package/dist/skills/product-design/references/interaction-patterns.md +0 -206
  39. package/dist/skills/product-design/references/visual-companion.md +0 -190
  40. package/dist/skills/product-design/server/index.js +0 -302
  41. package/dist/skills/product-design/server/index.ts +0 -359
  42. package/dist/skills/product-design/server/template.html +0 -256
@@ -1,302 +0,0 @@
1
- import http from "node:http";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- // Parse --task-path from CLI args
7
- const taskPathArg = process.argv.find((a) => a.startsWith("--task-path="));
8
- const taskPath = taskPathArg ? taskPathArg.split("=")[1] : null;
9
- if (!taskPath) {
10
- console.warn("[visual-companion] No --task-path provided. Mockups will NOT be saved to disk.");
11
- }
12
- // In-memory state: array of all mockups (screens)
13
- const mockups = [];
14
- let latestId = null;
15
- let version = 0;
16
- const sseClients = [];
17
- function slugify(title) {
18
- return (title
19
- .toLowerCase()
20
- .replace(/[^a-z0-9]+/g, "-")
21
- .replace(/^-|-$/g, "") || "untitled");
22
- }
23
- // Save a rendered standalone HTML file to disk. Returns true if saved, false if skipped.
24
- function saveToDisk(mockup) {
25
- if (!taskPath) {
26
- console.warn(`[visual-companion] Skipping disk save for "${mockup.id}" — no task path configured.`);
27
- return false;
28
- }
29
- const dir = path.join(taskPath, "analysis", "mockups");
30
- fs.mkdirSync(dir, { recursive: true });
31
- const html = renderScreen(mockup);
32
- const filePath = path.join(dir, `${mockup.id}.html`);
33
- fs.writeFileSync(filePath, html, "utf-8");
34
- return true;
35
- }
36
- // Render a single screen page with navigation
37
- function renderScreen(mockup) {
38
- const templatePath = path.join(__dirname, "template.html");
39
- let html = fs.readFileSync(templatePath, "utf-8");
40
- const title = mockup.title || "Untitled";
41
- const content = `<style>${mockup.css || ""}</style>\n<div id="mockup-content">${mockup.html || ""}</div>`;
42
- const annotations = JSON.stringify(mockup.annotations || []);
43
- // Build screen nav
44
- const navItems = mockups
45
- .map((m) => `<a href="/screen/${m.id}" class="nav-screen${m.id === mockup.id ? " active" : ""}">${m.title}</a>`)
46
- .join("");
47
- const idx = mockups.indexOf(mockup);
48
- const prev = idx > 0 ? mockups[idx - 1] : null;
49
- const next = idx < mockups.length - 1 ? mockups[idx + 1] : null;
50
- const prevLink = prev
51
- ? `<a href="/screen/${prev.id}" class="nav-arrow">&larr; ${prev.title}</a>`
52
- : "<span></span>";
53
- const nextLink = next
54
- ? `<a href="/screen/${next.id}" class="nav-arrow">${next.title} &rarr;</a>`
55
- : "<span></span>";
56
- const nav = mockups.length > 1
57
- ? `<nav class="screen-nav"><div class="nav-screens">${navItems}</div><div class="nav-prevnext">${prevLink}${nextLink}</div></nav>`
58
- : "";
59
- html = html.replace("{{TITLE}}", title).replace("{{TITLE}}", title);
60
- html = html.replace("{{NAV}}", nav);
61
- html = html.replace("{{CONTENT}}", content);
62
- html = html.replace("{{ANNOTATIONS}}", annotations);
63
- return html;
64
- }
65
- // Render the gallery index page
66
- function renderGallery() {
67
- const templatePath = path.join(__dirname, "template.html");
68
- let html = fs.readFileSync(templatePath, "utf-8");
69
- const title = `Design Gallery — ${mockups.length} screen${mockups.length !== 1 ? "s" : ""}`;
70
- let content;
71
- if (mockups.length === 0) {
72
- content =
73
- '<div class="placeholder">Waiting for design mockups...<br>The orchestrator will send screens here.</div>';
74
- }
75
- else {
76
- const cards = mockups
77
- .map((m) => `
78
- <a href="/screen/${m.id}" class="gallery-card">
79
- <div class="gallery-card-title">${m.title}</div>
80
- <div class="gallery-card-preview"><style scoped>${m.css || ""}</style>${m.html || ""}</div>
81
- </a>
82
- `)
83
- .join("");
84
- content = `<div class="gallery-grid">${cards}</div>`;
85
- }
86
- html = html.replace("{{TITLE}}", title).replace("{{TITLE}}", title);
87
- html = html.replace("{{NAV}}", "");
88
- html = html.replace("{{CONTENT}}", content);
89
- html = html.replace("{{ANNOTATIONS}}", "[]");
90
- return html;
91
- }
92
- // Parse JSON body from request
93
- function parseBody(req) {
94
- return new Promise((resolve, reject) => {
95
- let data = "";
96
- req.on("data", (chunk) => {
97
- data += chunk;
98
- });
99
- req.on("end", () => {
100
- try {
101
- resolve(data ? JSON.parse(data) : {});
102
- }
103
- catch (err) {
104
- reject(new Error("Invalid JSON body"));
105
- }
106
- });
107
- req.on("error", reject);
108
- });
109
- }
110
- // Notify all SSE clients
111
- function notifyClients() {
112
- for (let i = sseClients.length - 1; i >= 0; i--) {
113
- try {
114
- sseClients[i].write("data: refresh\n\n");
115
- }
116
- catch {
117
- sseClients.splice(i, 1);
118
- }
119
- }
120
- }
121
- function jsonResponse(res, statusCode, body) {
122
- const payload = JSON.stringify(body);
123
- res.writeHead(statusCode, {
124
- "Content-Type": "application/json",
125
- "Content-Length": Buffer.byteLength(payload),
126
- });
127
- res.end(payload);
128
- }
129
- function htmlResponse(res, html) {
130
- res.writeHead(200, {
131
- "Content-Type": "text/html",
132
- "Content-Length": Buffer.byteLength(html),
133
- });
134
- res.end(html);
135
- }
136
- // Main request handler
137
- async function handler(req, res) {
138
- const url = new URL(req.url, `http://${req.headers.host}`);
139
- try {
140
- // GET /status
141
- if (req.method === "GET" && url.pathname === "/status") {
142
- jsonResponse(res, 200, {
143
- status: "ok",
144
- version: "1.0.0",
145
- port: activePort,
146
- screens: mockups.length,
147
- taskPath: taskPath || null,
148
- persistence: !!taskPath,
149
- });
150
- return;
151
- }
152
- // POST /shutdown
153
- if (req.method === "POST" && url.pathname === "/shutdown") {
154
- jsonResponse(res, 200, { status: "shutting_down" });
155
- cleanupPidFile();
156
- setTimeout(() => process.exit(0), 100);
157
- return;
158
- }
159
- // GET /events (SSE)
160
- if (req.method === "GET" && url.pathname === "/events") {
161
- res.writeHead(200, {
162
- "Content-Type": "text/event-stream",
163
- "Cache-Control": "no-cache",
164
- Connection: "keep-alive",
165
- });
166
- res.write("data: connected\n\n");
167
- sseClients.push(res);
168
- req.on("close", () => {
169
- const idx = sseClients.indexOf(res);
170
- if (idx !== -1)
171
- sseClients.splice(idx, 1);
172
- });
173
- return;
174
- }
175
- // POST /update
176
- if (req.method === "POST" && url.pathname === "/update") {
177
- const body = await parseBody(req);
178
- const id = slugify(body.title || "untitled");
179
- const mockup = {
180
- id,
181
- type: body.type || "mockup",
182
- title: body.title || "Untitled",
183
- html: body.html || "",
184
- css: body.css || "",
185
- annotations: body.annotations || [],
186
- };
187
- // Update existing or add new
188
- const existingIdx = mockups.findIndex((m) => m.id === id);
189
- if (existingIdx !== -1) {
190
- mockups[existingIdx] = mockup;
191
- }
192
- else {
193
- mockups.push(mockup);
194
- }
195
- latestId = id;
196
- version++;
197
- const saved = saveToDisk(mockup);
198
- notifyClients();
199
- jsonResponse(res, 200, {
200
- status: "updated",
201
- version,
202
- id,
203
- screens: mockups.length,
204
- saved,
205
- });
206
- return;
207
- }
208
- // GET /screen/:id
209
- const screenMatch = url.pathname.match(/^\/screen\/([a-z0-9-]+)$/);
210
- if (req.method === "GET" && screenMatch) {
211
- const mockup = mockups.find((m) => m.id === screenMatch[1]);
212
- if (!mockup) {
213
- jsonResponse(res, 404, { error: "Screen not found" });
214
- return;
215
- }
216
- htmlResponse(res, renderScreen(mockup));
217
- return;
218
- }
219
- // GET /latest
220
- if (req.method === "GET" && url.pathname === "/latest") {
221
- const mockup = mockups.find((m) => m.id === latestId);
222
- if (!mockup) {
223
- htmlResponse(res, renderGallery());
224
- return;
225
- }
226
- htmlResponse(res, renderScreen(mockup));
227
- return;
228
- }
229
- // GET / (gallery)
230
- if (req.method === "GET" && url.pathname === "/") {
231
- htmlResponse(res, renderGallery());
232
- return;
233
- }
234
- jsonResponse(res, 404, { error: "Not found" });
235
- }
236
- catch (err) {
237
- console.error("Request error:", err.message);
238
- jsonResponse(res, 500, { error: err.message });
239
- }
240
- }
241
- // PID file management
242
- function pidFilePath() {
243
- if (!taskPath)
244
- return null;
245
- return path.join(taskPath, "analysis", "mockups", ".visual-companion.pid");
246
- }
247
- function writePidFile() {
248
- const p = pidFilePath();
249
- if (!p)
250
- return;
251
- fs.mkdirSync(path.dirname(p), { recursive: true });
252
- fs.writeFileSync(p, String(process.pid), "utf-8");
253
- }
254
- function cleanupPidFile() {
255
- const p = pidFilePath();
256
- if (p)
257
- try {
258
- fs.unlinkSync(p);
259
- }
260
- catch { }
261
- }
262
- process.on("SIGTERM", () => {
263
- cleanupPidFile();
264
- process.exit(0);
265
- });
266
- process.on("SIGINT", () => {
267
- cleanupPidFile();
268
- process.exit(0);
269
- });
270
- // Port fallback logic
271
- let activePort = null;
272
- function tryPort(port) {
273
- return new Promise((resolve, reject) => {
274
- const server = http.createServer(handler);
275
- server.listen(port, () => resolve(server));
276
- server.on("error", reject);
277
- });
278
- }
279
- async function start() {
280
- const ports = [3847, 3848, 3849, 3850];
281
- for (const port of ports) {
282
- try {
283
- await tryPort(port);
284
- activePort = port;
285
- console.log(`Visual companion server running at http://localhost:${port}`);
286
- if (taskPath)
287
- console.log(`Saving mockups to: ${path.join(taskPath, "analysis", "mockups")}`);
288
- writePidFile();
289
- return;
290
- }
291
- catch (err) {
292
- if (err.code === "EADDRINUSE") {
293
- console.error(`Port ${port} in use, trying next...`);
294
- continue;
295
- }
296
- throw err;
297
- }
298
- }
299
- console.error("All ports (3847-3850) in use. Cannot start server.");
300
- process.exit(1);
301
- }
302
- start();
@@ -1,359 +0,0 @@
1
- import http from "node:http";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
- // Parse --task-path from CLI args
9
- const taskPathArg = process.argv.find((a) => a.startsWith("--task-path="));
10
- const taskPath = taskPathArg ? taskPathArg.split("=")[1] : null;
11
-
12
- if (!taskPath) {
13
- console.warn(
14
- "[visual-companion] No --task-path provided. Mockups will NOT be saved to disk.",
15
- );
16
- }
17
-
18
- interface Mockup {
19
- id: string;
20
- title?: string;
21
- html?: string;
22
- css?: string;
23
- annotations?: any[];
24
- }
25
-
26
- // In-memory state: array of all mockups (screens)
27
- const mockups: Array<Mockup> = [];
28
- let latestId: string | null = null;
29
- let version = 0;
30
- const sseClients: Array<any> = [];
31
-
32
- function slugify(title: string): string {
33
- return (
34
- title
35
- .toLowerCase()
36
- .replace(/[^a-z0-9]+/g, "-")
37
- .replace(/^-|-$/g, "") || "untitled"
38
- );
39
- }
40
-
41
- // Save a rendered standalone HTML file to disk. Returns true if saved, false if skipped.
42
- function saveToDisk(mockup: Mockup): boolean {
43
- if (!taskPath) {
44
- console.warn(
45
- `[visual-companion] Skipping disk save for "${mockup.id}" — no task path configured.`,
46
- );
47
- return false;
48
- }
49
- const dir = path.join(taskPath, "analysis", "mockups");
50
- fs.mkdirSync(dir, { recursive: true });
51
-
52
- const html = renderScreen(mockup);
53
- const filePath = path.join(dir, `${mockup.id}.html`);
54
- fs.writeFileSync(filePath, html, "utf-8");
55
- return true;
56
- }
57
-
58
- // Render a single screen page with navigation
59
- function renderScreen(mockup: Mockup) {
60
- const templatePath = path.join(__dirname, "template.html");
61
- let html = fs.readFileSync(templatePath, "utf-8");
62
-
63
- const title = mockup.title || "Untitled";
64
- const content = `<style>${mockup.css || ""}</style>\n<div id="mockup-content">${mockup.html || ""}</div>`;
65
- const annotations = JSON.stringify(mockup.annotations || []);
66
-
67
- // Build screen nav
68
- const navItems = mockups
69
- .map(
70
- (m) =>
71
- `<a href="/screen/${m.id}" class="nav-screen${m.id === mockup.id ? " active" : ""}">${m.title}</a>`,
72
- )
73
- .join("");
74
-
75
- const idx = mockups.indexOf(mockup);
76
- const prev = idx > 0 ? mockups[idx - 1] : null;
77
- const next = idx < mockups.length - 1 ? mockups[idx + 1] : null;
78
- const prevLink = prev
79
- ? `<a href="/screen/${prev.id}" class="nav-arrow">&larr; ${prev.title}</a>`
80
- : "<span></span>";
81
- const nextLink = next
82
- ? `<a href="/screen/${next.id}" class="nav-arrow">${next.title} &rarr;</a>`
83
- : "<span></span>";
84
-
85
- const nav =
86
- mockups.length > 1
87
- ? `<nav class="screen-nav"><div class="nav-screens">${navItems}</div><div class="nav-prevnext">${prevLink}${nextLink}</div></nav>`
88
- : "";
89
-
90
- html = html.replace("{{TITLE}}", title).replace("{{TITLE}}", title);
91
- html = html.replace("{{NAV}}", nav);
92
- html = html.replace("{{CONTENT}}", content);
93
- html = html.replace("{{ANNOTATIONS}}", annotations);
94
-
95
- return html;
96
- }
97
-
98
- // Render the gallery index page
99
- function renderGallery() {
100
- const templatePath = path.join(__dirname, "template.html");
101
- let html = fs.readFileSync(templatePath, "utf-8");
102
-
103
- const title = `Design Gallery — ${mockups.length} screen${mockups.length !== 1 ? "s" : ""}`;
104
-
105
- let content;
106
- if (mockups.length === 0) {
107
- content =
108
- '<div class="placeholder">Waiting for design mockups...<br>The orchestrator will send screens here.</div>';
109
- } else {
110
- const cards = mockups
111
- .map(
112
- (m) => `
113
- <a href="/screen/${m.id}" class="gallery-card">
114
- <div class="gallery-card-title">${m.title}</div>
115
- <div class="gallery-card-preview"><style scoped>${m.css || ""}</style>${m.html || ""}</div>
116
- </a>
117
- `,
118
- )
119
- .join("");
120
- content = `<div class="gallery-grid">${cards}</div>`;
121
- }
122
-
123
- html = html.replace("{{TITLE}}", title).replace("{{TITLE}}", title);
124
- html = html.replace("{{NAV}}", "");
125
- html = html.replace("{{CONTENT}}", content);
126
- html = html.replace("{{ANNOTATIONS}}", "[]");
127
-
128
- return html;
129
- }
130
-
131
- // Parse JSON body from request
132
- function parseBody(req: http.IncomingMessage): Promise<any> {
133
- return new Promise((resolve, reject) => {
134
- let data = "";
135
- req.on("data", (chunk) => {
136
- data += chunk;
137
- });
138
- req.on("end", () => {
139
- try {
140
- resolve(data ? JSON.parse(data) : {});
141
- } catch (err) {
142
- reject(new Error("Invalid JSON body"));
143
- }
144
- });
145
- req.on("error", reject);
146
- });
147
- }
148
-
149
- // Notify all SSE clients
150
- function notifyClients() {
151
- for (let i = sseClients.length - 1; i >= 0; i--) {
152
- try {
153
- sseClients[i].write("data: refresh\n\n");
154
- } catch {
155
- sseClients.splice(i, 1);
156
- }
157
- }
158
- }
159
-
160
- function jsonResponse(res: http.ServerResponse, statusCode: number, body: any) {
161
- const payload = JSON.stringify(body);
162
- res.writeHead(statusCode, {
163
- "Content-Type": "application/json",
164
- "Content-Length": Buffer.byteLength(payload),
165
- });
166
- res.end(payload);
167
- }
168
-
169
- function htmlResponse(res: http.ServerResponse, html: string) {
170
- res.writeHead(200, {
171
- "Content-Type": "text/html",
172
- "Content-Length": Buffer.byteLength(html),
173
- });
174
- res.end(html);
175
- }
176
-
177
- // Main request handler
178
- async function handler(req: http.IncomingMessage, res: http.ServerResponse) {
179
- const url = new URL(req.url!, `http://${req.headers.host}`);
180
-
181
- try {
182
- // GET /status
183
- if (req.method === "GET" && url.pathname === "/status") {
184
- jsonResponse(res, 200, {
185
- status: "ok",
186
- version: "1.0.0",
187
- port: activePort,
188
- screens: mockups.length,
189
- taskPath: taskPath || null,
190
- persistence: !!taskPath,
191
- });
192
- return;
193
- }
194
-
195
- // POST /shutdown
196
- if (req.method === "POST" && url.pathname === "/shutdown") {
197
- jsonResponse(res, 200, { status: "shutting_down" });
198
- cleanupPidFile();
199
- setTimeout(() => process.exit(0), 100);
200
- return;
201
- }
202
-
203
- // GET /events (SSE)
204
- if (req.method === "GET" && url.pathname === "/events") {
205
- res.writeHead(200, {
206
- "Content-Type": "text/event-stream",
207
- "Cache-Control": "no-cache",
208
- Connection: "keep-alive",
209
- });
210
- res.write("data: connected\n\n");
211
- sseClients.push(res);
212
- req.on("close", () => {
213
- const idx = sseClients.indexOf(res);
214
- if (idx !== -1) sseClients.splice(idx, 1);
215
- });
216
- return;
217
- }
218
-
219
- // POST /update
220
- if (req.method === "POST" && url.pathname === "/update") {
221
- const body = await parseBody(req);
222
- const id = slugify(body.title || "untitled");
223
-
224
- const mockup = {
225
- id,
226
- type: body.type || "mockup",
227
- title: body.title || "Untitled",
228
- html: body.html || "",
229
- css: body.css || "",
230
- annotations: body.annotations || [],
231
- };
232
-
233
- // Update existing or add new
234
- const existingIdx = mockups.findIndex((m) => m.id === id);
235
- if (existingIdx !== -1) {
236
- mockups[existingIdx] = mockup;
237
- } else {
238
- mockups.push(mockup);
239
- }
240
-
241
- latestId = id;
242
- version++;
243
- const saved = saveToDisk(mockup);
244
- notifyClients();
245
- jsonResponse(res, 200, {
246
- status: "updated",
247
- version,
248
- id,
249
- screens: mockups.length,
250
- saved,
251
- });
252
- return;
253
- }
254
-
255
- // GET /screen/:id
256
- const screenMatch = url.pathname.match(/^\/screen\/([a-z0-9-]+)$/);
257
- if (req.method === "GET" && screenMatch) {
258
- const mockup = mockups.find((m) => m.id === screenMatch[1]);
259
- if (!mockup) {
260
- jsonResponse(res, 404, { error: "Screen not found" });
261
- return;
262
- }
263
- htmlResponse(res, renderScreen(mockup));
264
- return;
265
- }
266
-
267
- // GET /latest
268
- if (req.method === "GET" && url.pathname === "/latest") {
269
- const mockup = mockups.find((m) => m.id === latestId);
270
- if (!mockup) {
271
- htmlResponse(res, renderGallery());
272
- return;
273
- }
274
- htmlResponse(res, renderScreen(mockup));
275
- return;
276
- }
277
-
278
- // GET / (gallery)
279
- if (req.method === "GET" && url.pathname === "/") {
280
- htmlResponse(res, renderGallery());
281
- return;
282
- }
283
-
284
- jsonResponse(res, 404, { error: "Not found" });
285
- } catch (err: any) {
286
- console.error("Request error:", err.message);
287
- jsonResponse(res, 500, { error: err.message });
288
- }
289
- }
290
-
291
- // PID file management
292
- function pidFilePath() {
293
- if (!taskPath) return null;
294
- return path.join(taskPath, "analysis", "mockups", ".visual-companion.pid");
295
- }
296
-
297
- function writePidFile() {
298
- const p = pidFilePath();
299
- if (!p) return;
300
- fs.mkdirSync(path.dirname(p), { recursive: true });
301
- fs.writeFileSync(p, String(process.pid), "utf-8");
302
- }
303
-
304
- function cleanupPidFile() {
305
- const p = pidFilePath();
306
- if (p)
307
- try {
308
- fs.unlinkSync(p);
309
- } catch {}
310
- }
311
-
312
- process.on("SIGTERM", () => {
313
- cleanupPidFile();
314
- process.exit(0);
315
- });
316
- process.on("SIGINT", () => {
317
- cleanupPidFile();
318
- process.exit(0);
319
- });
320
-
321
- // Port fallback logic
322
- let activePort: number | null = null;
323
-
324
- function tryPort(port: number) {
325
- return new Promise<http.Server>((resolve, reject) => {
326
- const server = http.createServer(handler);
327
- server.listen(port, () => resolve(server));
328
- server.on("error", reject);
329
- });
330
- }
331
-
332
- async function start() {
333
- const ports = [3847, 3848, 3849, 3850];
334
- for (const port of ports) {
335
- try {
336
- await tryPort(port);
337
- activePort = port;
338
- console.log(
339
- `Visual companion server running at http://localhost:${port}`,
340
- );
341
- if (taskPath)
342
- console.log(
343
- `Saving mockups to: ${path.join(taskPath, "analysis", "mockups")}`,
344
- );
345
- writePidFile();
346
- return;
347
- } catch (err: any) {
348
- if (err.code === "EADDRINUSE") {
349
- console.error(`Port ${port} in use, trying next...`);
350
- continue;
351
- }
352
- throw err;
353
- }
354
- }
355
- console.error("All ports (3847-3850) in use. Cannot start server.");
356
- process.exit(1);
357
- }
358
-
359
- start();