owflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dawid Paluchowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ ![Owflow logo](logo.png)
2
+
3
+ # Owflow
4
+
5
+ **Structured, standards-aware development workflows for OpenCode**
6
+
7
+ Describe what you want to build, and the plugin handles the rest - from specification through implementation to verification - while enforcing your project's coding standards at every step.
8
+
9
+ Started as a simple OpenCode fork of [Maister Claude Code plugin](https://github.com/SkillPanel/maister), Owflow aims to deliver production ready workflows using the full [Opencode](https://opencode.ai/) plugin support.
10
+
11
+ ## What You Get
12
+
13
+ - **Guided workflows** for features, bug fixes, enhancements, performance, migrations, research, and product design
14
+ - **Auto-discovered standards** from your codebase - config files, source patterns, and documentation are analyzed and enforced throughout every workflow
15
+ - **Test-driven implementation** with automated planning, incremental verification, and full test suite runs before completion
16
+ - **Pause and resume** any workflow - state is preserved across sessions
17
+ - **Production readiness checks** including code review, reality assessment, and pragmatic over-engineering detection
18
+
19
+ ## Getting Started
20
+
21
+ ### Prerequisites
22
+
23
+ - [Opencode](https://opencode.ai/) CLI installed and configured
24
+
25
+ ### Installation
26
+
27
+ Add owflow to the `plugin` array in your `opencode.json` ([global or project-level](https://opencode.ai/docs/plugins/#load-order)):
28
+
29
+ ```json
30
+ {
31
+ "plugin": ["owflow"]
32
+ }
33
+ ```
34
+
35
+ ### Initial project setup
36
+
37
+ Initialize your project to auto-detect coding standards and generate project documentation:
38
+
39
+ ```bash
40
+ /flow-init
41
+ ```
42
+
43
+ This scans your codebase and creates `.owflow/` with standards, docs, and task folders. May take a few minutes on larger projects.
44
+
45
+ If you have another project already using Owflow, you can reuse its standards as a starting point:
46
+
47
+ ```bash
48
+ /flow-init --standards-from=/path/to/other-project
49
+ ```
50
+
51
+ ### First Workflow
52
+
53
+ ```bash
54
+ /development Add user profile page with avatar upload
55
+ ```
56
+
57
+ Or just discuss your task with LLM model and then run:
58
+
59
+ ```bash
60
+ /development
61
+ ```
62
+
63
+ The plugin picks up context from your conversation - no arguments needed.
64
+
65
+ ## How It Works
66
+
67
+ 1. You describe a task - either as an argument or just in conversation
68
+ 2. The plugin classifies it (feature, bug, enhancement, etc.) and proposes a workflow
69
+ 3. You confirm, and it guides you through phases: **requirements → spec → plan → implement → verify**
70
+ 4. At each phase, it asks for your input and decisions
71
+ 5. You get tested, verified code with a detailed work log
72
+
73
+ All artifacts are saved in `.owflow/tasks/` organized by type and date.
74
+
75
+ ### Context-Aware Commands
76
+
77
+ Every workflow command works without arguments. The plugin reads your current conversation to extract the task description and auto-detect the task type:
78
+
79
+ ```
80
+ You: "The login page throws a 500 error when the session expires"
81
+ You: /development
82
+ → Auto-detects: bug fix, extracts description from conversation
83
+ ```
84
+
85
+ ```
86
+ You: /standards-update
87
+ → Scans conversation for patterns like "we always use..." or "prefer X over Y"
88
+ ```
89
+
90
+ You can always be explicit when you prefer - arguments and flags simply override the auto-detection.
91
+
92
+ ## Supported Workflows
93
+
94
+ | Command | Use When |
95
+ | ----------------- | ------------------------------------------- |
96
+ | `/development` | Features, bug fixes, enhancements |
97
+ | `/research` | Research with synthesis and solution design |
98
+ | `/performance` | Optimizing speed or resource usage |
99
+ | `/migration` | Changing technologies or patterns |
100
+ | `/product-design` | Product and feature design |
101
+
102
+ Task type (feature/bug/enhancement) is auto-detected from context. Override with `--type=feature|bug|enhancement` if needed. Or use `/work` as a single entry point that routes to the right workflow.
103
+
104
+ ### Quick Commands
105
+
106
+ For smaller tasks that don't need a full workflow:
107
+
108
+ | Command | Use When |
109
+ | --------------- | ----------------------------------------------------------- |
110
+ | `/quick-plan` | You want a plan with standards awareness before coding |
111
+ | `/quick-dev` | You know what to do - just implement with standards applied |
112
+ | `/quick-bugfix` | Quick TDD-driven bug fix — write failing test, fix, verify |
113
+
114
+ ## Standards-Aware Development
115
+
116
+ This is the key differentiator. Owflow doesn't just run workflows - it learns your project's conventions and enforces them:
117
+
118
+ - **`flow-init`** scans config files, source code, and documentation to auto-detect your coding standards
119
+ - **Continuous checking** - standards are consulted before specification, during planning, and while coding (not just at the start)
120
+ - **`/standards-discover`** refreshes standards from your evolving codebase
121
+ - **`/standards-update`** lets you add or refine standards manually, or sync from another project with `--from=PATH`
122
+
123
+ Standards live in `.owflow/docs/standards/` and are indexed in `.owflow/docs/INDEX.md`.
124
+
125
+ **Important**: Do not use plan mode with workflows (see [Best Practices](#best-practices) below).
126
+
127
+ ## Beta Channel
128
+
129
+ Want to try experimental features before they hit stable? Install from the beta channel:
130
+
131
+ TBD
132
+
133
+ ## Best Practices
134
+
135
+ **Don't use plan mode when starting a workflow.** Planning is a built-in part of every workflow — the orchestrator creates specs, plans, and other files as it goes. Claude Code's plan mode restricts file creation, which conflicts with this. Let the workflow handle planning on its own.
136
+
137
+ **Start workflows in a fresh session.** This is especially useful when chaining workflows (e.g., research → development). Research and product-design artifacts already contain all the context needed, so a clean session avoids noise from prior conversation.
138
+
139
+ **Chain workflows by passing a task folder.** If you've completed a research or product-design workflow and want to build on those results, pass the task folder directly:
140
+
141
+ ```bash
142
+ /development .owflow/tasks/research/2026-01-12-oauth-research
143
+ ```
144
+
145
+ You can also append additional instructions to narrow scope or guide the workflow:
146
+
147
+ ```bash
148
+ /development .owflow/tasks/product-design/2026-03-10-dashboard-redesign Implement only phase 1
149
+ ```
150
+
151
+ ## Known Issues
152
+
153
+ **Orchestrator may stall after long phases.** After context compaction (which typically happens after lengthy phases like implementation), the main agent may stop progressing automatically. If you notice it's idle, just type something like "continue" or "proceed" — it will pick up where it left off. You can also re-invoke the workflow in resume mode to reload the orchestrator state:
154
+
155
+ ```bash
156
+ /development .owflow/tasks/development/2026-03-24-my-feature
157
+ ```
158
+
159
+ ## Learn More
160
+
161
+ - [Workflow Details](docs/workflows.md) - phases, examples, and task structure for each workflow type
162
+ - [Full Command Reference](docs/commands.md) - all workflow, review, utility, and quick commands
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Owflow OpenCode Plugin
3
+ * Registers skills/agents/commands via OpenCode's JS event system.
4
+ */
5
+ import path from "node:path";
6
+ import fs from "node:fs";
7
+ import { fileURLToPath } from "url";
8
+ import matter from "gray-matter";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const PLUGIN_ROOT = path.resolve(__dirname, "../..");
11
+ /**
12
+ * Resolves model aliases to the configured small_model value.
13
+ * Maps common small/fast model aliases to OpenCode's small_model config.
14
+ *
15
+ * @param {string} modelValue - The model value from agent frontmatter
16
+ * @param {string} smallModel - The small_model from OpenCode config
17
+ * @returns {string|undefined} - Resolved model value or undefined
18
+ */
19
+ const resolveModelAlias = (modelValue, smallModel) => {
20
+ // Aliases that should map to small_model
21
+ const SMALL_MODEL_ALIASES = [
22
+ "haiku", // Claude Code alias
23
+ "gpt-4o-mini", // OpenAI small model
24
+ "small", // Generic alias
25
+ "fast", // Generic alias
26
+ ];
27
+ // If no model specified or it's 'inherit', return undefined (let OpenCode decide)
28
+ if (!modelValue || modelValue === "inherit") {
29
+ return undefined;
30
+ }
31
+ // If it's a small model alias and we have a configured small_model, use it
32
+ if (SMALL_MODEL_ALIASES.includes(modelValue.toLowerCase())) {
33
+ if (smallModel) {
34
+ return smallModel;
35
+ }
36
+ // If no small_model configured, return undefined (inherit)
37
+ console.warn(`[owflow] Agent uses '${modelValue}' but no small_model configured in opencode.json - falling back to inherit`);
38
+ return undefined;
39
+ }
40
+ // Otherwise, pass through the original value
41
+ return modelValue;
42
+ };
43
+ const loadMarkdownDir = (dirPath) => {
44
+ try {
45
+ return fs
46
+ .readdirSync(dirPath)
47
+ .filter((f) => f.endsWith(".md"))
48
+ .map((f) => {
49
+ const raw = fs.readFileSync(path.join(dirPath, f), "utf8");
50
+ return matter(raw);
51
+ });
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ };
57
+ const prepareAgent = ({ data, content }, smallModel) => {
58
+ return {
59
+ prompt: content,
60
+ ...(data.description && { description: data.description }),
61
+ // Resolve model aliases (haiku, gpt-4o-mini, etc.) to small_model
62
+ ...(() => {
63
+ const resolvedModel = resolveModelAlias(data.model, smallModel);
64
+ return resolvedModel ? { model: resolvedModel } : {};
65
+ })(),
66
+ ...(data.mode && { mode: data.mode }),
67
+ // Add skills array if present
68
+ ...(data.skills && Array.isArray(data.skills) && { skills: data.skills }),
69
+ ...(data.hidden === "true" && { hidden: true }),
70
+ };
71
+ };
72
+ export const OwflowPlugin = async ({ $, directory }) => {
73
+ const agentBySession = new Map();
74
+ return {
75
+ /**
76
+ * Register owflow's skills, commands, and agents so OpenCode discovers
77
+ * them without requiring manual config file edits.
78
+ */
79
+ config: async (config) => {
80
+ let typedConfig = config;
81
+ // Get small_model config for alias resolution
82
+ // Try from config parameter first, fallback to reading opencode.json
83
+ let smallModel = typedConfig.small_model ?? "";
84
+ if (!smallModel) {
85
+ try {
86
+ // Check project-level opencode.json
87
+ const projectConfigPath = path.join(directory, "opencode.json");
88
+ if (fs.existsSync(projectConfigPath)) {
89
+ const rawConfig = fs.readFileSync(projectConfigPath, "utf8");
90
+ const opencodeConfig = JSON.parse(rawConfig);
91
+ smallModel = opencodeConfig.small_model;
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.warn(`Couldn't read or parse opencode.json config - small_model remains undefined`);
96
+ }
97
+ }
98
+ // --- Skills ---
99
+ typedConfig.skills = typedConfig.skills || {};
100
+ typedConfig.skills.paths = typedConfig.skills.paths || [];
101
+ const skillsDir = path.join(PLUGIN_ROOT, "skills");
102
+ if (!typedConfig.skills.paths.includes(skillsDir)) {
103
+ typedConfig.skills.paths.push(skillsDir);
104
+ }
105
+ // --- Commands ---
106
+ typedConfig.command = typedConfig.command || {};
107
+ const commandsDir = path.join(PLUGIN_ROOT, "commands");
108
+ for (const { data, content } of loadMarkdownDir(commandsDir)) {
109
+ const name = data.name;
110
+ if (!name)
111
+ continue;
112
+ // Don't overwrite commands the user has explicitly configured
113
+ if (typedConfig.command[name])
114
+ continue;
115
+ typedConfig.command[name] = {
116
+ template: content,
117
+ ...(data.description && { description: data.description }),
118
+ ...(data.agent && { agent: data.agent }),
119
+ ...(data.model && { model: data.model }),
120
+ ...(data.subtask !== undefined && {
121
+ subtask: data.subtask === "true",
122
+ }),
123
+ };
124
+ }
125
+ // --- Agents ---
126
+ typedConfig.agent = typedConfig.agent || {};
127
+ const agentsDir = path.join(PLUGIN_ROOT, "agents");
128
+ for (const markdown of loadMarkdownDir(agentsDir)) {
129
+ const name = markdown.data.name;
130
+ if (!name)
131
+ continue;
132
+ // Don't overwrite agents the user has explicitly configured
133
+ if (typedConfig.agent[name])
134
+ continue;
135
+ try {
136
+ typedConfig.agent[name] = prepareAgent(markdown, smallModel);
137
+ }
138
+ catch (error) {
139
+ console.warn(`[owflow] Failed to register agent '${name}': ${error.message}`);
140
+ }
141
+ }
142
+ config = typedConfig;
143
+ },
144
+ /**
145
+ * Post-compaction reminder.
146
+ * Injects orchestrator state reminder into the compaction context so the
147
+ * AI knows to re-read orchestrator-state.yml after the context is rebuilt.
148
+ */
149
+ "experimental.session.compacting": async (input, output) => {
150
+ const tasksDir = `${directory}/.owflow/tasks`;
151
+ try {
152
+ await $ `test -d ${tasksDir}`;
153
+ output.context.push(`## owflow Workflow State
154
+ If an orchestrator workflow was active before compaction, you MUST re-read
155
+ orchestrator-state.yml in that task's directory to verify completed_phases
156
+ and determine the next phase. Use the question tool at Phase Gates.`);
157
+ }
158
+ catch {
159
+ // No .owflow/tasks directory — not a owflow project, skip injection
160
+ }
161
+ },
162
+ "chat.message": async (input, _output) => {
163
+ try {
164
+ agentBySession.set(input.sessionID, input.agent ?? "main");
165
+ }
166
+ catch {
167
+ // ignore
168
+ }
169
+ },
170
+ "tool.execute.before": async (input, output) => {
171
+ try {
172
+ if (input.tool !== "bash")
173
+ return;
174
+ const agentName = agentBySession.get(input.sessionID) ?? "main";
175
+ const WHITELIST = [
176
+ "task-group-implementer",
177
+ "test-suite-runner",
178
+ "e2e-test-verifier",
179
+ "user-docs-generator",
180
+ "docs-operator",
181
+ ];
182
+ if (WHITELIST.includes(agentName))
183
+ return;
184
+ const cmd = output?.args?.command ?? "";
185
+ const DESTRUCTIVE = /git\s+stash|git\s+reset\s+--hard|git\s+checkout\s+--\s+\.|git\s+checkout\s+\.\s*(?:$|\s)|git\s+clean|git\s+push\s+(?:--force|-f)|rm\s+-[rf]{2}/i;
186
+ if (DESTRUCTIVE.test(cmd)) {
187
+ throw new Error(`Blocked: destructive command not permitted for agent "${agentName}"`);
188
+ }
189
+ }
190
+ catch (e) {
191
+ // Re-throw only intentional blocks; swallow unexpected errors
192
+ if (e.message?.startsWith("Blocked:"))
193
+ throw e;
194
+ }
195
+ },
196
+ };
197
+ };
@@ -0,0 +1,302 @@
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();
@@ -0,0 +1 @@
1
+ // workaround til something is not done with issue https://github.com/anomalyco/opencode/issues/24065
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "owflow",
3
+ "version": "0.1.0",
4
+ "description": "Spec-driven development AI workflow for OpenCode",
5
+ "type": "module",
6
+ "module": "index.ts",
7
+ "main": "./dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "build": "rm -rf dist && tsc"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "plugin",
19
+ "workflow",
20
+ "spec-driven",
21
+ "development"
22
+ ],
23
+ "author": "palucdev",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@opencode-ai/plugin": "^1.14.44",
27
+ "@types/bun": "^1.3.13",
28
+ "gray-matter": "^4.0.3"
29
+ },
30
+ "peerDependencies": {
31
+ "typescript": "^5"
32
+ },
33
+ "engines": {
34
+ "node": "^25"
35
+ }
36
+ }