boxglow 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 +21 -0
- package/README.md +169 -0
- package/bin/boxglow.js +2834 -0
- package/dist/assets/index-BgLclRaq.css +1 -0
- package/dist/assets/index-BxV1l9_P.js +3404 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +15 -0
- package/dist/icon-180.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +37 -0
- package/docs/AGENTS_SNIPPET.md +67 -0
- package/examples/logic-daw/boxglow.json +3379 -0
- package/package.json +82 -0
package/bin/boxglow.js
ADDED
|
@@ -0,0 +1,2834 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/main.ts
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// cli/mcp.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var text = (s) => ({ content: [{ type: "text", text: s.trim() || "(\u51FA\u529B\u306A\u3057)" }] });
|
|
11
|
+
var safe = (run, argv2) => {
|
|
12
|
+
try {
|
|
13
|
+
return text(run(argv2));
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return { ...text(`[boxglow] ${e instanceof Error ? e.message : String(e)}`), isError: true };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function opt(argv2, name, value) {
|
|
19
|
+
if (value === void 0 || value === false || value === "") return;
|
|
20
|
+
if (value === true) {
|
|
21
|
+
argv2.push(`--${name}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const v of Array.isArray(value) ? value : [value]) argv2.push(`--${name}`, String(v));
|
|
25
|
+
}
|
|
26
|
+
async function startMcp(run) {
|
|
27
|
+
const server = new McpServer({ name: "boxglow", version: "0.1.0" });
|
|
28
|
+
const block = z.string().describe("\u7BB1\u306E B \u756A\u53F7 (B12) \u304B\u984C\u540D");
|
|
29
|
+
server.registerTool("boxglow_status", { description: "\u8A08\u753B\u306E\u4ECA\u306E\u72B6\u6CC1: \u5224\u65AD\u5F85\u3061\u30FB\u4F5C\u696D\u4E2D\u30FB\u6B21\u306E\u5019\u88DC\u30FB\u968E\u5C64\u306E\u4E00\u89A7 (\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6700\u521D\u306B\u8AAD\u3080)", inputSchema: {} }, async () => safe(run, ["status"]));
|
|
30
|
+
server.registerTool("boxglow_show", { description: "\u7BB1\u306E\u8A73\u7D30 (\u5165\u51FA\u529B\u30FB\u7DDA\u30FB\u5224\u65AD\u30FB\u6210\u679C\u7269)", inputSchema: { block } }, async ({ block: b }) => safe(run, ["show", b]));
|
|
31
|
+
server.registerTool("boxglow_add", {
|
|
32
|
+
description: "\u7BB1\u3092\u8DB3\u3059\u3002\u51FA\u529B (\u6210\u679C\u7269\u306E\u540D\u524D) \u3092\u5FC5\u305A\u6C7A\u3081\u308B\u3002parent \u3092\u7701\u304F\u3068\u6700\u521D\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u306E\u76F4\u4E0B (\u5927\u9805\u76EE) \u306B\u5165\u308B",
|
|
33
|
+
inputSchema: { title: z.string(), out: z.string().describe("\u51FA\u529B\u306E\u540D\u524D (\u5177\u4F53\u7684\u306A\u6210\u679C\u7269: \u30D5\u30A1\u30A4\u30EB\u30FBURL\u30FBPR)"), parent: block.optional(), in: z.array(z.string()).optional().describe("\u5165\u529B\u306E\u540D\u524D"), category: z.string().optional().describe("design/build/verify/evaluate/study/research/ui/improve/fix/docs/ops/other"), note: z.string().optional() }
|
|
34
|
+
}, async (a) => {
|
|
35
|
+
const argv2 = ["add", a.title];
|
|
36
|
+
opt(argv2, "out", a.out);
|
|
37
|
+
opt(argv2, "parent", a.parent);
|
|
38
|
+
opt(argv2, "in", a.in);
|
|
39
|
+
opt(argv2, "category", a.category);
|
|
40
|
+
opt(argv2, "note", a.note);
|
|
41
|
+
return safe(run, argv2);
|
|
42
|
+
});
|
|
43
|
+
server.registerTool("boxglow_split", {
|
|
44
|
+
description: '\u5927\u304D\u3044\u7BB1\u3092\u4E2D\u306E\u7BB1\u306B\u5206\u89E3\u3059\u308B\u3002spec \u306F { blocks: [{ title, in: [\u540D\u524D], out: \u540D\u524D, category? }], wires: ["A.\u51FA\u529B -> B.\u5165\u529B"] } \u306E\u5F62',
|
|
45
|
+
inputSchema: { block, spec: z.record(z.string(), z.unknown()).describe("\u5206\u89E3\u306E\u6307\u5B9A (JSON)") }
|
|
46
|
+
}, async ({ block: b, spec }) => safe(run, ["split", b, "--spec", JSON.stringify(spec)]));
|
|
47
|
+
server.registerTool("boxglow_connect", { description: "\u7DDA\u3092\u3064\u306A\u3050: <\u984C\u540D.\u51FA\u529B\u540D> \u304B\u3089 <\u984C\u540D.\u5165\u529B\u540D> \u3078 (\u89AA\u306E\u5165\u529B\u306F project.\u5165\u529B\u540D\u3001\u89AA\u306E\u51FA\u529B\u3078\u306F \u984C\u540D.\u51FA\u529B\u540D -> project.\u51FA\u529B\u540D)", inputSchema: { from: z.string(), to: z.string() } }, async ({ from, to }) => safe(run, ["connect", from, to]));
|
|
48
|
+
server.registerTool("boxglow_disconnect", { description: "\u7DDA\u3092\u5916\u3059", inputSchema: { from: z.string(), to: z.string() } }, async ({ from, to }) => safe(run, ["disconnect", from, to]));
|
|
49
|
+
server.registerTool("boxglow_port", { description: "\u7BB1\u306B\u5165\u529B / \u51FA\u529B\u3092\u8DB3\u3059\u3001\u540D\u524D\u3092\u5909\u3048\u308B", inputSchema: { block, in: z.array(z.string()).optional(), out: z.array(z.string()).optional(), rename: z.string().optional().describe("\u65E7=\u65B0") } }, async (a) => {
|
|
50
|
+
const argv2 = ["port", a.block];
|
|
51
|
+
opt(argv2, "in", a.in);
|
|
52
|
+
opt(argv2, "out", a.out);
|
|
53
|
+
opt(argv2, "rename", a.rename);
|
|
54
|
+
return safe(run, argv2);
|
|
55
|
+
});
|
|
56
|
+
server.registerTool("boxglow_move", { description: "\u7BB1\u3092\u5225\u306E\u89AA\u306E\u4E2D\u3078\u79FB\u3059 (\u7DDA\u306F\u3064\u306A\u304E\u76F4\u3055\u308C\u308B)", inputSchema: { block, parent: z.string().describe("\u79FB\u3059\u5148\u306E\u7BB1\u304B project") } }, async ({ block: b, parent }) => safe(run, ["move", b, "--parent", parent]));
|
|
57
|
+
server.registerTool("boxglow_remove", { description: "\u7BB1\u3092\u6D88\u3059 (\u4E2D\u306B\u7BB1\u304C\u3042\u308C\u3070 force)", inputSchema: { block, force: z.boolean().optional() } }, async ({ block: b, force }) => {
|
|
58
|
+
const argv2 = ["remove", b];
|
|
59
|
+
opt(argv2, "force", force);
|
|
60
|
+
return safe(run, argv2);
|
|
61
|
+
});
|
|
62
|
+
server.registerTool("boxglow_start", { description: "\u4F5C\u696D\u3092\u59CB\u3081\u308B (\u4F5C\u696D\u4E2D\u306E\u672D\u304C\u4ED8\u304F)\u3002\u540C\u6642\u306B\u4F5C\u696D\u4E2D\u306B\u3059\u308B\u7BB1\u306F 1\u301C2 \u500B\u307E\u3067", inputSchema: { block, note: z.string().optional().describe("\u4F55\u3092\u3059\u308B\u304B") } }, async ({ block: b, note }) => {
|
|
63
|
+
const argv2 = ["start", b];
|
|
64
|
+
opt(argv2, "note", note);
|
|
65
|
+
return safe(run, argv2);
|
|
66
|
+
});
|
|
67
|
+
server.registerTool("boxglow_done", { description: "\u5B8C\u4E86\u306B\u3059\u308B\u3002\u6210\u679C\u7269 (\u540D\u524D=\u30D1\u30B9\u304B URL) \u3092\u4ED8\u3051\u308B\u3002Git \u306E\u30D5\u30A1\u30A4\u30EB\u306A\u3089 commit+path+blob \u304C\u8A18\u9332\u3055\u308C\u308B", inputSchema: { block, artifact: z.array(z.string()).optional().describe("\u540D\u524D=\u30D1\u30B9 \u307E\u305F\u306F \u540D\u524D=URL"), note: z.string().optional(), output: z.string().optional().describe("\u51FA\u529B\u306E\u540D\u524D\u3092\u5909\u3048\u308B\u3068\u304D") } }, async (a) => {
|
|
68
|
+
const argv2 = ["done", a.block];
|
|
69
|
+
opt(argv2, "artifact", a.artifact);
|
|
70
|
+
opt(argv2, "note", a.note);
|
|
71
|
+
opt(argv2, "output", a.output);
|
|
72
|
+
return safe(run, argv2);
|
|
73
|
+
});
|
|
74
|
+
server.registerTool("boxglow_blocked", { description: "\u8A70\u307E\u3063\u305F (\u56F0\u3063\u3066\u3044\u308B\u3053\u3068\u3092\u66F8\u3044\u3066\u4ED6\u306E\u7BB1\u3078\u79FB\u308B)", inputSchema: { block, note: z.string() } }, async ({ block: b, note }) => safe(run, ["blocked", b, "--note", note]));
|
|
75
|
+
server.registerTool("boxglow_ask", {
|
|
76
|
+
description: "\u4EBA\u306E\u5224\u65AD\u304C\u8981\u308B\u8CEA\u554F\u3092\u6B8B\u3059 (\u9078\u629E\u80A2\u4ED8\u304D)\u3002\u8CEA\u554F\u3060\u3051\u3067\u5224\u65AD\u3067\u304D\u308B\u3088\u3046\u306B\u3001\u524D\u63D0\u30FB\u6BD4\u8F03\u30FB\u5F71\u97FF\u3092 context \u306B\u66F8\u304F\u3002AI \u304C\u81EA\u5206\u3067\u9078\u3076\u3068\u304D\u3082 ask \u3057\u3066 answer (by=claude-code) \u3067\u8A18\u9332\u3059\u308B",
|
|
77
|
+
inputSchema: { block, question: z.string(), options: z.array(z.string()).optional(), context: z.string().optional() }
|
|
78
|
+
}, async (a) => {
|
|
79
|
+
const argv2 = ["ask", a.block, a.question];
|
|
80
|
+
if (a.options?.length) argv2.push("--options", a.options.join("|"));
|
|
81
|
+
opt(argv2, "context", a.context);
|
|
82
|
+
return safe(run, argv2);
|
|
83
|
+
});
|
|
84
|
+
server.registerTool("boxglow_answer", { description: "\u5224\u65AD\u306B\u7B54\u3048\u308B (\u4EBA\u304C\u30C1\u30E3\u30C3\u30C8\u3067\u7B54\u3048\u305F\u3089 by=human \u3067\u8A18\u9332)", inputSchema: { block, answer: z.string(), by: z.string().optional().describe("human / claude-code \u306A\u3069") } }, async (a) => {
|
|
85
|
+
const argv2 = ["answer", a.block, a.answer];
|
|
86
|
+
opt(argv2, "by", a.by);
|
|
87
|
+
return safe(run, argv2);
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("boxglow_reopen", { description: "\u5224\u65AD\u3092\u3084\u308A\u76F4\u3059 (\u524D\u306E\u7B54\u3048\u306F\u5C65\u6B74\u306B\u6B8B\u308B)", inputSchema: { block, note: z.string().optional() } }, async ({ block: b, note }) => {
|
|
90
|
+
const argv2 = ["reopen", b];
|
|
91
|
+
opt(argv2, "note", note);
|
|
92
|
+
return safe(run, argv2);
|
|
93
|
+
});
|
|
94
|
+
server.registerTool("boxglow_set", { description: "\u7BB1\u306E\u5C5E\u6027\u3092\u5909\u3048\u308B (\u984C\u540D\u30FB\u30E1\u30E2\u30FB\u30AB\u30C6\u30B4\u30EA\u30FB\u671F\u65E5\u30FB\u30EA\u30DD\u30B8\u30C8\u30EA\u30FB\u8AB2\u984C URL)", inputSchema: { block, title: z.string().optional(), note: z.string().optional(), category: z.string().optional(), start: z.string().optional().describe("YYYY-MM-DD"), due: z.string().optional().describe("YYYY-MM-DD"), repo: z.string().optional(), issue: z.string().optional() } }, async (a) => {
|
|
95
|
+
const argv2 = ["set", a.block];
|
|
96
|
+
for (const k of ["title", "note", "category", "start", "due", "repo", "issue"]) opt(argv2, k, a[k]);
|
|
97
|
+
return safe(run, argv2);
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("boxglow_export", { description: "\u8A08\u753B\u3092 Markdown \u306B\u66F8\u304D\u51FA\u3059 (docs/ROADMAP.md \u306A\u3069)", inputSchema: { out: z.string().optional() } }, async ({ out: o }) => {
|
|
100
|
+
const argv2 = ["export"];
|
|
101
|
+
opt(argv2, "out", o);
|
|
102
|
+
return safe(run, argv2);
|
|
103
|
+
});
|
|
104
|
+
server.registerTool("boxglow_validate", { description: "\u8A08\u753B\u306E\u554F\u984C (\u3064\u306A\u304C\u3063\u3066\u3044\u306A\u3044\u5165\u51FA\u529B\u306A\u3069) \u3092\u691C\u67FB\u3059\u308B", inputSchema: {} }, async () => safe(run, ["validate"]));
|
|
105
|
+
server.registerTool("boxglow_run", { description: '\u305D\u306E\u307B\u304B\u306E CLI \u30B3\u30DE\u30F3\u30C9\u3092\u305D\u306E\u307E\u307E\u5B9F\u884C\u3059\u308B (\u4F8B: ["log", "--n", "20"]\u3001["layout"]\u3001["help"])', inputSchema: { args: z.array(z.string()) } }, async ({ args }) => safe(run, args));
|
|
106
|
+
const transport = new StdioServerTransport();
|
|
107
|
+
await server.connect(transport);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// cli/serve.ts
|
|
111
|
+
import { createServer } from "node:http";
|
|
112
|
+
import { existsSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
|
|
113
|
+
import { basename, dirname, extname, join, normalize, resolve } from "node:path";
|
|
114
|
+
import { spawn } from "node:child_process";
|
|
115
|
+
var MIME = {
|
|
116
|
+
".html": "text/html; charset=utf-8",
|
|
117
|
+
".js": "text/javascript; charset=utf-8",
|
|
118
|
+
".css": "text/css; charset=utf-8",
|
|
119
|
+
".json": "application/json; charset=utf-8",
|
|
120
|
+
".svg": "image/svg+xml",
|
|
121
|
+
".png": "image/png",
|
|
122
|
+
".ico": "image/x-icon",
|
|
123
|
+
".webp": "image/webp",
|
|
124
|
+
".woff2": "font/woff2",
|
|
125
|
+
".map": "application/json"
|
|
126
|
+
};
|
|
127
|
+
function startServe(opts) {
|
|
128
|
+
const { file, port, dist, log } = opts;
|
|
129
|
+
if (!existsSync(join(dist, "index.html"))) throw new Error(`Web \u30A2\u30D7\u30EA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${dist} (npm run build \u3067 dist/ \u3092\u4F5C\u3063\u3066\u304F\u3060\u3055\u3044)`);
|
|
130
|
+
const clients = /* @__PURE__ */ new Set();
|
|
131
|
+
let lastWritten = "";
|
|
132
|
+
let timer = null;
|
|
133
|
+
const broadcast = () => {
|
|
134
|
+
for (const res of clients) res.write("event: change\ndata: {}\n\n");
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
watch(dirname(file), (_ev, name) => {
|
|
138
|
+
if (name && name !== basename(file)) return;
|
|
139
|
+
if (timer) clearTimeout(timer);
|
|
140
|
+
timer = setTimeout(() => {
|
|
141
|
+
timer = null;
|
|
142
|
+
try {
|
|
143
|
+
const text2 = readFileSync(file, "utf8");
|
|
144
|
+
if (text2 === lastWritten) return;
|
|
145
|
+
} catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
broadcast();
|
|
149
|
+
}, 250);
|
|
150
|
+
});
|
|
151
|
+
} catch (e) {
|
|
152
|
+
log(`[serve] \u30D5\u30A1\u30A4\u30EB\u306E\u76E3\u8996\u3092\u59CB\u3081\u3089\u308C\u307E\u305B\u3093: ${e instanceof Error ? e.message : String(e)}`);
|
|
153
|
+
}
|
|
154
|
+
const send = (res, status, body, type) => {
|
|
155
|
+
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
156
|
+
res.end(body);
|
|
157
|
+
};
|
|
158
|
+
const readBody = (req) => new Promise((ok, ng) => {
|
|
159
|
+
const chunks = [];
|
|
160
|
+
req.on("data", (c) => chunks.push(c));
|
|
161
|
+
req.on("end", () => ok(Buffer.concat(chunks).toString("utf8")));
|
|
162
|
+
req.on("error", ng);
|
|
163
|
+
});
|
|
164
|
+
const server = createServer(async (req, res) => {
|
|
165
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
166
|
+
try {
|
|
167
|
+
if (url.pathname === "/api/project") {
|
|
168
|
+
if (req.method === "GET") {
|
|
169
|
+
const text2 = readFileSync(file, "utf8");
|
|
170
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-boxglow-file": encodeURIComponent(basename(file)) });
|
|
171
|
+
res.end(text2);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (req.method === "PUT") {
|
|
175
|
+
const text2 = await readBody(req);
|
|
176
|
+
try {
|
|
177
|
+
JSON.parse(text2);
|
|
178
|
+
} catch {
|
|
179
|
+
send(res, 400, JSON.stringify({ ok: false, error: "JSON \u3067\u306F\u3042\u308A\u307E\u305B\u3093" }), "application/json");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
lastWritten = text2;
|
|
183
|
+
writeFileSync(file, text2, "utf8");
|
|
184
|
+
send(res, 200, JSON.stringify({ ok: true }), "application/json");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
send(res, 405, "method not allowed", "text/plain");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (url.pathname === "/api/events") {
|
|
191
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
192
|
+
res.write("event: hello\ndata: {}\n\n");
|
|
193
|
+
clients.add(res);
|
|
194
|
+
const ping = setInterval(() => res.write(": ping\n\n"), 25e3);
|
|
195
|
+
req.on("close", () => {
|
|
196
|
+
clearInterval(ping);
|
|
197
|
+
clients.delete(res);
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let rel = decodeURIComponent(url.pathname);
|
|
202
|
+
if (rel.endsWith("/")) rel += "index.html";
|
|
203
|
+
const target = normalize(join(dist, rel));
|
|
204
|
+
if (!target.startsWith(resolve(dist))) {
|
|
205
|
+
send(res, 403, "forbidden", "text/plain");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const path = existsSync(target) && statSync(target).isFile() ? target : join(dist, "index.html");
|
|
209
|
+
send(res, 200, readFileSync(path), MIME[extname(path)] ?? "application/octet-stream");
|
|
210
|
+
} catch (e) {
|
|
211
|
+
send(res, 500, `error: ${e instanceof Error ? e.message : String(e)}`, "text/plain");
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
server.listen(port, "127.0.0.1", () => {
|
|
215
|
+
const addr = `http://localhost:${port}/?serve=1`;
|
|
216
|
+
log(`[serve] ${file}
|
|
217
|
+
[serve] ${addr} (Ctrl+C \u3067\u7D42\u4E86)`);
|
|
218
|
+
if (opts.open) {
|
|
219
|
+
const cmd = process.platform === "win32" ? ["cmd", "/c", "start", "", addr] : process.platform === "darwin" ? ["open", addr] : ["xdg-open", addr];
|
|
220
|
+
try {
|
|
221
|
+
spawn(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref();
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// cli/main.ts
|
|
229
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
230
|
+
|
|
231
|
+
// docs/AGENTS_SNIPPET.md
|
|
232
|
+
var AGENTS_SNIPPET_default = '# Boxglow \u3092\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306B\u4F7F\u308F\u305B\u308B\u6307\u793A\u66F8 (AGENTS.md / CLAUDE.md \u306B\u8CBC\u308B)\n\n\u4E0B\u306E\u300C---\u300D\u306E\u9593\u3092\u305D\u306E\u307E\u307E\u30EA\u30DD\u30B8\u30C8\u30EA\u306E AGENTS.md (Codex) \u3084 CLAUDE.md (Claude Code) \u306B\u8CBC\u308B\u3002\nboxglow.json \u306F\u30EA\u30DD\u30B8\u30C8\u30EA\u76F4\u4E0B\u306B\u7F6E\u304F (`npx boxglow init --name "<\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D>"`)\u3002\n\n---\n\n## Boxglow (\u8A08\u753B\u3068\u9032\u6357\u306E\u5171\u6709)\n\n\u3053\u306E\u30EA\u30DD\u30B8\u30C8\u30EA\u306E\u8A08\u753B\u3068\u9032\u6357\u306F `boxglow.json` \u304C\u6B63\u672C\u3002\u4EBA\u9593\u306F Boxglow \u306E\u753B\u9762\u3067\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u3092\u898B\u3066\u3044\u308B\u3002\n\u30D5\u30A1\u30A4\u30EB\u3092\u76F4\u63A5\u7DE8\u96C6\u305B\u305A\u3001\u5FC5\u305A `npx boxglow` \u30B3\u30DE\u30F3\u30C9\u3067\u66F4\u65B0\u3059\u308B\u3053\u3068 (\u7D50\u7DDA\u30EB\u30FC\u30EB\u3068\u5C65\u6B74\u304C\u4FDD\u305F\u308C\u308B)\u3002\n\n\u30D6\u30ED\u30C3\u30AF = \u300C\u5165\u529B\u304B\u3089\u51FA\u529B (\u6210\u679C\u7269) \u3092\u4F5C\u308B\u30BF\u30B9\u30AF\u300D\u3002\u51FA\u529B\u306E\u6210\u679C\u7269\u304C\u78BA\u5B9A\u3057\u305F\u3089\u5B8C\u4E86 (WhiteBox)\u3002\n\u72B6\u614B: BlackBox (\u51FA\u529B\u3060\u3051\u6C7A\u3081\u305F) \u2192 GrayBox (\u5206\u89E3\u4E2D\u30FB\u7740\u624B\u4E2D) \u2192 WhiteBox (\u5B8C\u4E86)\u3002\n\n### \u4F5C\u696D\u306E\u305F\u3073\u306B\u5B88\u308B\u3053\u3068\n\n1. **\u6700\u521D\u306B\u8AAD\u3080**: `npx boxglow status` \u3067\u5168\u4F53\u30FB\u5224\u65AD\u5F85\u3061\u30FB\u4F5C\u696D\u4E2D\u30FB\u6B21\u306E\u5019\u88DC\u3092\u8AAD\u3080\u3002`npx boxglow show <block>` \u3067\u62C5\u5F53\u3059\u308B\u7BB1\u306E\u5165\u51FA\u529B\u3092\u78BA\u8A8D\u3059\u308B\n2. **\u7740\u624B\u3092\u8A18\u9332**: \u7BB1\u306B\u53D6\u308A\u304B\u304B\u308B\u3068\u304D `npx boxglow start <block> --note "<\u4F55\u3092\u3059\u308B\u304B>"`\u3002\u540C\u6642\u306B\u9032\u3081\u308B\u7BB1\u306F 1\u301C2 \u500B\u307E\u3067\n3. **\u5206\u89E3**: 1 \u3064\u306E\u7BB1\u3067\u51FA\u529B\u3092\u4F5C\u308B\u898B\u901A\u3057\u304C\u7ACB\u305F\u306A\u3044\u3068\u304D\u306F\u3001`npx boxglow split <block> --spec \'<JSON>\'` \u3067\u4E2D\u306B\u5C0F\u3055\u306A\u7BB1\u3092\u7F6E\u304F\u3002\u5404\u7BB1\u306B\u51FA\u529B (\u6210\u679C\u7269) \u3092\u5FC5\u305A\u6C7A\u3081\u308B\u3002\u5165\u529B\u306F\u4E0D\u660E\u306A\u3089\u7701\u7565\u3057\u3066\u3088\u3044 (\u81EA\u52D5\u3067\u4E0A\u306E\u968E\u5C64\u306E\u5165\u529B\u306B\u306A\u308B)\n4. **\u5B8C\u4E86\u3092\u8A18\u9332**: \u51FA\u529B\u306E\u6210\u679C\u7269\u304C\u3067\u304D\u305F\u3089 `npx boxglow done <block> --artifact "<\u540D\u524D>=<\u30EA\u30DD\u30B8\u30C8\u30EA\u5185\u306E\u30D1\u30B9>"`\u3002\n Git \u7BA1\u7406\u4E0B\u306E\u30D1\u30B9\u306F\u300C\u30B3\u30DF\u30C3\u30C8 + \u30D1\u30B9 + \u4E2D\u8EAB\u306E\u30CF\u30C3\u30B7\u30E5\u300D\u3067\u8A18\u9332\u3055\u308C\u308B (\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306F\u3057\u306A\u3044)\u3002\u30B3\u30DF\u30C3\u30C8\u3057\u3066\u304B\u3089 done \u3059\u308B\u3002PR \u3084\u5916\u90E8\u8CC7\u6599\u306F URL \u3067\u3082\u3088\u3044\u3002\n \u6210\u679C\u7269\u3060\u3051\u5148\u306B\u4ED8\u3051\u308B\u306A\u3089 `npx boxglow artifact <block> <\u30D1\u30B9>`\u3002\u9032\u6357\u306E\u9014\u4E2D\u7D4C\u904E\u306F `npx boxglow set <block> --progress 60`\n5. **\u4EBA\u9593\u306E\u5224\u65AD\u304C\u8981\u308B**: `npx boxglow ask <block> "<\u8CEA\u554F>" --options "A|B"`\u3002\u56DE\u7B54\u304C\u3042\u308B\u307E\u3067\u305D\u306E\u7BB1\u306F\u9032\u3081\u305A\u3001\u4ED6\u306E\u7BB1\u3078\u79FB\u308B\u3002\u56DE\u7B54\u306F `status` \u306E\u300C\u5224\u65AD\u5F85\u3061\u300D\u304C\u6D88\u3048\u305F\u3089 `show` \u3067\u8AAD\u3080\n6. **\u8A70\u307E\u3063\u305F\u3089**: `npx boxglow blocked <block> --note "<\u56F0\u3063\u3066\u3044\u308B\u3053\u3068>"`\n7. **\u6210\u679C\u7269\u306E\u78BA\u8A8D**: \u30D5\u30A1\u30A4\u30EB\u3092\u79FB\u52D5\u30FB\u6539\u540D\u3057\u305F\u3089 `npx boxglow check` \u3092\u5B9F\u884C\u3059\u308B (\u79FB\u52D5\u3092\u691C\u51FA\u3057\u3066\u30D1\u30B9\u3092\u4ED8\u3051\u66FF\u3048\u308B\u3002\u898B\u3064\u304B\u3089\u306A\u3051\u308C\u3070\u5370\u304C\u4ED8\u304F)\n8. **\u5831\u544A**: \u4F5C\u696D\u306E\u6700\u5F8C\u306B `npx boxglow status` \u306E\u5185\u5BB9\u3092\u8981\u7D04\u3057\u3066\u5831\u544A\u3059\u308B\n\n### \u69CB\u9020\u306E\u7D04\u675F\n\n- \u6700\u4E0A\u4F4D\u306B\u306F\u300C\u5165\u529B\u30CE\u30FC\u30C9\u300D\u300C\u6700\u7D42\u6210\u679C\u7269\u30CE\u30FC\u30C9\u300D\u3068\u300C\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u300D\u3060\u3051\u304C\u3042\u308B\u3002\u30BF\u30B9\u30AF\u306F\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u306E\u4E2D\u306B\u7F6E\u304F (`add` \u306E\u65E2\u5B9A\u306E\u89AA\u306F\u6700\u521D\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1)\n- \u968E\u5C64\u306E\u6DF1\u3055\u306B\u5236\u9650\u306F\u306A\u3044\u3002`split` \u306F\u4F55\u6BB5\u3067\u3082\u4F7F\u3048\u308B\n- \u5225\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3067\u3082\u4F7F\u3048\u305D\u3046\u306A\u7BB1 (\u4F8B: \u5165\u529B\u753B\u50CF\u306E\u30E2\u30CE\u30AF\u30ED\u5316) \u306F `npx boxglow export-block <block> --out <\u540D\u524D>.boxglow-block.json` \u3067\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306B\u3057\u3001\n `npx boxglow import-block <path> --parent <block>` \u3067\u633F\u5165\u3059\u308B\u3002\u30EA\u30DD\u30B8\u30C8\u30EA\u306E `boxglow-blocks/` \u306B\u7F6E\u3044\u3066\u5171\u6709\u3057\u3066\u3088\u3044\n\n\u6700\u4E0A\u4F4D\u306E\u5165\u529B\u304C\u591A\u3044\u3068\u304D\u306F `npx boxglow group "<\u30B0\u30EB\u30FC\u30D7\u540D>"` \u3067\u30B0\u30EB\u30FC\u30D7\u3092\u4F5C\u308A\u3001`npx boxglow group-set <\u5165\u529B\u540D> <\u30B0\u30EB\u30FC\u30D7\u540D>` \u3067\u5206\u3051\u308B (\u4F8B: PCIe \u4ED5\u69D8\u66F8 / DDR \u4ED5\u69D8\u66F8)\u3002\n\n### split \u306E JSON \u5F62\u5F0F\n\n```json\n{\n "blocks": [\n { "title": "\u8A2D\u8A08", "description": "\u3084\u308B\u3053\u3068", "inputs": ["\u4ED5\u69D8"], "outputs": ["\u8A2D\u8A08\u66F8"] },\n { "title": "\u5B9F\u88C5", "inputs": ["\u8A2D\u8A08\u66F8"], "outputs": ["\u30B3\u30FC\u30C9"] }\n ],\n "connections": [\n { "from": "parent.\u4ED5\u69D8", "to": "\u8A2D\u8A08.\u4ED5\u69D8" },\n { "from": "\u8A2D\u8A08.\u8A2D\u8A08\u66F8", "to": "\u5B9F\u88C5.\u8A2D\u8A08\u66F8" },\n { "from": "\u5B9F\u88C5.\u30B3\u30FC\u30C9", "to": "parent.\u51FA\u529B" }\n ]\n}\n```\n\n`parent.<\u540D\u524D>` \u306F\u5206\u89E3\u3059\u308B\u7BB1\u81EA\u8EAB\u306E\u5165\u529B / \u51FA\u529B\u3002`<block>` \u306F\u77ED\u3044 ID (B12 \u306A\u3069\u3002status \u306B\u51FA\u308B) \u304B\u984C\u540D\u3002\n\u4E0B\u306E\u968E\u5C64\u3092\u6301\u305F\u306A\u3044\u7BB1\u306E\u51FA\u529B\u306F 1 \u672C (outputs \u306F 1 \u3064\u3060\u3051\u66F8\u304F)\u3002\u984C\u540D\u306F\u300C\u4F55\u3092\u4F5C\u308B\u304B\u300D\u304C\u5206\u304B\u308B\u77ED\u3044\u8A00\u8449\u306B\u3059\u308B\u3002\n\u671F\u65E5\u3084\u6642\u9593\u306F `npx boxglow set <block> --due 2026-10-15 --start 2026-10-01 --estimate 8 --hours 3.5` \u3067\u8A18\u9332\u3059\u308B\u3002\n\u7BB1\u306B\u306F\u4ED5\u4E8B\u306E\u7A2E\u985E (\u30AB\u30C6\u30B4\u30EA) \u3092\u4ED8\u3051\u308B: `--category design` (\u8A2D\u8A08) / build (\u5B9F\u88C5) / verify (\u691C\u8A3C) / evaluate (\u8A55\u4FA1) / study (\u691C\u8A0E) / research (\u8ABF\u67FB) / ui (\u30C7\u30B6\u30A4\u30F3) / improve (\u6539\u5584) / fix (\u8AB2\u984C\u89E3\u6C7A) / docs (\u6587\u66F8) / ops (\u904B\u7528) / other (\u305D\u306E\u4ED6: \u8FF7\u3063\u305F\u3068\u304D)\u3002add \u306E\u3068\u304D\u306B\u4ED8\u3051\u3001\u5909\u3048\u308B\u306A\u3089 `npx boxglow set <block> --category verify`\u3002\n\u65E2\u5B58\u306E\u7BB1\u306B\u5165\u51FA\u529B\u3092\u8DB3\u3059 / \u540D\u524D\u3092\u5909\u3048\u308B\u306E\u306F `npx boxglow port <block> --in <\u540D\u524D> --out <\u540D\u524D> --rename <\u65E7>=<\u65B0>` (`project` = \u6700\u521D\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u3002\u6700\u7D42\u6210\u679C\u7269\u306E\u540D\u524D\u3082\u3053\u308C\u3067\u5909\u3048\u308B)\u3002\u7DDA\u3092\u5916\u3059\u306E\u306F `npx boxglow disconnect <\u984C\u540D.\u51FA\u529B\u540D> <\u984C\u540D.\u5165\u529B\u540D>`\u3001\u7BB1\u305D\u306E\u3082\u306E\u3092\u6D88\u3059\u306E\u306F `npx boxglow remove <block>` (\u4E2D\u306B\u7BB1\u304C\u3042\u308C\u3070 `--force`)\u3002MCP \u30C4\u30FC\u30EB (`boxglow_status` / `boxglow_start` / `boxglow_done` / `boxglow_ask` \u306A\u3069\u3002`.mcp.json` \u306B\u767B\u9332\u6E08\u307F\u306A\u3089\u4F7F\u3048\u308B) \u306F\u540C\u3058\u30B3\u30DE\u30F3\u30C9\u306E\u5225\u306E\u5165\u53E3\u3067\u3001\u3069\u3061\u3089\u3067\u66F8\u3044\u3066\u3082\u540C\u3058\u30D5\u30A1\u30A4\u30EB\u304C\u66F4\u65B0\u3055\u308C\u308B\u3002\u8A08\u753B\u306E\u6587\u66F8\u306F `npx boxglow export --out docs/ROADMAP.md`\u3002\n\u65B0\u3057\u3044\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3001\u65B9\u91DD (README \u3084\u4F9D\u983C\u6587) \u304B\u3089\u6700\u4E0A\u4F4D\u306E\u5927\u9805\u76EE 3\u301C7 \u500B\u3092\u5148\u306B\u4F5C\u308A\u3001\u4EBA\u306B `ask` \u3067\u78BA\u8A8D\u3057\u3066\u304B\u3089\u3001\u7740\u624B\u3059\u308B\u5927\u9805\u76EE\u3060\u3051\u3092 `split` \u3067\u5206\u89E3\u3059\u308B (\u5148\u306E\u6BB5\u968E\u306F\u7C97\u3044\u307E\u307E)\u3002`ask` \u306E\u8CEA\u554F\u306F\u305D\u308C\u3060\u3051\u3067\u5224\u65AD\u3067\u304D\u308B\u3088\u3046\u306B\u66F8\u304D\u3001\u524D\u63D0\u30FB\u6BD4\u8F03\u30FB\u5F71\u97FF\u306F `--context` \u306B\u5165\u308C\u308B (\u300C\u3053\u308C\u3067\u3088\u3044\u3067\u3059\u304B\u300D\u306E\u3088\u3046\u306B\u5916\u3092\u6307\u3055\u306A\u3044)\u3002\u5019\u88DC\u304B\u3089 1 \u3064\u3092\u9078\u3076\u5834\u9762\u3067\u306F\u3001\u81EA\u5206\u3067\u6C7A\u3081\u308B\u3068\u304D\u3082 `ask ... --options "A|B|C"` \u3068 `answer --by <\u81EA\u5206>` \u3067\u8A18\u9332\u3057\u3001\u9078\u3070\u306A\u304B\u3063\u305F\u5019\u88DC\u3092\u6B8B\u3059\u3002\u65B9\u91DD\u8EE2\u63DB\u306F `reopen <block> --note <\u7406\u7531>`\u3002\u3059\u3079\u3066 CLI \u3060\u3051\u3067\u6210\u7ACB\u3059\u308B (\u753B\u9762\u306F\u4EBA\u304C\u898B\u308B\u305F\u3081\u306E\u3082\u306E)\u3002\u4EBA\u304C\u3044\u306A\u3044\u904B\u7528\u306A\u3089\u3001AI \u304C `answer --by <\u81EA\u5206>` \u3067\u5224\u65AD\u3092\u8A18\u9332\u3057\u3066\u9032\u3081\u308B\u3002\n\u51FA\u529B\u306E\u540D\u524D\u306F\u300C\u5177\u4F53\u7684\u306A\u6210\u679C\u7269\u300D\u306B\u3059\u308B (\u4F8B: `\u8A2D\u8A08\u66F8 docs/design.md`\u3001`PR #12`\u3001`\u516C\u958B URL`\u3001`\u30C6\u30B9\u30C8\u7D50\u679C (vitest 53 \u4EF6)`)\u3002\u300C\u6A5F\u80FD\u4E00\u5F0F\u300D\u300C\u6240\u898B\u300D\u306E\u3088\u3046\u306A\u62BD\u8C61\u7684\u306A\u540D\u524D\u306F\u907F\u3051\u3001done \u306E\u3068\u304D\u306F\u5FC5\u305A --artifact \u3067\u30D5\u30A1\u30A4\u30EB\u30FBURL\u30FB\u30B3\u30DF\u30C3\u30C8\u3092\u4ED8\u3051\u308B\u3002\n\u3059\u3079\u3066\u306E\u7BB1\u306F\u300C\u5165\u529B \u2192 \u51FA\u529B\u300D\u304C\u4F55\u304B\u306B\u3064\u306A\u304C\u3063\u3066\u3044\u308B\u3088\u3046\u306B\u3059\u308B (\u3064\u306A\u304C\u3063\u3066\u3044\u306A\u3044\u7BB1\u306F\u8A08\u753B\u306E\u7A74\u3002\u6700\u4E0A\u4F4D\u306E\u5165\u529B\u30CE\u30FC\u30C9\u306F\u3001\u672A\u63A5\u7D9A\u306E\u5165\u529B\u304C\u81EA\u52D5\u3067\u4E0A\u304C\u308B)\u3002\n\u8907\u6570\u4EBA\u3067\u540C\u3058 boxglow.json \u3092\u7DE8\u96C6\u3059\u308B\u306A\u3089\u3001\u5404\u81EA\u306E clone \u3067\u4E00\u5EA6 `npx boxglow git-setup` \u3092\u5B9F\u884C\u3059\u308B (\u7BB1\u306E\u5358\u4F4D\u3067\u81EA\u52D5\u30DE\u30FC\u30B8\u3055\u308C\u3001\u5225\u306E\u7BB1\u306E\u5909\u66F4\u306F\u885D\u7A81\u3057\u306A\u3044)\u3002\n\u8907\u6570\u30EA\u30DD\u30B8\u30C8\u30EA\u3092 1 \u3064\u306E\u8A08\u753B\u3067\u7BA1\u7406\u3059\u308B\u3068\u304D\u306F\u3001boxglow.json \u3092\u4E0A\u306E\u30D5\u30A9\u30EB\u30C0\u306B\u7F6E\u304D\u3001\u5404\u30EA\u30DD\u30B8\u30C8\u30EA\u3067\u306F\u74B0\u5883\u5909\u6570 BOXGLOW_FILE \u306B\u305D\u306E\u5834\u6240\u3092\u5165\u308C\u308B (\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u306F\u30EA\u30DD\u30B8\u30C8\u30EA\u3054\u3068\u306B 1 \u3064\u3002`npx boxglow project "\u540D\u524D" --repo <\u30D1\u30B9>`)\u3002\n\u5165\u529B\u306F\u65E2\u5B9A\u3067\u5FC5\u9808\u3002\u7121\u304F\u3066\u3082\u7740\u624B\u3067\u304D\u308B\u5165\u529B\u306F\u753B\u9762\u3067\u300C\u4EFB\u610F\u300D\u306B\u3059\u308B\u3002status \u306E\u300C\u6B21\u306E\u5019\u88DC\u300D\u306F\u5FC5\u9808\u306E\u5165\u529B\u304C\u305D\u308D\u3063\u305F\u7BB1\u304B\u3089\u4E26\u3076 (\u300C\u7740\u624B\u3067\u304D\u308B\u300D/\u300C\u5FC5\u9808\u306E\u5165\u529B\u5F85\u3061\u300D\u304C\u4ED8\u304F)\u3002\n\u5165\u529B\u306E\u8AAC\u660E\u306F\u66F8\u304B\u306A\u3044 (\u3064\u306A\u304C\u3063\u305F\u51FA\u529B\u5074\u306E\u8AAC\u660E\u304C\u4F7F\u308F\u308C\u308B)\u3002\u51FA\u529B\u306E\u8AAC\u660E\u306B\u5F62\u5F0F\u30FB\u5236\u7D04\u3092\u66F8\u304F\u3002\n\n---\n';
|
|
233
|
+
|
|
234
|
+
// .claude/skills/boxglow/SKILL.md
|
|
235
|
+
var SKILL_default = '---\nname: boxglow\ndescription: Boxglow (boxglow.json) \u3067\u8A08\u753B\u3068\u9032\u6357\u3092\u4EBA\u9593\u3068\u5171\u6709\u3059\u308B\u3002\u4F5C\u696D\u306E\u958B\u59CB\u30FB\u5206\u89E3\u30FB\u5B8C\u4E86\u30FB\u5224\u65AD\u5F85\u3061\u3092 npx boxglow \u3067\u8A18\u9332\u3059\u308B\u3002\u300C\u9032\u6357\u3092 Boxglow \u306B\u8A18\u9332\u3057\u3066\u300D\u300Cboxglow \u3067\u72B6\u6CC1\u3092\u898B\u3066\u300D\u300C/boxglow\u300D\u3067\u4F7F\u7528\u3002\n---\n\n# Boxglow \u3067\u8A08\u753B\u3068\u9032\u6357\u3092\u5171\u6709\u3059\u308B\n\n\u30EA\u30DD\u30B8\u30C8\u30EA\u76F4\u4E0B\u306E `boxglow.json` \u304C\u8A08\u753B\u306E\u6B63\u672C\u3002\u4EBA\u9593\u306F Boxglow \u306E\u753B\u9762 (https://boxglow.pages.dev/ \u3067\u300Cboxglow.json \u3092\u958B\u304F\u300D) \u3067\u898B\u3066\u3044\u308B\u3002\n\u76F4\u63A5\u7DE8\u96C6\u305B\u305A `npx boxglow` \u3067\u66F4\u65B0\u3059\u308B\u3002\n\n## \u6700\u521D\u306E\u8A08\u753B\u3065\u304F\u308A (\u65B0\u3057\u3044\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u3068\u304D)\n\n1. \u65B9\u91DD (README\u3001\u4F9D\u983C\u6587\u3001\u8981\u4EF6\u30E1\u30E2) \u3092\u8AAD\u307F\u3001`npx boxglow init --name "<\u540D\u524D>"` \u306E\u3042\u3068\u3001\u6700\u4E0A\u4F4D\u306B\u5927\u9805\u76EE (3\u301C7 \u500B) \u3092 `add` \u3067\u7F6E\u304F\u3002\n \u5404\u5927\u9805\u76EE\u306B\u306F\u300C\u5165\u529B (\u4F55\u3092\u4F7F\u3046\u304B)\u300D\u3068\u300C\u51FA\u529B (\u5177\u4F53\u7684\u306A\u6210\u679C\u7269: \u30D5\u30A1\u30A4\u30EB\u30FBURL\u30FBPR)\u300D\u3092\u5FC5\u305A\u66F8\u304D\u3001\u6700\u7D42\u6210\u679C\u7269\u307E\u3067\u7DDA\u3067\u3064\u306A\u3050 (`connect`)\n2. \u4F5C\u3063\u305F\u3089 `npx boxglow ask <\u6700\u521D\u306E\u5927\u9805\u76EE> "\u3053\u306E\u5927\u9805\u76EE\u306E\u5206\u3051\u65B9\u3067\u3088\u3044\u3067\u3059\u304B?" --options "\u3088\u3044|\u76F4\u3057\u305F\u3044"` \u3067\u4EBA\u306E\u78BA\u8A8D\u3092\u53D6\u308B\u3002\n \u4EBA\u306F\u753B\u9762\u3067\u3082\u3001`npx boxglow answer <block> "<\u56DE\u7B54>"` \u3067\u3082\u3001\u30C1\u30E3\u30C3\u30C8\u3067\u3082\u7B54\u3048\u3089\u308C\u308B\u3002\u30C1\u30E3\u30C3\u30C8\u3067\u7B54\u3048\u304C\u8FD4\u3063\u305F\u3089 AI \u304C `answer --by human` \u3067\u8A18\u9332\u3059\u308B\u3002\n \u4EBA\u304C\u753B\u9762\u3092\u4F7F\u308F\u306A\u3044\u904B\u7528 (AI \u3060\u3051\u3067\u9032\u3081\u308B\u6307\u793A\u304C\u3042\u308B\u3068\u304D) \u306F\u3001AI \u81EA\u8EAB\u304C `answer --by claude-code "<\u5224\u65AD\u3068\u7406\u7531>"` \u3067\u8A18\u9332\u3057\u3066\u5148\u3078\u9032\u3080\u3002\u5224\u65AD\u306E\u8A18\u9332\u3092\u6B8B\u3059\u3053\u3068\u304C\u76EE\u7684\u3067\u3001\u753B\u9762\u306F\u5FC5\u9808\u3067\u306F\u306A\u3044\n3. \u7740\u624B\u3059\u308B\u5927\u9805\u76EE\u3060\u3051\u3092 `split` \u3067\u5206\u89E3\u3059\u308B (\u5148\u306E\u6BB5\u968E\u306F\u7C97\u3044\u307E\u307E\u3002\u9032\u3081\u306A\u304C\u3089\u7D30\u304B\u304F\u3059\u308B)\n\n## \u624B\u9806\n\n1. `npx boxglow status` \u3092\u8AAD\u3080\u3002\u5224\u65AD\u5F85\u3061\u306E\u56DE\u7B54\u3001\u4F5C\u696D\u4E2D\u306E\u7BB1\u3001\u6B21\u306E\u5019\u88DC\u3092\u628A\u63E1\u3059\u308B\n2. \u62C5\u5F53\u3059\u308B\u7BB1\u3092\u6C7A\u3081\u305F\u3089 `npx boxglow show <block>` \u3067\u5165\u51FA\u529B\u3092\u78BA\u8A8D\u3057\u3001`npx boxglow start <block> --note "<\u4F55\u3092\u3059\u308B\u304B>"`\n3. \u5927\u304D\u3044\u7BB1\u306F `npx boxglow split <block> --spec \'<JSON>\'` \u3067\u5206\u89E3\u3059\u308B (\u5F62\u5F0F\u306F `npx boxglow help`)\u3002\u5404\u7BB1\u306E\u51FA\u529B\u3092\u5FC5\u305A\u6C7A\u3081\u308B\n4. \u6210\u679C\u7269\u304C\u3067\u304D\u305F\u3089 `npx boxglow done <block> --artifact "<\u540D\u524D>=<URL \u307E\u305F\u306F\u30D1\u30B9>"`\n5. \u4EBA\u9593\u306E\u5224\u65AD\u304C\u8981\u308B\u3068\u304D\u306F `npx boxglow ask <block> "<\u8CEA\u554F>" --options "A|B"` \u3057\u3066\u4ED6\u306E\u7BB1\u3078\u79FB\u308B\n6. \u8A70\u307E\u3063\u305F\u3089 `npx boxglow blocked <block> --note "<\u56F0\u3063\u3066\u3044\u308B\u3053\u3068>"`\n7. \u6700\u5F8C\u306B `npx boxglow status` \u3092\u8981\u7D04\u3057\u3066\u5831\u544A\u3059\u308B\n\n## \u7D04\u675F\n\n- `ask` \u306E\u8CEA\u554F\u306F\u3001\u305D\u308C\u3060\u3051\u3067\u5224\u65AD\u3067\u304D\u308B\u3088\u3046\u306B\u66F8\u304F\u3002\u300C\u3053\u308C\u3067\u3088\u3044\u3067\u3059\u304B\u300D\u300C\u4E0A\u8A18\u306E\u6848\u300D\u306E\u3088\u3046\u306B\u5916\u3092\u6307\u3055\u305A\u3001\u524D\u63D0\u30FB\u6BD4\u8F03\u30FB\u5F71\u97FF\u3092 `--context` \u306B\u66F8\u304F (\u4EBA\u306F\u4E00\u89A7\u306E\u4E2D\u306E\u8CEA\u554F\u3060\u3051\u3092\u8AAD\u3093\u3067\u7B54\u3048\u308B)\n- \u5019\u88DC\u304B\u3089 1 \u3064\u3092\u9078\u3076\u5834\u9762 (\u65B9\u5F0F\u30FB\u30E9\u30A4\u30D6\u30E9\u30EA\u30FB\u8A2D\u8A08\u306E\u9078\u629E) \u3067\u306F\u3001AI \u304C\u81EA\u5206\u3067\u6C7A\u3081\u308B\u3068\u304D\u3082 `ask <block> "<\u554F\u3044>" --options "A|B|C"` \u2192 `answer --by claude-code "<\u9078\u3093\u3060\u3082\u306E>\u3002\u7406\u7531: ..."` \u306E\u5F62\u3067\u8A18\u9332\u3057\u3001\u9078\u3070\u306A\u304B\u3063\u305F\u5019\u88DC\u3092\u7BB1\u306B\u6B8B\u3059 (\u65B9\u91DD\u8EE2\u63DB\u306E\u3068\u304D\u306B\u623B\u308C\u308B)\n- \u65B9\u91DD\u8EE2\u63DB\u306F `reopen <block> --note "<\u7406\u7531>"` \u3067\u524D\u306E\u7B54\u3048\u3092\u5C65\u6B74\u306B\u6B8B\u3057\u3066\u9078\u3073\u76F4\u3059 (\u5019\u88DC\u306F\u6D88\u3055\u306A\u3044)\n\n- \u540C\u6642\u306B\u4F5C\u696D\u4E2D\u306B\u3059\u308B\u7BB1\u306F 1\u301C2 \u500B\u307E\u3067\n- \u6210\u679C\u7269 (artifact) \u306F\u5F8C\u304B\u3089\u4EBA\u304C\u958B\u3051\u308B\u3082\u306E (\u30B3\u30DF\u30C3\u30C8\u3001PR\u3001\u30D5\u30A1\u30A4\u30EB\u306E\u30D1\u30B9) \u306B\u3059\u308B\n- \u51FA\u529B\u306F\u5177\u4F53\u7684\u306A\u6210\u679C\u7269\u306E\u540D\u524D\u306B\u3059\u308B (\u30D5\u30A1\u30A4\u30EB\u30FBPR\u30FBURL\u30FB\u30C6\u30B9\u30C8\u7D50\u679C)\u3002\u62BD\u8C61\u7684\u306A\u540D\u524D (\u6A5F\u80FD\u4E00\u5F0F\u3001\u6240\u898B) \u306F\u907F\u3051\u308B\n- \u7BB1\u306F\u5FC5\u305A\u300C\u5165\u529B \u2192 \u51FA\u529B\u300D\u3067\u3064\u306A\u3050\u3002\u8DB3\u308A\u306A\u3044\u5165\u51FA\u529B\u306F `port`\u3001\u5916\u3059\u306E\u306F `disconnect`\u3001\u7BB1\u305D\u306E\u3082\u306E\u3092\u6D88\u3059\u306E\u306F `remove`\u3002\u6587\u66F8\u5316\u306F `export --out docs/ROADMAP.md`\n- \u7BB1\u3092 add \u3059\u308B\u3068\u304D\u306F\u4ED5\u4E8B\u306E\u7A2E\u985E\u3092 `--category` \u3067\u4ED8\u3051\u308B (design \u8A2D\u8A08 / build \u5B9F\u88C5 / verify \u691C\u8A3C / evaluate \u8A55\u4FA1 / study \u691C\u8A0E / research \u8ABF\u67FB / ui \u30C7\u30B6\u30A4\u30F3 / improve \u6539\u5584 / fix \u8AB2\u984C\u89E3\u6C7A / docs \u6587\u66F8 / ops \u904B\u7528 / other \u305D\u306E\u4ED6)\u3002\u753B\u9762\u3067\u306F\u8272\u306E\u5E2F\u3068\u672D\u306B\u306A\u308B\n- boxglow.json \u304C\u307E\u3060\u7121\u3051\u308C\u3070 `npx boxglow init --name "<\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D>"` \u3067\u4F5C\u308A\u3001\u6700\u7D42\u6210\u679C\u7269\u3068\u6700\u4E0A\u4F4D\u306E\u7BB1\u3092 `add` \u3067\u7F6E\u304F\n';
|
|
236
|
+
|
|
237
|
+
// src/model/categories.ts
|
|
238
|
+
var CATEGORIES = [
|
|
239
|
+
{ key: "study", label: "\u691C\u8A0E", en: "study", color: "#6b7075" },
|
|
240
|
+
{ key: "research", label: "\u8ABF\u67FB", en: "research", color: "#7a5230" },
|
|
241
|
+
{ key: "design", label: "\u8A2D\u8A08", en: "design", color: "#2f6fb3" },
|
|
242
|
+
{ key: "ui", label: "\u30C7\u30B6\u30A4\u30F3", en: "ui", color: "#c0427a" },
|
|
243
|
+
{ key: "build", label: "\u5B9F\u88C5", en: "build", color: "#5a4fcf" },
|
|
244
|
+
{ key: "verify", label: "\u691C\u8A3C", en: "verify", color: "#26753d" },
|
|
245
|
+
{ key: "evaluate", label: "\u8A55\u4FA1", en: "evaluate", color: "#8a6508" },
|
|
246
|
+
{ key: "improve", label: "\u6539\u5584", en: "improve", color: "#b5581a" },
|
|
247
|
+
{ key: "fix", label: "\u8AB2\u984C\u89E3\u6C7A", en: "fix", color: "#c0392b" },
|
|
248
|
+
{ key: "docs", label: "\u6587\u66F8", en: "docs", color: "#5f6f3a" },
|
|
249
|
+
{ key: "ops", label: "\u904B\u7528", en: "ops", color: "#1f7a8c" },
|
|
250
|
+
{ key: "other", label: "\u305D\u306E\u4ED6", en: "other", color: "#8a8f94", neutral: true }
|
|
251
|
+
];
|
|
252
|
+
function categoryOf(key) {
|
|
253
|
+
if (!key) return void 0;
|
|
254
|
+
return CATEGORIES.find((c) => c.key === key);
|
|
255
|
+
}
|
|
256
|
+
function findCategory(text2) {
|
|
257
|
+
const q = text2.trim().toLowerCase();
|
|
258
|
+
if (!q) return void 0;
|
|
259
|
+
return CATEGORIES.find((c) => c.key === q || c.en.toLowerCase() === q || c.label === text2.trim());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/model/types.ts
|
|
263
|
+
var ROOT_ID = "root";
|
|
264
|
+
var SCHEMA_VERSION = 5;
|
|
265
|
+
|
|
266
|
+
// node_modules/nanoid/url-alphabet/index.js
|
|
267
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
268
|
+
|
|
269
|
+
// node_modules/nanoid/index.js
|
|
270
|
+
var GET_RANDOM_LIMIT = 65536;
|
|
271
|
+
function fillRandom(buffer) {
|
|
272
|
+
let from = 0;
|
|
273
|
+
while (from < buffer.length) {
|
|
274
|
+
let to = Math.min(from + GET_RANDOM_LIMIT, buffer.length);
|
|
275
|
+
crypto.getRandomValues(buffer.subarray(from, to));
|
|
276
|
+
from = to;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function random(bytes) {
|
|
280
|
+
bytes |= 0;
|
|
281
|
+
if (bytes < 0) throw new RangeError("Wrong ID size");
|
|
282
|
+
let buffer = Buffer.allocUnsafe(bytes);
|
|
283
|
+
fillRandom(buffer);
|
|
284
|
+
return buffer;
|
|
285
|
+
}
|
|
286
|
+
function customRandom(alphabet, defaultSize, getRandom) {
|
|
287
|
+
let safeByteCutoff = 256 - 256 % alphabet.length;
|
|
288
|
+
if (safeByteCutoff === 256) {
|
|
289
|
+
let mask = alphabet.length - 1;
|
|
290
|
+
return (size = defaultSize) => {
|
|
291
|
+
if (!size) return "";
|
|
292
|
+
let id = "";
|
|
293
|
+
while (true) {
|
|
294
|
+
let bytes = getRandom(size);
|
|
295
|
+
let i = size;
|
|
296
|
+
while (i--) {
|
|
297
|
+
id += alphabet[bytes[i] & mask];
|
|
298
|
+
if (id.length >= size) return id;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
let step = Math.ceil(1.6 * 256 * defaultSize / safeByteCutoff);
|
|
304
|
+
return (size = defaultSize) => {
|
|
305
|
+
if (!size) return "";
|
|
306
|
+
let id = "";
|
|
307
|
+
while (true) {
|
|
308
|
+
let bytes = getRandom(step);
|
|
309
|
+
let i = step;
|
|
310
|
+
while (i--) {
|
|
311
|
+
if (bytes[i] < safeByteCutoff) {
|
|
312
|
+
id += alphabet[bytes[i] % alphabet.length];
|
|
313
|
+
if (id.length >= size) return id;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
var POOL_MAX = GET_RANDOM_LIMIT / 2;
|
|
320
|
+
function customAlphabet(alphabet, defaultSize = 21) {
|
|
321
|
+
if (typeof alphabet !== "string" || !alphabet || alphabet.length > 256) {
|
|
322
|
+
return customRandom(alphabet, defaultSize, random);
|
|
323
|
+
}
|
|
324
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
325
|
+
if (alphabet.charCodeAt(i) > 255) {
|
|
326
|
+
return customRandom(alphabet, defaultSize, random);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
let charCodes = Uint8Array.from(alphabet, (str2) => {
|
|
330
|
+
return str2.charCodeAt(0);
|
|
331
|
+
});
|
|
332
|
+
let alphabetLen = alphabet.length;
|
|
333
|
+
let mask = (2 << 31 - Math.clz32(alphabetLen - 1 | 1)) - 1;
|
|
334
|
+
let pool = "";
|
|
335
|
+
let poolOffset = 0;
|
|
336
|
+
let poolNext = 0;
|
|
337
|
+
return (size = defaultSize) => {
|
|
338
|
+
size |= 0;
|
|
339
|
+
if (size < 0) throw new RangeError("Wrong ID size");
|
|
340
|
+
if (size === 0) return "";
|
|
341
|
+
if (poolOffset + size > pool.length) {
|
|
342
|
+
let target = Math.max(poolNext, size);
|
|
343
|
+
poolNext = Math.min(target * 16, POOL_MAX);
|
|
344
|
+
let buffer = Buffer.allocUnsafe(target);
|
|
345
|
+
if (mask === alphabetLen - 1) {
|
|
346
|
+
fillRandom(buffer);
|
|
347
|
+
for (let i = 0; i < target; i++) {
|
|
348
|
+
buffer[i] = charCodes[buffer[i] & mask];
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
let randomBytes = Buffer.allocUnsafe(
|
|
352
|
+
Math.ceil(1.6 * (mask + 1) * target / alphabetLen)
|
|
353
|
+
);
|
|
354
|
+
let accepted = 0;
|
|
355
|
+
while (accepted < target) {
|
|
356
|
+
fillRandom(randomBytes);
|
|
357
|
+
for (let i = 0; i < randomBytes.length; i++) {
|
|
358
|
+
let index = randomBytes[i] & mask;
|
|
359
|
+
if (index < alphabetLen) {
|
|
360
|
+
buffer[accepted++] = charCodes[index];
|
|
361
|
+
if (accepted === target) break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
pool = buffer.toString("latin1");
|
|
367
|
+
poolOffset = 0;
|
|
368
|
+
}
|
|
369
|
+
poolOffset += size;
|
|
370
|
+
return pool.substring(poolOffset - size, poolOffset);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
var nanoid = customAlphabet(urlAlphabet);
|
|
374
|
+
|
|
375
|
+
// src/model/graph.ts
|
|
376
|
+
var LOG_LIMIT = 500;
|
|
377
|
+
var newId = () => nanoid(10);
|
|
378
|
+
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
379
|
+
var clone = (value) => structuredClone(value);
|
|
380
|
+
function nextKey(q) {
|
|
381
|
+
const n = q.nextKey ?? 1;
|
|
382
|
+
q.nextKey = n + 1;
|
|
383
|
+
return `B${n}`;
|
|
384
|
+
}
|
|
385
|
+
function ensureKeys(q) {
|
|
386
|
+
const used = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const b of Object.values(q.blocks)) if (b.key) used.add(b.key);
|
|
388
|
+
let n = q.nextKey ?? 1;
|
|
389
|
+
for (const b of Object.values(q.blocks)) {
|
|
390
|
+
if (b.id === ROOT_ID || b.key) continue;
|
|
391
|
+
while (used.has(`B${n}`)) n++;
|
|
392
|
+
b.key = `B${n}`;
|
|
393
|
+
used.add(b.key);
|
|
394
|
+
n++;
|
|
395
|
+
}
|
|
396
|
+
q.nextKey = Math.max(n, q.nextKey ?? 1);
|
|
397
|
+
}
|
|
398
|
+
var CHILD_PADDING = { left: 120, top: 76 };
|
|
399
|
+
var DEFAULT_HEADER_H = 60;
|
|
400
|
+
function childTop(p, parentId) {
|
|
401
|
+
const b = p.blocks[parentId];
|
|
402
|
+
if (!b) return CHILD_PADDING.top;
|
|
403
|
+
const lines = Math.min(3, Math.max(1, Math.ceil(b.title.length * 14 / 400)));
|
|
404
|
+
return DEFAULT_HEADER_H + (lines - 1) * 18 + 16;
|
|
405
|
+
}
|
|
406
|
+
function createProject(name) {
|
|
407
|
+
const t = now();
|
|
408
|
+
const root = {
|
|
409
|
+
id: ROOT_ID,
|
|
410
|
+
parentId: null,
|
|
411
|
+
title: name,
|
|
412
|
+
description: "",
|
|
413
|
+
status: "black",
|
|
414
|
+
assigneeIds: [],
|
|
415
|
+
position: { x: 0, y: 0 },
|
|
416
|
+
collapsed: false,
|
|
417
|
+
artifacts: [],
|
|
418
|
+
activity: null,
|
|
419
|
+
decisions: []
|
|
420
|
+
};
|
|
421
|
+
const outPort = {
|
|
422
|
+
id: newId(),
|
|
423
|
+
blockId: ROOT_ID,
|
|
424
|
+
direction: "out",
|
|
425
|
+
name: "\u6700\u7D42\u6210\u679C\u7269",
|
|
426
|
+
description: "",
|
|
427
|
+
required: true,
|
|
428
|
+
artifacts: []
|
|
429
|
+
};
|
|
430
|
+
const base = {
|
|
431
|
+
schemaVersion: SCHEMA_VERSION,
|
|
432
|
+
id: newId(),
|
|
433
|
+
name,
|
|
434
|
+
description: "",
|
|
435
|
+
createdAt: t,
|
|
436
|
+
visibility: "private",
|
|
437
|
+
members: [],
|
|
438
|
+
blocks: { [ROOT_ID]: root },
|
|
439
|
+
ports: { [outPort.id]: outPort },
|
|
440
|
+
edges: {},
|
|
441
|
+
terminals: { in: { x: 0, y: 200 }, out: { x: 1100, y: 200 } },
|
|
442
|
+
log: [],
|
|
443
|
+
agents: {},
|
|
444
|
+
nextKey: 1
|
|
445
|
+
};
|
|
446
|
+
return addProjectBlock(base, name).project;
|
|
447
|
+
}
|
|
448
|
+
function addProjectBlock(p, name) {
|
|
449
|
+
const q = clone(p);
|
|
450
|
+
const existing = projectBlocks(q);
|
|
451
|
+
const id = newId();
|
|
452
|
+
const y = existing.length === 0 ? 40 : Math.max(...existing.map((b) => b.position.y)) + 400;
|
|
453
|
+
q.blocks[id] = {
|
|
454
|
+
id,
|
|
455
|
+
key: nextKey(q),
|
|
456
|
+
kind: "project",
|
|
457
|
+
parentId: ROOT_ID,
|
|
458
|
+
title: name,
|
|
459
|
+
description: "",
|
|
460
|
+
status: "black",
|
|
461
|
+
assigneeIds: [],
|
|
462
|
+
position: { x: 300, y },
|
|
463
|
+
collapsed: false,
|
|
464
|
+
artifacts: [],
|
|
465
|
+
activity: null,
|
|
466
|
+
decisions: []
|
|
467
|
+
};
|
|
468
|
+
const out2 = { id: newId(), blockId: id, direction: "out", name: "\u6700\u7D42\u6210\u679C\u7269", description: "", required: true, artifacts: [] };
|
|
469
|
+
q.ports[out2.id] = out2;
|
|
470
|
+
let rootOut = portsOf(q, ROOT_ID, "out").find((o) => incomingEdges(q, { portId: o.id, side: "inner" }).length === 0);
|
|
471
|
+
if (!rootOut) {
|
|
472
|
+
rootOut = { id: newId(), blockId: ROOT_ID, direction: "out", name: existing.length === 0 ? "\u6700\u7D42\u6210\u679C\u7269" : `${name} \u306E\u6210\u679C\u7269`, description: "", required: true, artifacts: [] };
|
|
473
|
+
q.ports[rootOut.id] = rootOut;
|
|
474
|
+
}
|
|
475
|
+
const eid = newId();
|
|
476
|
+
q.edges[eid] = { id: eid, from: { portId: out2.id, side: "outer" }, to: { portId: rootOut.id, side: "inner" }, kind: "up", auto: false };
|
|
477
|
+
return { project: q, blockId: id };
|
|
478
|
+
}
|
|
479
|
+
function projectBlocks(p) {
|
|
480
|
+
return childrenOf(p, ROOT_ID).filter((b) => b.kind === "project");
|
|
481
|
+
}
|
|
482
|
+
function defaultTaskParent(p) {
|
|
483
|
+
return projectBlocks(p)[0]?.id ?? ROOT_ID;
|
|
484
|
+
}
|
|
485
|
+
function normalizeCollapsed(p) {
|
|
486
|
+
const ids = Object.values(p.blocks).filter((b) => b.id !== ROOT_ID && kindOf(b) !== "project" && !b.collapsed).map((b) => b.id);
|
|
487
|
+
if (ids.length === 0) return p;
|
|
488
|
+
const blocks = { ...p.blocks };
|
|
489
|
+
for (const id of ids) blocks[id] = { ...blocks[id], collapsed: true };
|
|
490
|
+
return { ...p, blocks };
|
|
491
|
+
}
|
|
492
|
+
var kindOf = (b) => b.kind ?? "task";
|
|
493
|
+
function createArtifact(title, url = "") {
|
|
494
|
+
return { id: newId(), title, url, kind: url ? "url" : "note", note: "" };
|
|
495
|
+
}
|
|
496
|
+
function createGitArtifact(title, git2) {
|
|
497
|
+
return { id: newId(), title, url: git2.url ?? "", kind: "git", note: "", repo: git2.repo, path: git2.path, commit: git2.commit, blob: git2.blob, state: "ok", checkedAt: now() };
|
|
498
|
+
}
|
|
499
|
+
function childrenOf(p, blockId) {
|
|
500
|
+
return Object.values(p.blocks).filter((b) => b.parentId === blockId);
|
|
501
|
+
}
|
|
502
|
+
function descendantsOf(p, blockId) {
|
|
503
|
+
const out2 = [];
|
|
504
|
+
const stack = [blockId];
|
|
505
|
+
while (stack.length > 0) {
|
|
506
|
+
const id = stack.pop();
|
|
507
|
+
for (const c of childrenOf(p, id)) {
|
|
508
|
+
out2.push(c);
|
|
509
|
+
stack.push(c.id);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return out2;
|
|
513
|
+
}
|
|
514
|
+
function ancestorsOf(p, blockId) {
|
|
515
|
+
const out2 = [];
|
|
516
|
+
let cur = p.blocks[blockId]?.parentId ?? null;
|
|
517
|
+
while (cur !== null) {
|
|
518
|
+
const b = p.blocks[cur];
|
|
519
|
+
if (!b) break;
|
|
520
|
+
out2.push(b);
|
|
521
|
+
cur = b.parentId;
|
|
522
|
+
}
|
|
523
|
+
return out2;
|
|
524
|
+
}
|
|
525
|
+
function portsOf(p, blockId, direction) {
|
|
526
|
+
return Object.values(p.ports).filter((q) => q.blockId === blockId && (direction === void 0 || q.direction === direction));
|
|
527
|
+
}
|
|
528
|
+
var sameEndpoint = (a, b) => a.portId === b.portId && a.side === b.side;
|
|
529
|
+
function incomingEdges(p, ep) {
|
|
530
|
+
return Object.values(p.edges).filter((e) => sameEndpoint(e.to, ep));
|
|
531
|
+
}
|
|
532
|
+
function outgoingEdges(p, ep) {
|
|
533
|
+
return Object.values(p.edges).filter((e) => sameEndpoint(e.from, ep));
|
|
534
|
+
}
|
|
535
|
+
function scopeOf(p, ep) {
|
|
536
|
+
const port = p.ports[ep.portId];
|
|
537
|
+
if (!port) return null;
|
|
538
|
+
const block = p.blocks[port.blockId];
|
|
539
|
+
if (!block) return null;
|
|
540
|
+
return ep.side === "inner" ? block.id : block.parentId;
|
|
541
|
+
}
|
|
542
|
+
function validateConnection(p, from, to) {
|
|
543
|
+
const fp = p.ports[from.portId];
|
|
544
|
+
const tp = p.ports[to.portId];
|
|
545
|
+
if (!fp || !tp) return { ok: false, reason: "\u30DD\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" };
|
|
546
|
+
if (from.portId === to.portId) return { ok: false, reason: "\u540C\u3058\u30DD\u30FC\u30C8\u3069\u3046\u3057\u306F\u3064\u306A\u3052\u307E\u305B\u3093" };
|
|
547
|
+
const fromIsSource = fp.direction === "out" && from.side === "outer" || fp.direction === "in" && from.side === "inner";
|
|
548
|
+
const toIsTarget = tp.direction === "in" && to.side === "outer" || tp.direction === "out" && to.side === "inner";
|
|
549
|
+
if (!fromIsSource) return { ok: false, reason: "\u7DDA\u306F\u51FA\u529B\u30DD\u30FC\u30C8 (\u307E\u305F\u306F\u89AA\u306E\u5165\u529B) \u304B\u3089\u5F15\u3044\u3066\u304F\u3060\u3055\u3044" };
|
|
550
|
+
if (!toIsTarget) return { ok: false, reason: "\u7DDA\u306F\u5165\u529B\u30DD\u30FC\u30C8 (\u307E\u305F\u306F\u89AA\u306E\u51FA\u529B) \u306B\u3064\u306A\u3044\u3067\u304F\u3060\u3055\u3044" };
|
|
551
|
+
const sf = scopeOf(p, from);
|
|
552
|
+
const st = scopeOf(p, to);
|
|
553
|
+
if (sf === null || st === null || sf !== st) return { ok: false, reason: "\u540C\u3058\u968E\u5C64\u306E\u30DD\u30FC\u30C8\u3069\u3046\u3057\u3060\u3051\u3064\u306A\u3052\u307E\u3059" };
|
|
554
|
+
let kind;
|
|
555
|
+
if (fp.direction === "out" && tp.direction === "in") kind = "sibling";
|
|
556
|
+
else if (fp.direction === "out" && tp.direction === "out") kind = "up";
|
|
557
|
+
else if (fp.direction === "in" && tp.direction === "in") kind = "down";
|
|
558
|
+
else kind = "through";
|
|
559
|
+
if (kind === "sibling" && wouldCreateCycle(p, fp.blockId, tp.blockId)) {
|
|
560
|
+
return { ok: false, reason: "\u5FAA\u74B0\u3059\u308B\u7D50\u7DDA\u306F\u3067\u304D\u307E\u305B\u3093" };
|
|
561
|
+
}
|
|
562
|
+
return { ok: true, kind };
|
|
563
|
+
}
|
|
564
|
+
function wouldCreateCycle(p, fromBlock, toBlock) {
|
|
565
|
+
if (fromBlock === toBlock) return true;
|
|
566
|
+
const visited = /* @__PURE__ */ new Set();
|
|
567
|
+
const stack = [toBlock];
|
|
568
|
+
while (stack.length > 0) {
|
|
569
|
+
const cur = stack.pop();
|
|
570
|
+
if (cur === fromBlock) return true;
|
|
571
|
+
if (visited.has(cur)) continue;
|
|
572
|
+
visited.add(cur);
|
|
573
|
+
for (const port of portsOf(p, cur, "out")) {
|
|
574
|
+
for (const e of outgoingEdges(p, { portId: port.id, side: "outer" })) {
|
|
575
|
+
if (e.kind !== "sibling") continue;
|
|
576
|
+
const next = p.ports[e.to.portId]?.blockId;
|
|
577
|
+
if (next) stack.push(next);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
function touch(p) {
|
|
584
|
+
return clone(p);
|
|
585
|
+
}
|
|
586
|
+
function addBlock(p, args) {
|
|
587
|
+
const q = touch(p);
|
|
588
|
+
const id = newId();
|
|
589
|
+
const siblings = childrenOf(q, args.parentId);
|
|
590
|
+
let defaultPos;
|
|
591
|
+
if (args.parentId === ROOT_ID) {
|
|
592
|
+
defaultPos = siblings.length === 0 ? { x: 260, y: 80 } : { x: Math.max(...siblings.map((s) => s.position.x)) + 300, y: 80 };
|
|
593
|
+
} else {
|
|
594
|
+
defaultPos = siblings.length === 0 ? { x: CHILD_PADDING.left, y: childTop(p, args.parentId) } : { x: CHILD_PADDING.left, y: Math.max(...siblings.map((s) => s.position.y)) + 110 };
|
|
595
|
+
}
|
|
596
|
+
q.blocks[id] = {
|
|
597
|
+
id,
|
|
598
|
+
key: nextKey(q),
|
|
599
|
+
parentId: args.parentId,
|
|
600
|
+
title: args.title,
|
|
601
|
+
description: "",
|
|
602
|
+
status: "black",
|
|
603
|
+
assigneeIds: [],
|
|
604
|
+
position: args.position ?? defaultPos,
|
|
605
|
+
collapsed: false,
|
|
606
|
+
artifacts: [],
|
|
607
|
+
activity: null,
|
|
608
|
+
decisions: []
|
|
609
|
+
};
|
|
610
|
+
const out2 = { id: newId(), blockId: id, direction: "out", name: args.outputName ?? "\u51FA\u529B", description: "", required: true, artifacts: [] };
|
|
611
|
+
q.ports[out2.id] = out2;
|
|
612
|
+
if (args.actor) appendLog(q, { actor: args.actor, kind: "added", blockId: id, message: `\u300C${args.title}\u300D\u3092\u8FFD\u52A0` });
|
|
613
|
+
const parent = q.blocks[args.parentId];
|
|
614
|
+
if (parent && parent.id !== ROOT_ID && parent.status === "black") parent.status = "gray";
|
|
615
|
+
return { project: normalizePromotions(q), blockId: id };
|
|
616
|
+
}
|
|
617
|
+
function updateBlock(p, blockId, patch) {
|
|
618
|
+
const q = touch(p);
|
|
619
|
+
const b = q.blocks[blockId];
|
|
620
|
+
if (!b) return p;
|
|
621
|
+
Object.assign(b, patch);
|
|
622
|
+
return q;
|
|
623
|
+
}
|
|
624
|
+
function removeBlock(p, blockId) {
|
|
625
|
+
if (blockId === ROOT_ID) return p;
|
|
626
|
+
const q = touch(p);
|
|
627
|
+
const ids = /* @__PURE__ */ new Set([blockId, ...descendantsOf(q, blockId).map((b) => b.id)]);
|
|
628
|
+
const portIds = new Set(Object.values(q.ports).filter((x) => ids.has(x.blockId)).map((x) => x.id));
|
|
629
|
+
for (const e of Object.values(q.edges)) {
|
|
630
|
+
if (portIds.has(e.from.portId) || portIds.has(e.to.portId)) delete q.edges[e.id];
|
|
631
|
+
}
|
|
632
|
+
for (const id of portIds) delete q.ports[id];
|
|
633
|
+
for (const id of ids) delete q.blocks[id];
|
|
634
|
+
return normalizePromotions(q);
|
|
635
|
+
}
|
|
636
|
+
function addPort(p, args) {
|
|
637
|
+
if (args.direction === "out" && args.blockId !== ROOT_ID && !canAddOutput(p, args.blockId)) {
|
|
638
|
+
return { project: p, portId: portsOf(p, args.blockId, "out")[0]?.id ?? "" };
|
|
639
|
+
}
|
|
640
|
+
const q = touch(p);
|
|
641
|
+
const id = newId();
|
|
642
|
+
q.ports[id] = {
|
|
643
|
+
id,
|
|
644
|
+
blockId: args.blockId,
|
|
645
|
+
direction: args.direction,
|
|
646
|
+
name: args.name,
|
|
647
|
+
description: "",
|
|
648
|
+
required: true,
|
|
649
|
+
artifacts: []
|
|
650
|
+
};
|
|
651
|
+
if (args.direction === "out" && kindOf(q.blocks[args.blockId]) === "project") mirrorProjectOutput(q, id);
|
|
652
|
+
return { project: normalizePromotions(q), portId: id };
|
|
653
|
+
}
|
|
654
|
+
function mirrorProjectOutput(q, portId) {
|
|
655
|
+
const port = q.ports[portId];
|
|
656
|
+
if (!port || port.direction !== "out") return;
|
|
657
|
+
const already = Object.values(q.edges).some((e) => e.from.portId === portId && e.from.side === "outer" && q.ports[e.to.portId]?.blockId === ROOT_ID);
|
|
658
|
+
if (already) return;
|
|
659
|
+
const rootOut = { id: newId(), blockId: ROOT_ID, direction: "out", name: port.name, description: port.description, required: true, artifacts: [] };
|
|
660
|
+
q.ports[rootOut.id] = rootOut;
|
|
661
|
+
const eid = newId();
|
|
662
|
+
q.edges[eid] = { id: eid, from: { portId, side: "outer" }, to: { portId: rootOut.id, side: "inner" }, kind: "up", auto: true };
|
|
663
|
+
}
|
|
664
|
+
function mirroredRootOutput(p, portId) {
|
|
665
|
+
const e = Object.values(p.edges).find((x) => x.from.portId === portId && x.from.side === "outer" && p.ports[x.to.portId]?.blockId === ROOT_ID);
|
|
666
|
+
return e ? p.ports[e.to.portId] ?? null : null;
|
|
667
|
+
}
|
|
668
|
+
function canAddOutput(p, blockId) {
|
|
669
|
+
if (kindOf(p.blocks[blockId]) === "project") return true;
|
|
670
|
+
if (blockId === ROOT_ID) return true;
|
|
671
|
+
if (portsOf(p, blockId, "out").length === 0) return true;
|
|
672
|
+
return childrenOf(p, blockId).length > 0;
|
|
673
|
+
}
|
|
674
|
+
function updatePort(p, portId, patch) {
|
|
675
|
+
const q = touch(p);
|
|
676
|
+
const x = q.ports[portId];
|
|
677
|
+
if (!x) return p;
|
|
678
|
+
Object.assign(x, patch);
|
|
679
|
+
if (patch.name !== void 0 && x.direction === "out" && kindOf(q.blocks[x.blockId]) === "project") {
|
|
680
|
+
const mirror = mirroredRootOutput(q, portId);
|
|
681
|
+
if (mirror) mirror.name = patch.name;
|
|
682
|
+
}
|
|
683
|
+
return q;
|
|
684
|
+
}
|
|
685
|
+
function connect(p, from, to) {
|
|
686
|
+
const check = validateConnection(p, from, to);
|
|
687
|
+
if (!check.ok) return { project: p, error: check.reason };
|
|
688
|
+
const q = touch(p);
|
|
689
|
+
for (const e of incomingEdges(q, to)) delete q.edges[e.id];
|
|
690
|
+
const id = newId();
|
|
691
|
+
q.edges[id] = { id, from, to, kind: check.kind, auto: false };
|
|
692
|
+
return { project: normalizePromotions(q), edgeId: id };
|
|
693
|
+
}
|
|
694
|
+
function disconnect(p, edgeId) {
|
|
695
|
+
if (!p.edges[edgeId]) return p;
|
|
696
|
+
const q = touch(p);
|
|
697
|
+
delete q.edges[edgeId];
|
|
698
|
+
return normalizePromotions(q);
|
|
699
|
+
}
|
|
700
|
+
function normalizePromotions(p) {
|
|
701
|
+
const q = clone(p);
|
|
702
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
703
|
+
let changed = false;
|
|
704
|
+
for (const e of Object.values(q.edges)) {
|
|
705
|
+
if (!e.auto) continue;
|
|
706
|
+
const manual = incomingEdges(q, e.to).some((o) => !o.auto);
|
|
707
|
+
if (manual) {
|
|
708
|
+
delete q.edges[e.id];
|
|
709
|
+
changed = true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
for (const port of Object.values(q.ports)) {
|
|
713
|
+
if (!port.promotedFrom) continue;
|
|
714
|
+
const used = outgoingEdges(q, { portId: port.id, side: "inner" }).length > 0;
|
|
715
|
+
if (!used) {
|
|
716
|
+
for (const e of Object.values(q.edges)) {
|
|
717
|
+
if (e.from.portId === port.id || e.to.portId === port.id) delete q.edges[e.id];
|
|
718
|
+
}
|
|
719
|
+
delete q.ports[port.id];
|
|
720
|
+
changed = true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
for (const port of Object.values(q.ports)) {
|
|
724
|
+
if (!port.promotedFrom || !q.ports[port.id]) continue;
|
|
725
|
+
const twin = Object.values(q.ports).find((x) => x.id !== port.id && x.blockId === port.blockId && x.direction === "in" && x.promotedFrom && x.name === port.name);
|
|
726
|
+
if (!twin) continue;
|
|
727
|
+
for (const e of Object.values(q.edges)) {
|
|
728
|
+
if (e.from.portId === twin.id) e.from = { ...e.from, portId: port.id };
|
|
729
|
+
if (e.to.portId === twin.id) e.to = { ...e.to, portId: port.id };
|
|
730
|
+
}
|
|
731
|
+
delete q.ports[twin.id];
|
|
732
|
+
changed = true;
|
|
733
|
+
}
|
|
734
|
+
for (const port of Object.values(q.ports)) {
|
|
735
|
+
if (port.direction !== "in") continue;
|
|
736
|
+
const block = q.blocks[port.blockId];
|
|
737
|
+
if (!block || block.parentId === null) continue;
|
|
738
|
+
if (incomingEdges(q, { portId: port.id, side: "outer" }).length > 0) continue;
|
|
739
|
+
let promoted = Object.values(q.ports).find((x) => x.blockId === block.parentId && x.direction === "in" && x.promotedFrom && x.name === port.name);
|
|
740
|
+
if (!promoted) {
|
|
741
|
+
promoted = {
|
|
742
|
+
id: newId(),
|
|
743
|
+
blockId: block.parentId,
|
|
744
|
+
direction: "in",
|
|
745
|
+
name: port.name,
|
|
746
|
+
description: port.description,
|
|
747
|
+
required: port.required,
|
|
748
|
+
artifacts: [],
|
|
749
|
+
promotedFrom: port.id
|
|
750
|
+
};
|
|
751
|
+
q.ports[promoted.id] = promoted;
|
|
752
|
+
}
|
|
753
|
+
const id = newId();
|
|
754
|
+
q.edges[id] = { id, from: { portId: promoted.id, side: "inner" }, to: { portId: port.id, side: "outer" }, kind: "down", auto: true };
|
|
755
|
+
changed = true;
|
|
756
|
+
}
|
|
757
|
+
if (!changed) break;
|
|
758
|
+
}
|
|
759
|
+
return q;
|
|
760
|
+
}
|
|
761
|
+
function effectiveProgress(p, blockId) {
|
|
762
|
+
const b = p.blocks[blockId];
|
|
763
|
+
if (!b) return 0;
|
|
764
|
+
if (b.status === "white") return 100;
|
|
765
|
+
if (typeof b.progress === "number") return Math.max(0, Math.min(100, b.progress));
|
|
766
|
+
const kids = childrenOf(p, blockId);
|
|
767
|
+
if (kids.length === 0) return 0;
|
|
768
|
+
return Math.round(kids.reduce((acc, k) => acc + effectiveProgress(p, k.id), 0) / kids.length);
|
|
769
|
+
}
|
|
770
|
+
function computeProgress(p, blockId) {
|
|
771
|
+
const targets = descendantsOf(p, blockId).filter((b) => kindOf(b) !== "project");
|
|
772
|
+
const list2 = targets.length > 0 ? targets : blockId === ROOT_ID ? [] : [p.blocks[blockId]].filter((b) => b && kindOf(b) !== "project");
|
|
773
|
+
const count = (s) => list2.filter((b) => b.status === s).length;
|
|
774
|
+
const white = count("white");
|
|
775
|
+
const total = list2.length;
|
|
776
|
+
const percent = blockId === ROOT_ID ? (() => {
|
|
777
|
+
const pj = projectBlocks(p);
|
|
778
|
+
return pj.length === 0 ? 0 : Math.round(pj.reduce((acc, b) => acc + effectiveProgress(p, b.id), 0) / pj.length);
|
|
779
|
+
})() : effectiveProgress(p, blockId);
|
|
780
|
+
return { total, white, gray: count("gray"), black: count("black"), ratio: total === 0 ? 0 : white / total, percent };
|
|
781
|
+
}
|
|
782
|
+
function setProgress(p, blockId, value, actor) {
|
|
783
|
+
const b = p.blocks[blockId];
|
|
784
|
+
if (!b) return p;
|
|
785
|
+
const q = touch(p);
|
|
786
|
+
if (value === null) delete q.blocks[blockId].progress;
|
|
787
|
+
else q.blocks[blockId].progress = Math.max(0, Math.min(100, Math.round(value)));
|
|
788
|
+
if (value !== null && b.status === "black" && value > 0) q.blocks[blockId].status = "gray";
|
|
789
|
+
appendLog(q, { actor, kind: "note", blockId, message: `\u300C${b.title}\u300D\u306E\u9032\u6357 ${value === null ? "\u81EA\u52D5" : value + "%"}` });
|
|
790
|
+
return q;
|
|
791
|
+
}
|
|
792
|
+
function isSourceReady(p, ep, seen = /* @__PURE__ */ new Set()) {
|
|
793
|
+
const key = `${ep.portId}:${ep.side}`;
|
|
794
|
+
if (seen.has(key)) return false;
|
|
795
|
+
seen.add(key);
|
|
796
|
+
const port = p.ports[ep.portId];
|
|
797
|
+
if (!port) return false;
|
|
798
|
+
if (port.direction === "out") {
|
|
799
|
+
const b = p.blocks[port.blockId];
|
|
800
|
+
return port.artifacts.length > 0 || b?.status === "white";
|
|
801
|
+
}
|
|
802
|
+
if (port.artifacts.length > 0) return true;
|
|
803
|
+
const incoming = incomingEdges(p, { portId: port.id, side: "outer" });
|
|
804
|
+
return incoming.some((e) => isSourceReady(p, e.from, seen));
|
|
805
|
+
}
|
|
806
|
+
function isEdgeReady(p, e) {
|
|
807
|
+
return isSourceReady(p, e.from);
|
|
808
|
+
}
|
|
809
|
+
function isInputReady(p, portId) {
|
|
810
|
+
const port = p.ports[portId];
|
|
811
|
+
if (!port || port.direction !== "in") return false;
|
|
812
|
+
if (port.artifacts.length > 0) return true;
|
|
813
|
+
return incomingEdges(p, { portId, side: "outer" }).some((e) => isEdgeReady(p, e));
|
|
814
|
+
}
|
|
815
|
+
function missingRequiredInputs(p, blockId) {
|
|
816
|
+
return portsOf(p, blockId, "in").filter((q) => q.required && !isInputReady(p, q.id));
|
|
817
|
+
}
|
|
818
|
+
function toJSON(p) {
|
|
819
|
+
const { updatedAt: _u, version: _v, ...rest } = p;
|
|
820
|
+
return JSON.stringify(rest, null, 2);
|
|
821
|
+
}
|
|
822
|
+
function fromJSON(text2) {
|
|
823
|
+
let data;
|
|
824
|
+
try {
|
|
825
|
+
data = JSON.parse(text2);
|
|
826
|
+
} catch {
|
|
827
|
+
throw new Error("JSON \u3068\u3057\u3066\u8AAD\u3081\u307E\u305B\u3093");
|
|
828
|
+
}
|
|
829
|
+
if (typeof data !== "object" || data === null) throw new Error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093");
|
|
830
|
+
const d = data;
|
|
831
|
+
const ver = Number(d.schemaVersion);
|
|
832
|
+
if (![1, 2, 3, 4, 5].includes(ver)) throw new Error(`\u5BFE\u5FDC\u3057\u3066\u3044\u306A\u3044\u30C7\u30FC\u30BF\u5F62\u5F0F\u306E\u7248\u3067\u3059 (schemaVersion=${String(d.schemaVersion)})`);
|
|
833
|
+
if (!d.blocks || !d.ports || !d.edges || !d.blocks[ROOT_ID]) throw new Error("\u30D6\u30ED\u30C3\u30AF\u30FB\u30DD\u30FC\u30C8\u30FB\u7DDA\u306E\u30C7\u30FC\u30BF\u304C\u8DB3\u308A\u307E\u305B\u3093");
|
|
834
|
+
const q = {
|
|
835
|
+
schemaVersion: SCHEMA_VERSION,
|
|
836
|
+
id: typeof d.id === "string" ? d.id : newId(),
|
|
837
|
+
name: typeof d.name === "string" ? d.name : "\u7121\u984C",
|
|
838
|
+
description: typeof d.description === "string" ? d.description : "",
|
|
839
|
+
createdAt: typeof d.createdAt === "string" ? d.createdAt : now(),
|
|
840
|
+
visibility: d.visibility ?? "private",
|
|
841
|
+
members: Array.isArray(d.members) ? d.members : [],
|
|
842
|
+
blocks: d.blocks,
|
|
843
|
+
ports: d.ports,
|
|
844
|
+
edges: d.edges,
|
|
845
|
+
terminals: d.terminals ?? { in: { x: 0, y: 200 }, out: { x: 1100, y: 200 } },
|
|
846
|
+
log: Array.isArray(d.log) ? d.log : [],
|
|
847
|
+
agents: d.agents && typeof d.agents === "object" ? d.agents : {},
|
|
848
|
+
nextKey: typeof d.nextKey === "number" ? d.nextKey : 1,
|
|
849
|
+
inputGroups: Array.isArray(d.inputGroups) ? d.inputGroups : []
|
|
850
|
+
};
|
|
851
|
+
for (const x of Object.values(q.ports)) if (x.groupId && !q.inputGroups.some((gp) => gp.id === x.groupId)) delete x.groupId;
|
|
852
|
+
for (const b of Object.values(q.blocks)) {
|
|
853
|
+
if (b.activity === void 0) b.activity = null;
|
|
854
|
+
if (!Array.isArray(b.decisions)) b.decisions = [];
|
|
855
|
+
if (!Array.isArray(b.artifacts)) b.artifacts = [];
|
|
856
|
+
if (!Array.isArray(b.assigneeIds)) b.assigneeIds = [];
|
|
857
|
+
if (b.progress !== void 0 && typeof b.progress !== "number") delete b.progress;
|
|
858
|
+
}
|
|
859
|
+
if (ver < 4) {
|
|
860
|
+
for (const x of Object.values(q.ports)) if (x.direction === "in") x.required = true;
|
|
861
|
+
}
|
|
862
|
+
ensureKeys(q);
|
|
863
|
+
for (const x of Object.values(q.ports)) if (!q.blocks[x.blockId]) delete q.ports[x.id];
|
|
864
|
+
for (const e of Object.values(q.edges)) if (!q.ports[e.from.portId] || !q.ports[e.to.portId]) delete q.edges[e.id];
|
|
865
|
+
return normalizePromotions(wrapIntoProject(q));
|
|
866
|
+
}
|
|
867
|
+
function wrapIntoProject(p) {
|
|
868
|
+
const topTasks = childrenOf(p, ROOT_ID).filter((b) => kindOf(b) !== "project");
|
|
869
|
+
if (topTasks.length === 0) return p;
|
|
870
|
+
const q = clone(p);
|
|
871
|
+
const pid = newId();
|
|
872
|
+
q.blocks[pid] = {
|
|
873
|
+
id: pid,
|
|
874
|
+
key: nextKey(q),
|
|
875
|
+
kind: "project",
|
|
876
|
+
parentId: ROOT_ID,
|
|
877
|
+
title: q.name,
|
|
878
|
+
description: q.description,
|
|
879
|
+
status: "gray",
|
|
880
|
+
assigneeIds: [],
|
|
881
|
+
position: { x: 300, y: 40 },
|
|
882
|
+
collapsed: false,
|
|
883
|
+
artifacts: [],
|
|
884
|
+
activity: null,
|
|
885
|
+
decisions: []
|
|
886
|
+
};
|
|
887
|
+
const minX = Math.min(...topTasks.map((b) => b.position.x));
|
|
888
|
+
const minY = Math.min(...topTasks.map((b) => b.position.y));
|
|
889
|
+
for (const b of topTasks) {
|
|
890
|
+
q.blocks[b.id].parentId = pid;
|
|
891
|
+
q.blocks[b.id].position = { x: b.position.x - minX + CHILD_PADDING.left, y: b.position.y - minY + childTop(q, pid) };
|
|
892
|
+
}
|
|
893
|
+
for (const e of Object.values(q.edges)) {
|
|
894
|
+
const tp = q.ports[e.to.portId];
|
|
895
|
+
const fp = q.ports[e.from.portId];
|
|
896
|
+
if (!tp || !fp) continue;
|
|
897
|
+
if (e.kind === "up" && tp.blockId === ROOT_ID && e.to.side === "inner") {
|
|
898
|
+
let mid = portsOf(q, pid, "out").find((o) => o.name === tp.name);
|
|
899
|
+
if (!mid) {
|
|
900
|
+
mid = { id: newId(), blockId: pid, direction: "out", name: tp.name, description: tp.description, required: true, artifacts: [] };
|
|
901
|
+
q.ports[mid.id] = mid;
|
|
902
|
+
const up = newId();
|
|
903
|
+
q.edges[up] = { id: up, from: { portId: mid.id, side: "outer" }, to: { portId: tp.id, side: "inner" }, kind: "up", auto: false };
|
|
904
|
+
}
|
|
905
|
+
e.to = { portId: mid.id, side: "inner" };
|
|
906
|
+
}
|
|
907
|
+
if (e.kind === "down" && fp.blockId === ROOT_ID && e.from.side === "inner") {
|
|
908
|
+
if (e.auto) {
|
|
909
|
+
delete q.edges[e.id];
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
let mid = portsOf(q, pid, "in").find((o) => o.name === fp.name);
|
|
913
|
+
if (!mid) {
|
|
914
|
+
mid = { id: newId(), blockId: pid, direction: "in", name: fp.name, description: fp.description, required: fp.required, artifacts: [] };
|
|
915
|
+
q.ports[mid.id] = mid;
|
|
916
|
+
const down = newId();
|
|
917
|
+
q.edges[down] = { id: down, from: { portId: fp.id, side: "inner" }, to: { portId: mid.id, side: "outer" }, kind: "down", auto: false };
|
|
918
|
+
}
|
|
919
|
+
e.from = { portId: mid.id, side: "inner" };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const x of portsOf(q, ROOT_ID, "in")) if (x.promotedFrom) delete q.ports[x.id];
|
|
923
|
+
for (const e of Object.values(q.edges)) if (!q.ports[e.from.portId] || !q.ports[e.to.portId]) delete q.edges[e.id];
|
|
924
|
+
if (portsOf(q, pid, "out").length === 0) {
|
|
925
|
+
const out2 = { id: newId(), blockId: pid, direction: "out", name: "\u6700\u7D42\u6210\u679C\u7269", description: "", required: true, artifacts: [] };
|
|
926
|
+
q.ports[out2.id] = out2;
|
|
927
|
+
const rootOut = portsOf(q, ROOT_ID, "out")[0];
|
|
928
|
+
if (rootOut && incomingEdges(q, { portId: rootOut.id, side: "inner" }).length === 0) {
|
|
929
|
+
const up = newId();
|
|
930
|
+
q.edges[up] = { id: up, from: { portId: out2.id, side: "outer" }, to: { portId: rootOut.id, side: "inner" }, kind: "up", auto: false };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return q;
|
|
934
|
+
}
|
|
935
|
+
function appendLog(q, ev) {
|
|
936
|
+
q.log.push({ id: newId(), at: now(), ...ev });
|
|
937
|
+
if (q.log.length > LOG_LIMIT) q.log.splice(0, q.log.length - LOG_LIMIT);
|
|
938
|
+
q.agents[ev.actor] = { lastSeen: now() };
|
|
939
|
+
}
|
|
940
|
+
function setActivity(p, blockId, actor, state, note) {
|
|
941
|
+
const q = touch(p);
|
|
942
|
+
const b = q.blocks[blockId];
|
|
943
|
+
if (!b || blockId === ROOT_ID) return p;
|
|
944
|
+
b.activity = { actor, state, note, since: now() };
|
|
945
|
+
if (state === "working" && b.status === "black") b.status = "gray";
|
|
946
|
+
const kind = state === "working" ? "started" : state === "blocked" ? "blocked" : "note";
|
|
947
|
+
const label = state === "working" ? "\u958B\u59CB" : state === "blocked" ? "\u8A70\u307E\u308A" : state === "waiting_review" ? "\u78BA\u8A8D\u5F85\u3061" : "\u5224\u65AD\u5F85\u3061";
|
|
948
|
+
appendLog(q, { actor, kind, blockId, message: `\u300C${b.title}\u300D${label}${note ? ": " + note : ""}` });
|
|
949
|
+
return q;
|
|
950
|
+
}
|
|
951
|
+
function clearActivity(p, blockId) {
|
|
952
|
+
const q = touch(p);
|
|
953
|
+
const b = q.blocks[blockId];
|
|
954
|
+
if (!b) return p;
|
|
955
|
+
b.activity = null;
|
|
956
|
+
return q;
|
|
957
|
+
}
|
|
958
|
+
function finishBlock(p, blockId, actor, args = {}) {
|
|
959
|
+
const b = p.blocks[blockId];
|
|
960
|
+
if (!b || blockId === ROOT_ID) return { project: p, error: "\u30D6\u30ED\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" };
|
|
961
|
+
const outs = portsOf(p, blockId, "out");
|
|
962
|
+
let target = outs[0];
|
|
963
|
+
if (args.outputName) {
|
|
964
|
+
const found = outs.find((o) => o.name === args.outputName);
|
|
965
|
+
if (!found) return { project: p, error: `\u51FA\u529B\u300C${args.outputName}\u300D\u304C\u3042\u308A\u307E\u305B\u3093 (${outs.map((o) => o.name).join(", ")})` };
|
|
966
|
+
target = found;
|
|
967
|
+
}
|
|
968
|
+
const q = touch(p);
|
|
969
|
+
if (target && args.artifacts) {
|
|
970
|
+
for (const a of args.artifacts) q.ports[target.id].artifacts.push("id" in a && "kind" in a ? a : createArtifact(a.title, a.url ?? ""));
|
|
971
|
+
}
|
|
972
|
+
q.blocks[blockId].activity = null;
|
|
973
|
+
q.blocks[blockId].status = "white";
|
|
974
|
+
const arts = (args.artifacts ?? []).map((a) => a.title).join(", ");
|
|
975
|
+
appendLog(q, { actor, kind: "done", blockId, message: `\u300C${b.title}\u300D\u5B8C\u4E86${arts ? " (" + arts + ")" : ""}${args.note ? ": " + args.note : ""}` });
|
|
976
|
+
return { project: q };
|
|
977
|
+
}
|
|
978
|
+
function askDecision(p, blockId, actor, question, options = [], context = "") {
|
|
979
|
+
const b = p.blocks[blockId];
|
|
980
|
+
if (!b || blockId === ROOT_ID) return { project: p };
|
|
981
|
+
const q = touch(p);
|
|
982
|
+
const d = { id: newId(), question, options, askedBy: actor, askedAt: now(), ...context ? { context } : {} };
|
|
983
|
+
q.blocks[blockId].decisions.push(d);
|
|
984
|
+
q.blocks[blockId].activity = { actor, state: "needs_decision", note: question, since: now() };
|
|
985
|
+
appendLog(q, { actor, kind: "asked", blockId, message: `\u300C${b.title}\u300D\u3067\u5224\u65AD\u5F85\u3061: ${question}` });
|
|
986
|
+
return { project: q, decisionId: d.id };
|
|
987
|
+
}
|
|
988
|
+
function updateDecision(p, blockId, decisionId, patch) {
|
|
989
|
+
const b = p.blocks[blockId];
|
|
990
|
+
const d = b?.decisions.find((x) => x.id === decisionId);
|
|
991
|
+
if (!b || !d || d.answer !== void 0) return p;
|
|
992
|
+
const q = touch(p);
|
|
993
|
+
const qd = q.blocks[blockId].decisions.find((x) => x.id === decisionId);
|
|
994
|
+
if (patch.question !== void 0) qd.question = patch.question;
|
|
995
|
+
if (patch.options !== void 0) qd.options = patch.options;
|
|
996
|
+
if (patch.context !== void 0) {
|
|
997
|
+
if (patch.context) qd.context = patch.context;
|
|
998
|
+
else delete qd.context;
|
|
999
|
+
}
|
|
1000
|
+
if (q.blocks[blockId].activity?.state === "needs_decision") q.blocks[blockId].activity = { ...q.blocks[blockId].activity, note: qd.question };
|
|
1001
|
+
return q;
|
|
1002
|
+
}
|
|
1003
|
+
function answerDecision(p, blockId, decisionId, answer, by) {
|
|
1004
|
+
const b = p.blocks[blockId];
|
|
1005
|
+
const d = b?.decisions.find((x) => x.id === decisionId);
|
|
1006
|
+
if (!b || !d) return p;
|
|
1007
|
+
const q = touch(p);
|
|
1008
|
+
const qd = q.blocks[blockId].decisions.find((x) => x.id === decisionId);
|
|
1009
|
+
qd.answer = answer;
|
|
1010
|
+
qd.answeredBy = by;
|
|
1011
|
+
qd.answeredAt = now();
|
|
1012
|
+
if (q.blocks[blockId].activity?.state === "needs_decision") q.blocks[blockId].activity = null;
|
|
1013
|
+
appendLog(q, { actor: by, kind: "answered", blockId, message: `\u300C${b.title}\u300D\u306E\u5224\u65AD: ${d.question} \u2192 ${answer}` });
|
|
1014
|
+
return q;
|
|
1015
|
+
}
|
|
1016
|
+
function candidatesOf(d) {
|
|
1017
|
+
if (d.answer === void 0) return { rejected: [...d.options] };
|
|
1018
|
+
return { chosen: d.answer, rejected: d.options.filter((o) => o !== d.answer) };
|
|
1019
|
+
}
|
|
1020
|
+
function reopenDecision(p, blockId, decisionId, by, note = "") {
|
|
1021
|
+
const b = p.blocks[blockId];
|
|
1022
|
+
const d = b?.decisions.find((x) => x.id === decisionId);
|
|
1023
|
+
if (!b || !d || d.answer === void 0) return p;
|
|
1024
|
+
const q = touch(p);
|
|
1025
|
+
const qd = q.blocks[blockId].decisions.find((x) => x.id === decisionId);
|
|
1026
|
+
qd.history = [...qd.history ?? [], { answer: d.answer, by: d.answeredBy ?? "", at: d.answeredAt ?? now(), note: note || void 0 }];
|
|
1027
|
+
delete qd.answer;
|
|
1028
|
+
delete qd.answeredBy;
|
|
1029
|
+
delete qd.answeredAt;
|
|
1030
|
+
q.blocks[blockId].activity = { actor: by, state: "needs_decision", note: d.question, since: now() };
|
|
1031
|
+
appendLog(q, { actor: by, kind: "asked", blockId, message: `\u300C${b.title}\u300D\u306E\u5224\u65AD\u3092\u3084\u308A\u76F4\u3057: ${d.question} (\u524D\u306E\u7B54\u3048: ${d.answer}${note ? "\u3002\u7406\u7531: " + note : ""})` });
|
|
1032
|
+
return q;
|
|
1033
|
+
}
|
|
1034
|
+
function pendingDecisions(p) {
|
|
1035
|
+
const out2 = [];
|
|
1036
|
+
for (const b of Object.values(p.blocks)) {
|
|
1037
|
+
for (const d of b.decisions) if (d.answer === void 0) out2.push({ block: b, decision: d });
|
|
1038
|
+
}
|
|
1039
|
+
return out2;
|
|
1040
|
+
}
|
|
1041
|
+
function setStatus(p, blockId, status, actor) {
|
|
1042
|
+
const b = p.blocks[blockId];
|
|
1043
|
+
if (!b || b.status === status) return p;
|
|
1044
|
+
const q = updateBlock(p, blockId, { status });
|
|
1045
|
+
if (status === "white") q.blocks[blockId].activity = null;
|
|
1046
|
+
appendLog(q, { actor, kind: "status", blockId, message: `\u300C${b.title}\u300D\u3092 ${status} \u306B` });
|
|
1047
|
+
return q;
|
|
1048
|
+
}
|
|
1049
|
+
function summarize(p) {
|
|
1050
|
+
const blocks = Object.values(p.blocks).filter((b) => b.id !== ROOT_ID && kindOf(b) !== "project");
|
|
1051
|
+
const working = blocks.filter((b) => b.activity?.state === "working").map((b) => ({ block: b, actor: b.activity.actor, note: b.activity.note, since: b.activity.since }));
|
|
1052
|
+
const blocked = blocks.filter((b) => b.activity?.state === "blocked" || b.activity?.state === "waiting_review").map((b) => ({ block: b, actor: b.activity.actor, note: b.activity.note }));
|
|
1053
|
+
const leafBlack = blocks.filter((b) => b.status === "black" && !b.activity && childrenOf(p, b.id).length === 0).sort((a, b) => Number(missingRequiredInputs(p, a.id).length > 0) - Number(missingRequiredInputs(p, b.id).length > 0));
|
|
1054
|
+
return {
|
|
1055
|
+
total: blocks.length,
|
|
1056
|
+
white: blocks.filter((b) => b.status === "white").length,
|
|
1057
|
+
gray: blocks.filter((b) => b.status === "gray").length,
|
|
1058
|
+
black: blocks.filter((b) => b.status === "black").length,
|
|
1059
|
+
working,
|
|
1060
|
+
blocked,
|
|
1061
|
+
decisions: pendingDecisions(p),
|
|
1062
|
+
next: leafBlack,
|
|
1063
|
+
overdue: blocks.filter((b) => isOverdue(b))
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function findBlock(p, ref) {
|
|
1067
|
+
if (p.blocks[ref] && ref !== ROOT_ID) return { block: p.blocks[ref], candidates: [] };
|
|
1068
|
+
const blocks = Object.values(p.blocks).filter((b) => b.id !== ROOT_ID);
|
|
1069
|
+
const byKey = blocks.find((b) => b.key && b.key.toLowerCase() === ref.toLowerCase());
|
|
1070
|
+
if (byKey) return { block: byKey, candidates: [] };
|
|
1071
|
+
const exact = blocks.filter((b) => b.title === ref);
|
|
1072
|
+
if (exact.length === 1) return { block: exact[0], candidates: [] };
|
|
1073
|
+
const partial = blocks.filter((b) => b.title.includes(ref));
|
|
1074
|
+
if (partial.length === 1) return { block: partial[0], candidates: [] };
|
|
1075
|
+
return { block: null, candidates: exact.length > 0 ? exact : partial };
|
|
1076
|
+
}
|
|
1077
|
+
function splitBlock(p, parentId, spec, actor) {
|
|
1078
|
+
const errors = [];
|
|
1079
|
+
const parent = p.blocks[parentId];
|
|
1080
|
+
if (!parent) return { project: p, errors: ["\u89AA\u30D6\u30ED\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093"] };
|
|
1081
|
+
let q = clone(p);
|
|
1082
|
+
const made = {};
|
|
1083
|
+
for (const b of spec.blocks) {
|
|
1084
|
+
const r = addBlock(q, { parentId, title: b.title, outputName: b.outputs?.[0] });
|
|
1085
|
+
q = r.project;
|
|
1086
|
+
made[b.title] = r.blockId;
|
|
1087
|
+
if (b.description) q.blocks[r.blockId].description = b.description;
|
|
1088
|
+
if ((b.outputs ?? []).length > 1) errors.push(`\u300C${b.title}\u300D\u306E\u51FA\u529B\u306F 1 \u672C\u306B\u3057\u307E\u3057\u305F (\u4E0B\u306E\u968E\u5C64\u3092\u6301\u305F\u306A\u3044\u7BB1\u306E\u51FA\u529B\u306F 1 \u672C\u3002${b.outputs.slice(1).join(", ")} \u306F\u7701\u7565)`);
|
|
1089
|
+
for (const name of b.inputs ?? []) q = addPort(q, { blockId: r.blockId, direction: "in", name }).project;
|
|
1090
|
+
}
|
|
1091
|
+
const resolve4 = (ref, dir) => {
|
|
1092
|
+
let why = "\u7BB1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093";
|
|
1093
|
+
for (const { title, portName } of refSplits(ref)) {
|
|
1094
|
+
const isParent = title === "parent" || title === parent.title || title === parentId;
|
|
1095
|
+
const blockId = isParent ? parentId : made[title] ?? findBlock(q, title).block?.id;
|
|
1096
|
+
if (!blockId) continue;
|
|
1097
|
+
const want = isParent ? dir === "out" ? "in" : "out" : dir;
|
|
1098
|
+
const port = portsOf(q, blockId, want).find((x) => x.name === portName);
|
|
1099
|
+
if (!port) {
|
|
1100
|
+
why = `\u300C${title}\u300D\u306B${want === "in" ? "\u5165\u529B" : "\u51FA\u529B"}\u300C${portName}\u300D\u304C\u3042\u308A\u307E\u305B\u3093`;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
return { ep: { portId: port.id, side: isParent ? "inner" : "outer" }, why: "" };
|
|
1104
|
+
}
|
|
1105
|
+
return { ep: null, why };
|
|
1106
|
+
};
|
|
1107
|
+
for (const c of spec.connections ?? []) {
|
|
1108
|
+
const from = resolve4(c.from, "out");
|
|
1109
|
+
const to = resolve4(c.to, "in");
|
|
1110
|
+
if (!from.ep || !to.ep) {
|
|
1111
|
+
errors.push(`\u7D50\u7DDA\u3067\u304D\u307E\u305B\u3093: ${c.from} -> ${c.to} (${!from.ep ? from.why : to.why})`);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const r = connect(q, from.ep, to.ep);
|
|
1115
|
+
if (r.error) errors.push(`\u7D50\u7DDA\u3067\u304D\u307E\u305B\u3093: ${c.from} -> ${c.to} (${r.error})`);
|
|
1116
|
+
q = r.project;
|
|
1117
|
+
}
|
|
1118
|
+
if (q.blocks[parentId].status === "black") q.blocks[parentId].status = "gray";
|
|
1119
|
+
q.blocks[parentId].collapsed = false;
|
|
1120
|
+
appendLog(q, { actor, kind: "split", blockId: parentId, message: `\u300C${parent.title}\u300D\u3092 ${spec.blocks.length} \u500B\u306B\u5206\u89E3: ${spec.blocks.map((b) => b.title).join(", ")}` });
|
|
1121
|
+
return { project: q, errors };
|
|
1122
|
+
}
|
|
1123
|
+
function toTemplateNode(p, blockId) {
|
|
1124
|
+
const b = p.blocks[blockId];
|
|
1125
|
+
const kids = childrenOf(p, blockId);
|
|
1126
|
+
const name = (id) => id === blockId ? "parent" : p.blocks[id]?.title ?? "?";
|
|
1127
|
+
const connections = [];
|
|
1128
|
+
const kidIds = new Set(kids.map((k) => k.id));
|
|
1129
|
+
for (const e of Object.values(p.edges)) {
|
|
1130
|
+
if (e.auto) continue;
|
|
1131
|
+
const fp = p.ports[e.from.portId];
|
|
1132
|
+
const tp = p.ports[e.to.portId];
|
|
1133
|
+
if (!fp || !tp) continue;
|
|
1134
|
+
const inScope = (port, side) => side === "inner" ? port.blockId === blockId : kidIds.has(port.blockId);
|
|
1135
|
+
if (!inScope(fp, e.from.side) || !inScope(tp, e.to.side)) continue;
|
|
1136
|
+
connections.push({ from: `${name(fp.blockId)}.${fp.name}`, to: `${name(tp.blockId)}.${tp.name}` });
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
title: b.title,
|
|
1140
|
+
category: b.category,
|
|
1141
|
+
description: b.description,
|
|
1142
|
+
inputs: portsOf(p, blockId, "in").filter((x) => !x.promotedFrom).map((x) => ({ name: x.name, description: x.description, required: x.required })),
|
|
1143
|
+
outputs: portsOf(p, blockId, "out").map((x) => ({ name: x.name, description: x.description })),
|
|
1144
|
+
children: kids.sort((a, c) => a.position.y - c.position.y || a.position.x - c.position.x).map((k) => toTemplateNode(p, k.id)),
|
|
1145
|
+
connections
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function extractTemplate(p, blockId, opts = {}) {
|
|
1149
|
+
const b = p.blocks[blockId];
|
|
1150
|
+
const t = now();
|
|
1151
|
+
return {
|
|
1152
|
+
schema: "boxglow-block",
|
|
1153
|
+
schemaVersion: 1,
|
|
1154
|
+
id: b.template?.id ?? newId(),
|
|
1155
|
+
name: opts.name ?? b.title,
|
|
1156
|
+
version: (b.template?.version ?? 0) + 1,
|
|
1157
|
+
description: opts.description ?? b.description,
|
|
1158
|
+
tags: opts.tags ?? [],
|
|
1159
|
+
createdAt: t,
|
|
1160
|
+
updatedAt: t,
|
|
1161
|
+
root: toTemplateNode(p, blockId)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function instantiateNode(p, parentId, node, origin, position) {
|
|
1165
|
+
const r = addBlock(p, { parentId, title: node.title, outputName: node.outputs[0]?.name, position });
|
|
1166
|
+
let q = r.project;
|
|
1167
|
+
const id = r.blockId;
|
|
1168
|
+
q.blocks[id].description = node.description;
|
|
1169
|
+
q.blocks[id].template = origin;
|
|
1170
|
+
if (node.category) q.blocks[id].category = node.category;
|
|
1171
|
+
if (node.outputs[0]) q = updatePort(q, portsOf(q, id, "out")[0].id, { description: node.outputs[0].description });
|
|
1172
|
+
for (const o of node.outputs.slice(1)) {
|
|
1173
|
+
const a = addPort(q, { blockId: id, direction: "out", name: o.name });
|
|
1174
|
+
q = updatePort(a.project, a.portId, { description: o.description });
|
|
1175
|
+
}
|
|
1176
|
+
for (const i of node.inputs) {
|
|
1177
|
+
const a = addPort(q, { blockId: id, direction: "in", name: i.name });
|
|
1178
|
+
q = updatePort(a.project, a.portId, { description: i.description, required: i.required });
|
|
1179
|
+
}
|
|
1180
|
+
if (node.children.length > 0) {
|
|
1181
|
+
const made = {};
|
|
1182
|
+
let y = childTop(q, id);
|
|
1183
|
+
for (const c of node.children) {
|
|
1184
|
+
const rc = instantiateNode(q, id, c, origin, { x: CHILD_PADDING.left, y });
|
|
1185
|
+
q = rc.project;
|
|
1186
|
+
made[c.title] = rc.blockId;
|
|
1187
|
+
y += 120;
|
|
1188
|
+
}
|
|
1189
|
+
const resolve4 = (ref, dir) => {
|
|
1190
|
+
for (const { title, portName } of refSplits(ref)) {
|
|
1191
|
+
const isParent = title === "parent";
|
|
1192
|
+
const blockId = isParent ? id : made[title];
|
|
1193
|
+
if (!blockId) continue;
|
|
1194
|
+
const want = isParent ? dir === "out" ? "in" : "out" : dir;
|
|
1195
|
+
const port = portsOf(q, blockId, want).find((x) => x.name === portName);
|
|
1196
|
+
if (port) return { portId: port.id, side: isParent ? "inner" : "outer" };
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
};
|
|
1200
|
+
for (const c of node.connections) {
|
|
1201
|
+
const from = resolve4(c.from, "out");
|
|
1202
|
+
const to = resolve4(c.to, "in");
|
|
1203
|
+
if (from && to) q = connect(q, from, to).project;
|
|
1204
|
+
}
|
|
1205
|
+
q.blocks[id].status = "gray";
|
|
1206
|
+
}
|
|
1207
|
+
return { project: q, blockId: id };
|
|
1208
|
+
}
|
|
1209
|
+
function instantiateTemplate(p, parentId, template, actor, position) {
|
|
1210
|
+
const origin = { id: template.id, name: template.name, version: template.version };
|
|
1211
|
+
const r = instantiateNode(p, parentId, template.root, origin, position);
|
|
1212
|
+
const q = r.project;
|
|
1213
|
+
appendLog(q, { actor, kind: "added", blockId: r.blockId, message: `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u300C${template.name}\u300Dv${template.version} \u3092\u633F\u5165: ${template.root.title}` });
|
|
1214
|
+
return { project: normalizePromotions(q), blockId: r.blockId };
|
|
1215
|
+
}
|
|
1216
|
+
function parseTemplate(text2) {
|
|
1217
|
+
let d;
|
|
1218
|
+
try {
|
|
1219
|
+
d = JSON.parse(text2);
|
|
1220
|
+
} catch {
|
|
1221
|
+
throw new Error("JSON \u3068\u3057\u3066\u8AAD\u3081\u307E\u305B\u3093");
|
|
1222
|
+
}
|
|
1223
|
+
const t = d;
|
|
1224
|
+
if (!t || t.schema !== "boxglow-block" || !t.root || typeof t.root.title !== "string") throw new Error("Boxglow \u306E\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8 (boxglow-block) \u3067\u306F\u3042\u308A\u307E\u305B\u3093");
|
|
1225
|
+
const fix = (n) => ({
|
|
1226
|
+
title: n.title ?? "\u7121\u984C",
|
|
1227
|
+
description: n.description ?? "",
|
|
1228
|
+
category: typeof n.category === "string" ? n.category : void 0,
|
|
1229
|
+
inputs: (n.inputs ?? []).map((i) => ({ name: i.name, description: i.description ?? "", required: i.required ?? true })),
|
|
1230
|
+
outputs: (n.outputs ?? []).map((o) => ({ name: o.name, description: o.description ?? "" })),
|
|
1231
|
+
children: (n.children ?? []).map(fix),
|
|
1232
|
+
connections: n.connections ?? []
|
|
1233
|
+
});
|
|
1234
|
+
return {
|
|
1235
|
+
schema: "boxglow-block",
|
|
1236
|
+
schemaVersion: 1,
|
|
1237
|
+
id: t.id ?? newId(),
|
|
1238
|
+
name: t.name ?? t.root.title,
|
|
1239
|
+
version: t.version ?? 1,
|
|
1240
|
+
description: t.description ?? "",
|
|
1241
|
+
tags: t.tags ?? [],
|
|
1242
|
+
createdAt: t.createdAt ?? now(),
|
|
1243
|
+
updatedAt: t.updatedAt ?? now(),
|
|
1244
|
+
root: fix(t.root)
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
function setCategory(p, blockId, key) {
|
|
1248
|
+
const b = p.blocks[blockId];
|
|
1249
|
+
if (!b) return p;
|
|
1250
|
+
if (key && !categoryOf(key)) throw new Error(`\u77E5\u3089\u306A\u3044\u30AB\u30C6\u30B4\u30EA\u3067\u3059: ${key}`);
|
|
1251
|
+
const q = touch(p);
|
|
1252
|
+
if (key) q.blocks[blockId].category = key;
|
|
1253
|
+
else delete q.blocks[blockId].category;
|
|
1254
|
+
return q;
|
|
1255
|
+
}
|
|
1256
|
+
function setSchedule(p, blockId, args, actor) {
|
|
1257
|
+
const b = p.blocks[blockId];
|
|
1258
|
+
if (!b) return p;
|
|
1259
|
+
const q = touch(p);
|
|
1260
|
+
const t = q.blocks[blockId];
|
|
1261
|
+
const put = (k, v) => {
|
|
1262
|
+
if (v === void 0) return;
|
|
1263
|
+
if (v === null || v === "" || typeof v === "number" && Number.isNaN(v)) delete t[k];
|
|
1264
|
+
else t[k] = v;
|
|
1265
|
+
};
|
|
1266
|
+
put("startDate", args.startDate);
|
|
1267
|
+
put("dueDate", args.dueDate);
|
|
1268
|
+
put("estimateHours", args.estimateHours);
|
|
1269
|
+
put("actualHours", args.actualHours);
|
|
1270
|
+
const parts = [];
|
|
1271
|
+
if (args.dueDate !== void 0) parts.push(`\u671F\u65E5 ${args.dueDate ?? "\u306A\u3057"}`);
|
|
1272
|
+
if (args.startDate !== void 0) parts.push(`\u958B\u59CB ${args.startDate ?? "\u306A\u3057"}`);
|
|
1273
|
+
if (args.actualHours !== void 0) parts.push(`\u5B9F\u7E3E ${args.actualHours ?? 0}h`);
|
|
1274
|
+
if (args.estimateHours !== void 0) parts.push(`\u898B\u7A4D ${args.estimateHours ?? 0}h`);
|
|
1275
|
+
if (parts.length > 0) appendLog(q, { actor, kind: "note", blockId, message: `\u300C${b.title}\u300D${parts.join(", ")}` });
|
|
1276
|
+
return q;
|
|
1277
|
+
}
|
|
1278
|
+
function isOverdue(b, today = /* @__PURE__ */ new Date()) {
|
|
1279
|
+
if (!b.dueDate || b.status === "white") return false;
|
|
1280
|
+
const d = /* @__PURE__ */ new Date(b.dueDate + "T23:59:59");
|
|
1281
|
+
return d.getTime() < today.getTime();
|
|
1282
|
+
}
|
|
1283
|
+
function searchBlocks(p, text2, limit = 20) {
|
|
1284
|
+
const q = text2.trim().toLowerCase();
|
|
1285
|
+
if (!q) return [];
|
|
1286
|
+
const blocks = Object.values(p.blocks).filter((b) => b.id !== ROOT_ID);
|
|
1287
|
+
const exactKey = blocks.filter((b) => b.key?.toLowerCase() === q);
|
|
1288
|
+
const catOf = (b) => categoryOf(b.category);
|
|
1289
|
+
const rest = blocks.filter((b) => !exactKey.includes(b) && (b.title.toLowerCase().includes(q) || (b.key ?? "").toLowerCase().includes(q) || b.description.toLowerCase().includes(q) || catOf(b)?.label === text2.trim() || catOf(b)?.en === q));
|
|
1290
|
+
return [...exactKey, ...rest].slice(0, limit);
|
|
1291
|
+
}
|
|
1292
|
+
function sourceOfInput(p, portId, seen = /* @__PURE__ */ new Set()) {
|
|
1293
|
+
if (seen.has(portId)) return null;
|
|
1294
|
+
seen.add(portId);
|
|
1295
|
+
const port = p.ports[portId];
|
|
1296
|
+
if (!port || port.direction !== "in") return null;
|
|
1297
|
+
const e = incomingEdges(p, { portId, side: "outer" })[0];
|
|
1298
|
+
if (!e) return null;
|
|
1299
|
+
const from = p.ports[e.from.portId];
|
|
1300
|
+
if (!from) return null;
|
|
1301
|
+
if (from.direction === "out") return from;
|
|
1302
|
+
if (from.blockId === ROOT_ID) return from;
|
|
1303
|
+
return sourceOfInput(p, from.id, seen) ?? from;
|
|
1304
|
+
}
|
|
1305
|
+
function effectiveDescription(p, portId) {
|
|
1306
|
+
const port = p.ports[portId];
|
|
1307
|
+
if (!port) return "";
|
|
1308
|
+
if (port.direction === "out") return port.description;
|
|
1309
|
+
const src = sourceOfInput(p, portId);
|
|
1310
|
+
return src ? src.description : port.description;
|
|
1311
|
+
}
|
|
1312
|
+
function scopeChain(p, scope) {
|
|
1313
|
+
const out2 = [];
|
|
1314
|
+
let cur = scope;
|
|
1315
|
+
while (cur !== null) {
|
|
1316
|
+
out2.push(cur);
|
|
1317
|
+
cur = p.blocks[cur]?.parentId ?? null;
|
|
1318
|
+
}
|
|
1319
|
+
return out2;
|
|
1320
|
+
}
|
|
1321
|
+
function findOrAddPort(q, blockId, direction, name) {
|
|
1322
|
+
const existing = portsOf(q, blockId, direction).find((x) => x.name === name && !x.promotedFrom);
|
|
1323
|
+
if (existing) return existing;
|
|
1324
|
+
const r = addPort(q, { blockId, direction, name });
|
|
1325
|
+
Object.assign(q, r.project);
|
|
1326
|
+
return q.ports[r.portId] ?? null;
|
|
1327
|
+
}
|
|
1328
|
+
function routeConnect(q, from, to) {
|
|
1329
|
+
const fs = scopeOf(q, from);
|
|
1330
|
+
const ts = scopeOf(q, to);
|
|
1331
|
+
if (fs === null || ts === null) return;
|
|
1332
|
+
const fromPort = q.ports[from.portId];
|
|
1333
|
+
const toPort = q.ports[to.portId];
|
|
1334
|
+
if (!fromPort || !toPort) return;
|
|
1335
|
+
const fChain = scopeChain(q, fs);
|
|
1336
|
+
const tChain = scopeChain(q, ts);
|
|
1337
|
+
const common = fChain.find((s) => tChain.includes(s));
|
|
1338
|
+
if (!common) return;
|
|
1339
|
+
let f = from;
|
|
1340
|
+
for (const sc of fChain.slice(0, fChain.indexOf(common))) {
|
|
1341
|
+
if (sc === ROOT_ID) return;
|
|
1342
|
+
const via = findOrAddPort(q, sc, "out", fromPort.name);
|
|
1343
|
+
if (!via) return;
|
|
1344
|
+
const r = connect(q, f, { portId: via.id, side: "inner" });
|
|
1345
|
+
if (!r.error) Object.assign(q, r.project);
|
|
1346
|
+
f = { portId: via.id, side: "outer" };
|
|
1347
|
+
}
|
|
1348
|
+
for (const sc of tChain.slice(0, tChain.indexOf(common)).reverse()) {
|
|
1349
|
+
if (sc === ROOT_ID) return;
|
|
1350
|
+
const via = findOrAddPort(q, sc, "in", toPort.name);
|
|
1351
|
+
if (!via) return;
|
|
1352
|
+
const r = connect(q, f, { portId: via.id, side: "outer" });
|
|
1353
|
+
if (!r.error) Object.assign(q, r.project);
|
|
1354
|
+
f = { portId: via.id, side: "inner" };
|
|
1355
|
+
}
|
|
1356
|
+
const last = connect(q, f, to);
|
|
1357
|
+
if (!last.error) Object.assign(q, last.project);
|
|
1358
|
+
}
|
|
1359
|
+
function refSplits(ref) {
|
|
1360
|
+
const out2 = [];
|
|
1361
|
+
for (let i = ref.indexOf("."); i >= 0; i = ref.indexOf(".", i + 1)) {
|
|
1362
|
+
const title = ref.slice(0, i);
|
|
1363
|
+
const portName = ref.slice(i + 1);
|
|
1364
|
+
if (title && portName) out2.push({ title, portName });
|
|
1365
|
+
}
|
|
1366
|
+
return out2;
|
|
1367
|
+
}
|
|
1368
|
+
function moveBlockToParent(p, blockId, newParentId, position) {
|
|
1369
|
+
const b = p.blocks[blockId];
|
|
1370
|
+
const np = p.blocks[newParentId];
|
|
1371
|
+
if (!b || !np || blockId === ROOT_ID || newParentId === ROOT_ID || b.parentId === newParentId) return p;
|
|
1372
|
+
if (kindOf(b) === "project") return p;
|
|
1373
|
+
if (newParentId === blockId || descendantsOf(p, blockId).some((d) => d.id === newParentId)) return p;
|
|
1374
|
+
const q = touch(p);
|
|
1375
|
+
const oldParentId = b.parentId;
|
|
1376
|
+
const myPorts = new Set(portsOf(q, blockId).map((x) => x.id));
|
|
1377
|
+
const pending = [];
|
|
1378
|
+
for (const e of Object.values(q.edges)) {
|
|
1379
|
+
const mineIsFrom = myPorts.has(e.from.portId) && e.from.side === "outer";
|
|
1380
|
+
const mineIsTo = myPorts.has(e.to.portId) && e.to.side === "outer";
|
|
1381
|
+
if (!mineIsFrom && !mineIsTo) continue;
|
|
1382
|
+
delete q.edges[e.id];
|
|
1383
|
+
if (e.auto) continue;
|
|
1384
|
+
const myEp = mineIsFrom ? e.from : e.to;
|
|
1385
|
+
const otherEp = mineIsFrom ? e.to : e.from;
|
|
1386
|
+
const otherPort = q.ports[otherEp.portId];
|
|
1387
|
+
if (!otherPort) continue;
|
|
1388
|
+
if (otherEp.side === "outer" && otherPort.blockId === newParentId) continue;
|
|
1389
|
+
if (otherEp.side === "inner" && otherPort.blockId === oldParentId && oldParentId !== null) {
|
|
1390
|
+
if (mineIsTo) {
|
|
1391
|
+
const src = incomingEdges(q, { portId: otherPort.id, side: "outer" })[0];
|
|
1392
|
+
if (src) pending.push({ from: src.from, to: myEp });
|
|
1393
|
+
} else {
|
|
1394
|
+
for (const dst of outgoingEdges(q, { portId: otherPort.id, side: "outer" })) pending.push({ from: myEp, to: dst.to });
|
|
1395
|
+
}
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
pending.push(mineIsFrom ? { from: myEp, to: otherEp } : { from: otherEp, to: myEp });
|
|
1399
|
+
}
|
|
1400
|
+
q.blocks[blockId].parentId = newParentId;
|
|
1401
|
+
q.blocks[blockId].position = { x: Math.max(CHILD_PADDING.left, position.x), y: Math.max(childTop(q, newParentId), position.y) };
|
|
1402
|
+
for (const pr of pending) routeConnect(q, pr.from, pr.to);
|
|
1403
|
+
if (q.blocks[newParentId].status === "black") q.blocks[newParentId].status = "gray";
|
|
1404
|
+
q.blocks[newParentId].collapsed = false;
|
|
1405
|
+
appendLog(q, { actor: "human", kind: "note", blockId, message: `\u300C${b.title}\u300D\u3092\u300C${np.title}\u300D\u306E\u4E2D\u3078\u79FB\u52D5` });
|
|
1406
|
+
return normalizePromotions(q);
|
|
1407
|
+
}
|
|
1408
|
+
function resolveOverlap(p, blockId, sizeOf) {
|
|
1409
|
+
const b = p.blocks[blockId];
|
|
1410
|
+
if (!b || b.parentId === null) return p;
|
|
1411
|
+
const q = clone(p);
|
|
1412
|
+
const GAP = 24;
|
|
1413
|
+
const me = sizeOf(q, blockId);
|
|
1414
|
+
for (let iter = 0; iter < 8; iter++) {
|
|
1415
|
+
const cur = q.blocks[blockId];
|
|
1416
|
+
const sib = childrenOf(q, cur.parentId).find((s) => {
|
|
1417
|
+
if (s.id === blockId) return false;
|
|
1418
|
+
const sz2 = sizeOf(q, s.id);
|
|
1419
|
+
return cur.position.x < s.position.x + sz2.width + GAP && cur.position.x + me.width + GAP > s.position.x && cur.position.y < s.position.y + sz2.height + GAP && cur.position.y + me.height + GAP > s.position.y;
|
|
1420
|
+
});
|
|
1421
|
+
if (!sib) break;
|
|
1422
|
+
const sz = sizeOf(q, sib.id);
|
|
1423
|
+
const moves = [
|
|
1424
|
+
{ x: sib.position.x + sz.width + GAP, y: cur.position.y, d: sib.position.x + sz.width + GAP - cur.position.x },
|
|
1425
|
+
{ x: cur.position.x, y: sib.position.y + sz.height + GAP, d: sib.position.y + sz.height + GAP - cur.position.y },
|
|
1426
|
+
{ x: sib.position.x - me.width - GAP, y: cur.position.y, d: cur.position.x - (sib.position.x - me.width - GAP) },
|
|
1427
|
+
{ x: cur.position.x, y: sib.position.y - me.height - GAP, d: cur.position.y - (sib.position.y - me.height - GAP) }
|
|
1428
|
+
].filter((m) => m.d >= 0);
|
|
1429
|
+
const nested = cur.parentId !== ROOT_ID;
|
|
1430
|
+
const ok = moves.filter((m) => !nested || m.x >= CHILD_PADDING.left && m.y >= childTop(q, cur.parentId));
|
|
1431
|
+
const best = (ok.length > 0 ? ok : moves).sort((a, c) => a.d - c.d)[0];
|
|
1432
|
+
if (!best) break;
|
|
1433
|
+
q.blocks[blockId].position = { x: best.x, y: best.y };
|
|
1434
|
+
}
|
|
1435
|
+
return q;
|
|
1436
|
+
}
|
|
1437
|
+
var inputGroupsOf = (p) => p.inputGroups ?? [];
|
|
1438
|
+
function rootInputsOf(p, groupId) {
|
|
1439
|
+
return portsOf(p, ROOT_ID, "in").filter((x) => groupId === null ? !x.groupId : x.groupId === groupId);
|
|
1440
|
+
}
|
|
1441
|
+
function addInputGroup(p, name) {
|
|
1442
|
+
const q = touch(p);
|
|
1443
|
+
const list2 = q.inputGroups ?? (q.inputGroups = []);
|
|
1444
|
+
const id = newId();
|
|
1445
|
+
const y = list2.length === 0 ? q.terminals.in.y + 160 : Math.max(...list2.map((gp) => gp.position.y)) + 160;
|
|
1446
|
+
list2.push({ id, name, description: "", position: { x: q.terminals.in.x, y } });
|
|
1447
|
+
appendLog(q, { actor: "human", kind: "added", message: `\u5165\u529B\u30B0\u30EB\u30FC\u30D7\u300C${name}\u300D\u3092\u8FFD\u52A0` });
|
|
1448
|
+
return { project: q, groupId: id };
|
|
1449
|
+
}
|
|
1450
|
+
function updateInputGroup(p, groupId, patch) {
|
|
1451
|
+
const q = touch(p);
|
|
1452
|
+
const gp = (q.inputGroups ?? []).find((x) => x.id === groupId);
|
|
1453
|
+
if (!gp) return p;
|
|
1454
|
+
Object.assign(gp, patch);
|
|
1455
|
+
return q;
|
|
1456
|
+
}
|
|
1457
|
+
function setInputGroup(p, portId, groupId) {
|
|
1458
|
+
const port = p.ports[portId];
|
|
1459
|
+
if (!port || port.blockId !== ROOT_ID || port.direction !== "in") return p;
|
|
1460
|
+
const q = touch(p);
|
|
1461
|
+
if (groupId) q.ports[portId].groupId = groupId;
|
|
1462
|
+
else delete q.ports[portId].groupId;
|
|
1463
|
+
return q;
|
|
1464
|
+
}
|
|
1465
|
+
function exportInputGroup(p, groupId) {
|
|
1466
|
+
const gp = (p.inputGroups ?? []).find((x) => x.id === groupId);
|
|
1467
|
+
const inputs = rootInputsOf(p, groupId).map((x) => ({ name: x.name, description: x.description, artifacts: x.artifacts.map((a) => ({ title: a.title, url: a.url })) }));
|
|
1468
|
+
return JSON.stringify({ schema: "boxglow-input-group", name: gp?.name ?? "", description: gp?.description ?? "", inputs }, null, 2);
|
|
1469
|
+
}
|
|
1470
|
+
function importInputGroup(p, text2) {
|
|
1471
|
+
let d;
|
|
1472
|
+
try {
|
|
1473
|
+
d = JSON.parse(text2);
|
|
1474
|
+
} catch {
|
|
1475
|
+
throw new Error("JSON \u3068\u3057\u3066\u8AAD\u3081\u307E\u305B\u3093");
|
|
1476
|
+
}
|
|
1477
|
+
if (d.schema !== "boxglow-input-group" || !Array.isArray(d.inputs)) throw new Error("\u5165\u529B\u30B0\u30EB\u30FC\u30D7\u306E JSON (boxglow-input-group) \u3067\u306F\u3042\u308A\u307E\u305B\u3093");
|
|
1478
|
+
const r = addInputGroup(p, d.name ?? "Inputs");
|
|
1479
|
+
let q = updateInputGroup(r.project, r.groupId, { description: d.description ?? "" });
|
|
1480
|
+
for (const i of d.inputs) {
|
|
1481
|
+
const a = addPort(q, { blockId: ROOT_ID, direction: "in", name: i.name });
|
|
1482
|
+
q = updatePort(a.project, a.portId, { description: i.description ?? "", artifacts: (i.artifacts ?? []).map((x) => createArtifact(x.title, x.url ?? "")) });
|
|
1483
|
+
q = setInputGroup(q, a.portId, r.groupId);
|
|
1484
|
+
}
|
|
1485
|
+
return { project: q, groupId: r.groupId };
|
|
1486
|
+
}
|
|
1487
|
+
function resolveAllOverlaps(p, sizeOf) {
|
|
1488
|
+
let q = p;
|
|
1489
|
+
const parents = new Set(Object.values(p.blocks).filter((b) => b.id !== ROOT_ID).map((b) => b.parentId));
|
|
1490
|
+
for (const parentId of parents) {
|
|
1491
|
+
const kids = childrenOf(q, parentId).sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
|
|
1492
|
+
for (let i = 1; i < kids.length; i++) q = resolveOverlap(q, kids[i].id, sizeOf);
|
|
1493
|
+
}
|
|
1494
|
+
return q;
|
|
1495
|
+
}
|
|
1496
|
+
function issueKeyOf(url) {
|
|
1497
|
+
const u = url.trim();
|
|
1498
|
+
let m = u.match(/\/browse\/([A-Z][A-Z0-9_]+-\d+)/i);
|
|
1499
|
+
if (m) return m[1].toUpperCase();
|
|
1500
|
+
m = u.match(/\/issues\/(\d+)/);
|
|
1501
|
+
if (m) return `#${m[1]}`;
|
|
1502
|
+
m = u.match(/\/pull\/(\d+)/);
|
|
1503
|
+
if (m) return `PR #${m[1]}`;
|
|
1504
|
+
try {
|
|
1505
|
+
return new URL(u).hostname.replace(/^www\./, "");
|
|
1506
|
+
} catch {
|
|
1507
|
+
return u.slice(0, 20);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/model/export.ts
|
|
1512
|
+
var STATUS_LABEL = {
|
|
1513
|
+
black: "New (BlackBox: \u51FA\u529B\u3060\u3051\u6C7A\u3081\u3066\u3042\u308B)",
|
|
1514
|
+
gray: "In Progress (GrayBox: \u5206\u89E3\u4E2D\u30FB\u4F5C\u696D\u4E2D)",
|
|
1515
|
+
white: "Done (WhiteBox: \u51FA\u529B\u306B\u6210\u679C\u7269\u304C\u4ED8\u3044\u3066\u5B8C\u4E86)"
|
|
1516
|
+
};
|
|
1517
|
+
function portLine(p, port) {
|
|
1518
|
+
const parts = [`- **${port.name}**`];
|
|
1519
|
+
const desc = effectiveDescription(p, port.id);
|
|
1520
|
+
if (desc) parts.push(`: ${desc}`);
|
|
1521
|
+
if (port.direction === "in" && !port.required) parts.push(" (\u4EFB\u610F)");
|
|
1522
|
+
if (port.artifacts.length > 0) {
|
|
1523
|
+
parts.push(" / " + port.artifacts.map((a) => a.url ? `[${a.title}](${a.url})` : a.title).join(", "));
|
|
1524
|
+
}
|
|
1525
|
+
const links = port.direction === "in" ? incomingEdges(p, { portId: port.id, side: "outer" }).map((e) => describeEndpointSource(p, e.from.portId)) : outgoingEdges(p, { portId: port.id, side: "outer" }).map((e) => describeEndpointTarget(p, e.to.portId));
|
|
1526
|
+
if (links.length > 0) parts.push(` (${port.direction === "in" ? "\u4F9B\u7D66\u5143" : "\u884C\u304D\u5148"}: ${links.join(", ")})`);
|
|
1527
|
+
return parts.join("");
|
|
1528
|
+
}
|
|
1529
|
+
function describeEndpointSource(p, portId) {
|
|
1530
|
+
const port = p.ports[portId];
|
|
1531
|
+
if (!port) return "?";
|
|
1532
|
+
const block = p.blocks[port.blockId];
|
|
1533
|
+
const name = block?.id === ROOT_ID ? "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u5165\u529B" : block?.title ?? "?";
|
|
1534
|
+
return `${name}.${port.name}`;
|
|
1535
|
+
}
|
|
1536
|
+
function describeEndpointTarget(p, portId) {
|
|
1537
|
+
const port = p.ports[portId];
|
|
1538
|
+
if (!port) return "?";
|
|
1539
|
+
const block = p.blocks[port.blockId];
|
|
1540
|
+
const name = block?.id === ROOT_ID ? "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u51FA\u529B" : block?.title ?? "?";
|
|
1541
|
+
return `${name}.${port.name}`;
|
|
1542
|
+
}
|
|
1543
|
+
function blockSection(p, blockId, level, withChildren) {
|
|
1544
|
+
const b = p.blocks[blockId];
|
|
1545
|
+
if (!b) return "";
|
|
1546
|
+
const h = "#".repeat(Math.min(level, 6));
|
|
1547
|
+
const lines = [];
|
|
1548
|
+
lines.push(`${h} ${b.id === ROOT_ID ? `\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8: ${p.name}` : `${b.key ? b.key + " " : ""}${b.title}`}`);
|
|
1549
|
+
lines.push("");
|
|
1550
|
+
if (b.id !== ROOT_ID) {
|
|
1551
|
+
lines.push(`- \u72B6\u614B: ${STATUS_LABEL[b.status]}`);
|
|
1552
|
+
const cat = categoryOf(b.category);
|
|
1553
|
+
if (cat) lines.push(`- \u30AB\u30C6\u30B4\u30EA: ${cat.label}`);
|
|
1554
|
+
const names = b.assigneeIds.map((id) => p.members.find((m) => m.id === id)?.name).filter(Boolean);
|
|
1555
|
+
if (names.length > 0) lines.push(`- \u62C5\u5F53: ${names.join(", ")}`);
|
|
1556
|
+
const prog = computeProgress(p, b.id);
|
|
1557
|
+
if (childrenOf(p, b.id).length > 0) lines.push(`- \u9032\u6357: \u4E0B\u306E\u968E\u5C64 ${prog.white}/${prog.total} \u304C\u5B8C\u4E86`);
|
|
1558
|
+
}
|
|
1559
|
+
if (b.id !== ROOT_ID && b.decisions.length > 0) {
|
|
1560
|
+
lines.push("- \u5224\u65AD:");
|
|
1561
|
+
for (const d of b.decisions) {
|
|
1562
|
+
const c = candidatesOf(d);
|
|
1563
|
+
lines.push(` - ${d.question}${d.answer !== void 0 ? ` \u2192 ${d.answer}` : " (\u672A\u56DE\u7B54)"}${c.rejected.length ? `\u3002\u6B8B\u3057\u305F\u5019\u88DC: ${c.rejected.join(" / ")}` : ""}${(d.history ?? []).length ? `\u3002\u4EE5\u524D\u306E\u7B54\u3048: ${d.history.map((h2) => h2.answer).join(" \u2192 ")}` : ""}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (b.description) {
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
lines.push(b.description);
|
|
1569
|
+
}
|
|
1570
|
+
const ins = portsOf(p, b.id, "in");
|
|
1571
|
+
const outs = portsOf(p, b.id, "out");
|
|
1572
|
+
lines.push("");
|
|
1573
|
+
lines.push(`**\u5165\u529B** (${ins.length === 0 ? "\u306A\u3057" : `${ins.length} \u4EF6`})`);
|
|
1574
|
+
for (const q of ins) lines.push(portLine(p, q));
|
|
1575
|
+
lines.push("");
|
|
1576
|
+
lines.push(`**\u51FA\u529B** (${outs.length} \u4EF6)`);
|
|
1577
|
+
for (const q of outs) lines.push(portLine(p, q));
|
|
1578
|
+
if (b.artifacts.length > 0) {
|
|
1579
|
+
lines.push("");
|
|
1580
|
+
lines.push("**\u95A2\u9023\u8CC7\u6599**");
|
|
1581
|
+
for (const a of b.artifacts) lines.push(`- ${a.url ? `[${a.title}](${a.url})` : a.title}${a.note ? `: ${a.note}` : ""}`);
|
|
1582
|
+
}
|
|
1583
|
+
lines.push("");
|
|
1584
|
+
if (withChildren) {
|
|
1585
|
+
for (const c of childrenOf(p, b.id)) lines.push(blockSection(p, c.id, level + 1, true));
|
|
1586
|
+
}
|
|
1587
|
+
return lines.join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
function projectToMarkdown(p) {
|
|
1590
|
+
const prog = computeProgress(p, ROOT_ID);
|
|
1591
|
+
const head = [
|
|
1592
|
+
`# ${p.name}`,
|
|
1593
|
+
"",
|
|
1594
|
+
p.description,
|
|
1595
|
+
"",
|
|
1596
|
+
`\u5168\u4F53\u306E\u9032\u6357: ${prog.white}/${prog.total} \u30D6\u30ED\u30C3\u30AF\u304C\u5B8C\u4E86 (BlackBox ${prog.black} / GrayBox ${prog.gray} / WhiteBox ${prog.white})`,
|
|
1597
|
+
"",
|
|
1598
|
+
"\u30D6\u30ED\u30C3\u30AF\u306F\u300C\u5165\u529B\u304B\u3089\u51FA\u529B\u3092\u4F5C\u308B\u30BF\u30B9\u30AF\u300D\u3002BlackBox \u306F\u51FA\u529B\u3060\u3051\u6C7A\u307E\u3063\u3066\u3044\u3066\u4E2D\u8EAB\u304C\u672A\u5B9A\u3001GrayBox \u306F\u5206\u89E3\u4E2D\u30FB\u7740\u624B\u4E2D\u3001WhiteBox \u306F\u5B8C\u4E86\u3002",
|
|
1599
|
+
""
|
|
1600
|
+
].join("\n");
|
|
1601
|
+
return head + blockSection(p, ROOT_ID, 2, true);
|
|
1602
|
+
}
|
|
1603
|
+
function blockToPrompt(p, blockId, ask) {
|
|
1604
|
+
const b = p.blocks[blockId];
|
|
1605
|
+
if (!b) return "";
|
|
1606
|
+
const chain = [...ancestorsOf(p, blockId)].reverse().map((a) => a.id === ROOT_ID ? p.name : a.title);
|
|
1607
|
+
const lines = [];
|
|
1608
|
+
lines.push(`# \u4F9D\u983C: ${b.title}`);
|
|
1609
|
+
lines.push("");
|
|
1610
|
+
lines.push(`\u3053\u306E\u30BF\u30B9\u30AF\u306F\u300C${chain.join(" > ")}\u300D\u306E\u4E2D\u306E 1 \u30D6\u30ED\u30C3\u30AF\u3067\u3059\u3002\u30D6\u30ED\u30C3\u30AF\u306F\u300C\u5165\u529B\u304B\u3089\u51FA\u529B\u3092\u4F5C\u308B\u4F5C\u696D\u300D\u3067\u3001\u51FA\u529B\u306E\u6210\u679C\u7269\u304C\u78BA\u5B9A\u3057\u305F\u3089\u5B8C\u4E86 (WhiteBox) \u306B\u306A\u308A\u307E\u3059\u3002`);
|
|
1611
|
+
lines.push("");
|
|
1612
|
+
lines.push(blockSection(p, blockId, 2, false));
|
|
1613
|
+
const siblings = childrenOf(p, b.parentId ?? ROOT_ID).filter((s) => s.id !== blockId);
|
|
1614
|
+
if (siblings.length > 0) {
|
|
1615
|
+
lines.push("## \u540C\u3058\u968E\u5C64\u306E\u4ED6\u306E\u30D6\u30ED\u30C3\u30AF");
|
|
1616
|
+
lines.push("");
|
|
1617
|
+
for (const s of siblings) lines.push(`- ${s.title} (${STATUS_LABEL[s.status]})`);
|
|
1618
|
+
lines.push("");
|
|
1619
|
+
}
|
|
1620
|
+
const kids = childrenOf(p, blockId);
|
|
1621
|
+
if (kids.length > 0) {
|
|
1622
|
+
lines.push("## \u73FE\u5728\u306E\u4E0B\u306E\u968E\u5C64");
|
|
1623
|
+
lines.push("");
|
|
1624
|
+
for (const k of kids) lines.push(`- ${k.title} (${STATUS_LABEL[k.status]})`);
|
|
1625
|
+
lines.push("");
|
|
1626
|
+
}
|
|
1627
|
+
lines.push("## \u304A\u9858\u3044");
|
|
1628
|
+
lines.push("");
|
|
1629
|
+
if (ask === "plan") {
|
|
1630
|
+
lines.push("\u4E0A\u306E\u5165\u529B\u304B\u3089\u51FA\u529B\u3092\u5F97\u308B\u305F\u3081\u306E\u624B\u9806\u3092\u63D0\u6848\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u524D\u63D0\u304C\u8DB3\u308A\u306A\u3051\u308C\u3070\u3001\u8FFD\u52A0\u3067\u5FC5\u8981\u306A\u5165\u529B\u3092\u6319\u3052\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
1631
|
+
} else if (ask === "decompose") {
|
|
1632
|
+
lines.push("\u3053\u306E\u30D6\u30ED\u30C3\u30AF\u3092\u3001\u5165\u529B\u3068\u51FA\u529B\u3092\u6301\u3064\u5C0F\u3055\u306A\u30D6\u30ED\u30C3\u30AF\u306B\u5206\u89E3\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u6B21\u306E JSON \u5F62\u5F0F\u3067\u7B54\u3048\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
lines.push("```json");
|
|
1635
|
+
lines.push(JSON.stringify({ blocks: [{ title: "\u30D6\u30ED\u30C3\u30AF\u540D", description: "\u3084\u308B\u3053\u3068", inputs: ["\u5165\u529B\u540D"], outputs: ["\u51FA\u529B\u540D"] }], connections: [{ from: "\u30D6\u30ED\u30C3\u30AF\u540D.\u51FA\u529B\u540D", to: "\u30D6\u30ED\u30C3\u30AF\u540D.\u5165\u529B\u540D" }] }, null, 2));
|
|
1636
|
+
lines.push("```");
|
|
1637
|
+
} else {
|
|
1638
|
+
lines.push("\u5165\u529B\u3068\u51FA\u529B\u306E\u5B9A\u7FA9\u306B\u629C\u3051\u3084\u66D6\u6627\u3055\u304C\u306A\u3044\u304B\u6307\u6458\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u51FA\u529B\u304C\u300C\u78BA\u5B9A\u3057\u305F\u300D\u3068\u8A00\u3048\u308B\u6761\u4EF6\u3082\u63D0\u6848\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
1639
|
+
}
|
|
1640
|
+
lines.push("");
|
|
1641
|
+
return lines.join("\n");
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/model/size.ts
|
|
1645
|
+
var BLOCK_W = 240;
|
|
1646
|
+
var HEADER_H = 44;
|
|
1647
|
+
var TITLE_H = 36;
|
|
1648
|
+
var TITLE_LINE_H = 18;
|
|
1649
|
+
var META_H = 24;
|
|
1650
|
+
var ROW_H = 26;
|
|
1651
|
+
var PAD_BOTTOM = 12;
|
|
1652
|
+
var EXPANDED_MIN_W = 320;
|
|
1653
|
+
var EXPANDED_PAD = 96;
|
|
1654
|
+
var EXPANDED_PAD_Y = 104;
|
|
1655
|
+
var TERMINAL_W = 200;
|
|
1656
|
+
var BLOCK_MAX_W = 460;
|
|
1657
|
+
function textWidth(s, fontPx) {
|
|
1658
|
+
let w = 0;
|
|
1659
|
+
for (const ch of s) {
|
|
1660
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1661
|
+
w += code > 11903 ? fontPx * 1.05 : fontPx * 0.6;
|
|
1662
|
+
}
|
|
1663
|
+
return w;
|
|
1664
|
+
}
|
|
1665
|
+
function metaRowWidth(opts) {
|
|
1666
|
+
const chip = (text2) => textWidth(text2, 10) + 14 + 6;
|
|
1667
|
+
let w = 12 + 10;
|
|
1668
|
+
if (!opts.isProject) w += chip("In Progress");
|
|
1669
|
+
if (opts.assigneeText) w += chip(opts.assigneeText);
|
|
1670
|
+
else if (opts.unassigned) w += chip("\u672A\u62C5\u5F53");
|
|
1671
|
+
if (opts.hasPercent) w += chip("100%");
|
|
1672
|
+
if (opts.activityText) w += chip(opts.activityText);
|
|
1673
|
+
if (opts.hasReady) w += chip("Ready");
|
|
1674
|
+
if (opts.hasDue) w += chip("\u671F\u65E5 10/15");
|
|
1675
|
+
w += opts.keyLen * 7 + 8;
|
|
1676
|
+
return w;
|
|
1677
|
+
}
|
|
1678
|
+
function isExpanded(p, blockId) {
|
|
1679
|
+
const b = p.blocks[blockId];
|
|
1680
|
+
return !!b && !b.collapsed && childrenOf(p, blockId).length > 0;
|
|
1681
|
+
}
|
|
1682
|
+
function blockSize(p, blockId) {
|
|
1683
|
+
const b = p.blocks[blockId];
|
|
1684
|
+
const insP = portsOf(p, blockId, "in");
|
|
1685
|
+
const outsP = portsOf(p, blockId, "out");
|
|
1686
|
+
const rows = Math.max(insP.length, outsP.length, 1);
|
|
1687
|
+
const kids = childrenOf(p, blockId);
|
|
1688
|
+
const titleExtras = 12 + 10 + (b?.kind === "project" ? 66 : 0) + (kids.length > 0 ? 30 : 0);
|
|
1689
|
+
const titleW = textWidth(b?.title ?? "", 14) + 4;
|
|
1690
|
+
const wantTitle = titleExtras + titleW;
|
|
1691
|
+
const titleLines = wantTitle > BLOCK_MAX_W ? Math.min(3, Math.ceil(titleW / (BLOCK_MAX_W - titleExtras))) : 1;
|
|
1692
|
+
const actorText = (actor) => {
|
|
1693
|
+
const a = actor.toLowerCase();
|
|
1694
|
+
return a.startsWith("claude") ? "Claude Code" : a.startsWith("codex") ? "Codex" : a.startsWith("human:") ? actor.slice(6) : actor;
|
|
1695
|
+
};
|
|
1696
|
+
const stateText = { working: "\u4F5C\u696D\u4E2D", blocked: "\u8A70\u307E\u308A", needs_decision: "\u5224\u65AD\u5F85\u3061", waiting_review: "\u78BA\u8A8D\u5F85\u3061" };
|
|
1697
|
+
const metaW = metaRowWidth({
|
|
1698
|
+
assigneeText: "",
|
|
1699
|
+
unassigned: false,
|
|
1700
|
+
hasPercent: !!b && b.status !== "white" && (kids.length > 0 || typeof b.progress === "number" && b.progress > 0),
|
|
1701
|
+
activityText: b?.activity ? `${stateText[b.activity.state] ?? ""} (${actorText(b.activity.actor)})` : null,
|
|
1702
|
+
hasDue: !!b?.dueDate && b.status !== "white",
|
|
1703
|
+
keyLen: (b?.key ?? "").length,
|
|
1704
|
+
isProject: b?.kind === "project",
|
|
1705
|
+
hasReady: !!b && b.status === "black" && b.kind !== "project" && insP.length > 0 && missingRequiredInputs(p, b.id).length === 0
|
|
1706
|
+
});
|
|
1707
|
+
const headWidth = Math.min(BLOCK_MAX_W, Math.max(BLOCK_W, wantTitle, metaW));
|
|
1708
|
+
const headerH = TITLE_H + (titleLines - 1) * TITLE_LINE_H + META_H;
|
|
1709
|
+
const inW = Math.max(0, ...insP.map((q) => textWidth(q.name, 11) + (q.name ? 14 : 0) + (q.required ? 0 : textWidth("(\u4EFB\u610F)", 9) + 4)));
|
|
1710
|
+
const outW = Math.max(0, ...outsP.map((q) => textWidth(q.name, 11)));
|
|
1711
|
+
const portsWidth = 12 + inW + 24 + outW + 10;
|
|
1712
|
+
const width = Math.max(BLOCK_W, headWidth, Math.min(BLOCK_MAX_W + 120, portsWidth));
|
|
1713
|
+
const baseH = headerH + rows * ROW_H + PAD_BOTTOM;
|
|
1714
|
+
if (!isExpanded(p, blockId)) return { width, height: baseH, headerH };
|
|
1715
|
+
let right = 0;
|
|
1716
|
+
let bottom = 0;
|
|
1717
|
+
for (const c of kids) {
|
|
1718
|
+
const s = blockSize(p, c.id);
|
|
1719
|
+
right = Math.max(right, c.position.x + s.width);
|
|
1720
|
+
bottom = Math.max(bottom, c.position.y + s.height);
|
|
1721
|
+
}
|
|
1722
|
+
return {
|
|
1723
|
+
width: Math.max(EXPANDED_MIN_W, width, right + EXPANDED_PAD),
|
|
1724
|
+
height: Math.max(baseH + 16, bottom + EXPANDED_PAD_Y + 12 + (headerH - HEADER_H)),
|
|
1725
|
+
headerH
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function terminalHeight(p, which) {
|
|
1729
|
+
const n = portsOf(p, ROOT_ID, which).length;
|
|
1730
|
+
return HEADER_H + Math.max(n, 1) * ROW_H + PAD_BOTTOM;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/model/merge.ts
|
|
1734
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1735
|
+
function pick(path, base, ours, theirs, r) {
|
|
1736
|
+
if (same(ours, theirs)) return ours;
|
|
1737
|
+
if (same(ours, base)) {
|
|
1738
|
+
r.merged++;
|
|
1739
|
+
return theirs;
|
|
1740
|
+
}
|
|
1741
|
+
if (same(theirs, base)) return ours;
|
|
1742
|
+
r.conflicts.push({ path, ours, theirs });
|
|
1743
|
+
return ours;
|
|
1744
|
+
}
|
|
1745
|
+
function mergeObject(path, base, ours, theirs, r) {
|
|
1746
|
+
const out2 = {};
|
|
1747
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs), ...Object.keys(base ?? {})]);
|
|
1748
|
+
for (const k of keys) {
|
|
1749
|
+
const b = base?.[k];
|
|
1750
|
+
const o = ours[k];
|
|
1751
|
+
const t = theirs[k];
|
|
1752
|
+
const inO = k in ours;
|
|
1753
|
+
const inT = k in theirs;
|
|
1754
|
+
const inB = base ? k in base : false;
|
|
1755
|
+
if (!inO && inT) {
|
|
1756
|
+
if (inB && same(t, b)) continue;
|
|
1757
|
+
out2[k] = t;
|
|
1758
|
+
if (!inB) r.merged++;
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
if (inO && !inT) {
|
|
1762
|
+
if (inB && same(o, b)) continue;
|
|
1763
|
+
out2[k] = o;
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
if (!inO && !inT) continue;
|
|
1767
|
+
out2[k] = pick(`${path}.${k}`, b, o, t, r);
|
|
1768
|
+
}
|
|
1769
|
+
return out2;
|
|
1770
|
+
}
|
|
1771
|
+
function mergeMap(path, base, ours, theirs, r) {
|
|
1772
|
+
const out2 = {};
|
|
1773
|
+
const ids = /* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs), ...Object.keys(base ?? {})]);
|
|
1774
|
+
for (const id of ids) {
|
|
1775
|
+
const b = base?.[id];
|
|
1776
|
+
const o = ours[id];
|
|
1777
|
+
const t = theirs[id];
|
|
1778
|
+
if (o && t) {
|
|
1779
|
+
out2[id] = mergeObject(`${path}.${id}`, b, o, t, r);
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (o && !t) {
|
|
1783
|
+
if (b && same(o, b)) continue;
|
|
1784
|
+
out2[id] = o;
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (!o && t) {
|
|
1788
|
+
if (b && same(t, b)) continue;
|
|
1789
|
+
out2[id] = t;
|
|
1790
|
+
r.merged++;
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return out2;
|
|
1795
|
+
}
|
|
1796
|
+
function mergeArrayById(path, base, ours, theirs, r) {
|
|
1797
|
+
const toMap = (xs) => Object.fromEntries((xs ?? []).map((x) => [x.id, x]));
|
|
1798
|
+
const merged = mergeMap(path, base ? toMap(base) : void 0, toMap(ours), toMap(theirs), r);
|
|
1799
|
+
const order = [...ours.map((x) => x.id), ...theirs.map((x) => x.id).filter((id) => !ours.some((x) => x.id === id))];
|
|
1800
|
+
return order.filter((id) => merged[id]).map((id) => merged[id]);
|
|
1801
|
+
}
|
|
1802
|
+
function mergeProjects(base, ours, theirs) {
|
|
1803
|
+
const r = { project: ours, merged: 0, conflicts: [] };
|
|
1804
|
+
const b = base ?? void 0;
|
|
1805
|
+
const o = ours;
|
|
1806
|
+
const t = theirs;
|
|
1807
|
+
const out2 = { ...o };
|
|
1808
|
+
for (const k of ["name", "description", "visibility", "terminals"]) out2[k] = pick(k, b?.[k], o[k], t[k], r);
|
|
1809
|
+
for (const k of ["blocks", "ports", "edges", "agents"]) {
|
|
1810
|
+
out2[k] = mergeMap(k, b?.[k], o[k] ?? {}, t[k] ?? {}, r);
|
|
1811
|
+
}
|
|
1812
|
+
out2.members = mergeArrayById("members", base?.members, ours.members ?? [], theirs.members ?? [], r);
|
|
1813
|
+
out2.inputGroups = mergeArrayById("inputGroups", base?.inputGroups, ours.inputGroups ?? [], theirs.inputGroups ?? [], r);
|
|
1814
|
+
const log = /* @__PURE__ */ new Map();
|
|
1815
|
+
for (const e of [...ours.log ?? [], ...theirs.log ?? []]) log.set(e.id, e);
|
|
1816
|
+
out2.log = [...log.values()].sort((p, q) => p.at < q.at ? -1 : p.at > q.at ? 1 : 0);
|
|
1817
|
+
const blocks = out2.blocks;
|
|
1818
|
+
const used = /* @__PURE__ */ new Map();
|
|
1819
|
+
let nextKey2 = Math.max(ours.nextKey ?? 1, theirs.nextKey ?? 1);
|
|
1820
|
+
for (const id of Object.keys(ours.blocks ?? {})) {
|
|
1821
|
+
const key = blocks[id]?.key;
|
|
1822
|
+
if (key) used.set(key, id);
|
|
1823
|
+
}
|
|
1824
|
+
for (const id of Object.keys(blocks)) {
|
|
1825
|
+
const key = blocks[id].key;
|
|
1826
|
+
if (!key) continue;
|
|
1827
|
+
const owner = used.get(key);
|
|
1828
|
+
if (owner && owner !== id) {
|
|
1829
|
+
blocks[id] = { ...blocks[id], key: `B${nextKey2}` };
|
|
1830
|
+
r.conflicts.push({ path: `blocks.${id}.key`, ours: key, theirs: `B${nextKey2} \u306B\u632F\u308A\u76F4\u3057` });
|
|
1831
|
+
nextKey2++;
|
|
1832
|
+
} else used.set(key, id);
|
|
1833
|
+
}
|
|
1834
|
+
out2.nextKey = nextKey2;
|
|
1835
|
+
r.project = out2;
|
|
1836
|
+
return r;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// cli/main.ts
|
|
1840
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1841
|
+
import { dirname as dirname3, join as join2, resolve as resolve3 } from "node:path";
|
|
1842
|
+
|
|
1843
|
+
// src/model/report.ts
|
|
1844
|
+
var MARK = { black: "[ ]", gray: "[~]", white: "[x]" };
|
|
1845
|
+
function actorLabel(actor) {
|
|
1846
|
+
const a = actor.toLowerCase();
|
|
1847
|
+
if (a.startsWith("claude")) return "CC";
|
|
1848
|
+
if (a.startsWith("codex")) return "CX";
|
|
1849
|
+
if (a.startsWith("gemini")) return "GM";
|
|
1850
|
+
if (a.startsWith("copilot")) return "CP";
|
|
1851
|
+
if (a.startsWith("human:")) return actor.slice(6, 7).toUpperCase() || "H";
|
|
1852
|
+
return actor.slice(0, 2).toUpperCase();
|
|
1853
|
+
}
|
|
1854
|
+
var ACTIVITY_LABEL = { working: "\u4F5C\u696D\u4E2D", blocked: "\u8A70\u307E\u308A", needs_decision: "\u5224\u65AD\u5F85\u3061", waiting_review: "\u78BA\u8A8D\u5F85\u3061" };
|
|
1855
|
+
function ago(iso, now2 = Date.now()) {
|
|
1856
|
+
const sec = Math.max(0, (now2 - new Date(iso).getTime()) / 1e3);
|
|
1857
|
+
if (sec < 60) return "\u4ECA";
|
|
1858
|
+
if (sec < 3600) return `${Math.floor(sec / 60)} \u5206`;
|
|
1859
|
+
if (sec < 86400) return `${Math.floor(sec / 3600)} \u6642\u9593`;
|
|
1860
|
+
return `${Math.floor(sec / 86400)} \u65E5`;
|
|
1861
|
+
}
|
|
1862
|
+
function agoText(iso) {
|
|
1863
|
+
const a = ago(iso);
|
|
1864
|
+
return a === "\u4ECA" ? "\u305F\u3063\u305F\u4ECA" : `${a}\u524D\u304B\u3089`;
|
|
1865
|
+
}
|
|
1866
|
+
function shortTime(iso) {
|
|
1867
|
+
const d = new Date(iso);
|
|
1868
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1869
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1870
|
+
}
|
|
1871
|
+
function treeLines(p, parentId, depth, lines) {
|
|
1872
|
+
const kids = childrenOf(p, parentId).sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
|
|
1873
|
+
for (const b of kids) {
|
|
1874
|
+
const outs = portsOf(p, b.id, "out").map((o) => o.name + (o.artifacts.length > 0 ? "*" : "")).join(", ");
|
|
1875
|
+
const act = b.activity ? ` <- ${actorLabel(b.activity.actor)} ${ACTIVITY_LABEL[b.activity.state]}${b.activity.note ? ": " + b.activity.note : ""}` : "";
|
|
1876
|
+
const cat = categoryOf(b.category);
|
|
1877
|
+
const tag = (cat ? ` [${cat.label}]` : "") + (b.kind === "project" ? ` [\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8${b.repo ? ": " + b.repo : ""}]` : b.template ? ` [\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8: ${b.template.name}]` : "") + (b.issue ? ` [${issueKeyOf(b.issue)}]` : "");
|
|
1878
|
+
const pct = b.status === "white" ? "" : (() => {
|
|
1879
|
+
const v = effectiveProgress(p, b.id);
|
|
1880
|
+
return v > 0 ? ` ${v}%` : "";
|
|
1881
|
+
})();
|
|
1882
|
+
const who = b.assigneeIds.length === 0 && b.kind !== "project" ? " (\u672A\u62C5\u5F53)" : "";
|
|
1883
|
+
const due = b.dueDate && b.status !== "white" ? ` \u671F\u65E5 ${b.dueDate}${isOverdue(b) ? " (\u8D85\u904E)" : ""}` : "";
|
|
1884
|
+
lines.push(`${" ".repeat(depth)}- ${MARK[b.status]}${pct} ${b.key ?? ""} ${b.title}${tag}${who}${due} -> ${outs}${act}`);
|
|
1885
|
+
treeLines(p, b.id, depth + 1, lines);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function statusReport(p) {
|
|
1889
|
+
const s = summarize(p);
|
|
1890
|
+
const lines = [];
|
|
1891
|
+
lines.push(`# ${p.name}`);
|
|
1892
|
+
if (p.description) lines.push("", p.description);
|
|
1893
|
+
const prog = computeProgress(p, ROOT_ID);
|
|
1894
|
+
lines.push("", `\u5B8C\u4E86 ${s.white} / ${s.total} \u30FB \u9032\u6357 ${prog.percent}% (BlackBox ${s.black}, GrayBox ${s.gray}, WhiteBox ${s.white})`);
|
|
1895
|
+
const rootOuts = portsOf(p, ROOT_ID, "out").map((o) => o.name + (o.artifacts.length > 0 ? "*" : ""));
|
|
1896
|
+
const rootIns = portsOf(p, ROOT_ID, "in").filter((o) => !o.groupId).map((o) => o.name + (o.promotedFrom ? " (\u81EA\u52D5)" : ""));
|
|
1897
|
+
lines.push(`\u6700\u7D42\u6210\u679C\u7269: ${rootOuts.join(", ") || "(\u672A\u8A2D\u5B9A)"}`);
|
|
1898
|
+
lines.push(`\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u5165\u529B: ${rootIns.join(", ") || "(\u306A\u3057)"}`);
|
|
1899
|
+
for (const gp of p.inputGroups ?? []) {
|
|
1900
|
+
const names = portsOf(p, ROOT_ID, "in").filter((o) => o.groupId === gp.id).map((o) => o.name + (o.artifacts.length > 0 ? "*" : ""));
|
|
1901
|
+
lines.push(` \u5165\u529B\u30B0\u30EB\u30FC\u30D7\u300C${gp.name}\u300D: ${names.join(", ") || "(\u7A7A)"}`);
|
|
1902
|
+
}
|
|
1903
|
+
if (s.decisions.length > 0) {
|
|
1904
|
+
lines.push("", "## \u5224\u65AD\u5F85\u3061 (\u4EBA\u9593\u306E\u56DE\u7B54\u304C\u5FC5\u8981)");
|
|
1905
|
+
for (const { block, decision } of s.decisions) {
|
|
1906
|
+
lines.push(`- ${block.key ?? ""} \u300C${block.title}\u300D ${decision.question}${decision.options.length > 0 ? " \u9078\u629E\u80A2: " + decision.options.join(" / ") : ""} (decision: ${decision.id})`);
|
|
1907
|
+
if (decision.context) lines.push(` \u5224\u65AD\u6750\u6599: ${decision.context.replace(/\n/g, "\n ")}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (s.overdue.length > 0) {
|
|
1911
|
+
lines.push("", "## \u671F\u65E5\u8D85\u904E");
|
|
1912
|
+
for (const b of s.overdue) lines.push(`- ${b.key ?? ""} \u300C${b.title}\u300D \u671F\u65E5 ${b.dueDate}`);
|
|
1913
|
+
}
|
|
1914
|
+
if (s.working.length > 0) {
|
|
1915
|
+
lines.push("", "## \u4F5C\u696D\u4E2D");
|
|
1916
|
+
for (const w of s.working) lines.push(`- ${actorLabel(w.actor)} ${w.block.key ?? ""} \u300C${w.block.title}\u300D ${w.note} (${agoText(w.since)})`);
|
|
1917
|
+
}
|
|
1918
|
+
if (s.blocked.length > 0) {
|
|
1919
|
+
lines.push("", "## \u8A70\u307E\u308A\u30FB\u78BA\u8A8D\u5F85\u3061");
|
|
1920
|
+
for (const w of s.blocked) lines.push(`- ${actorLabel(w.actor)} ${w.block.key ?? ""} \u300C${w.block.title}\u300D ${w.note}`);
|
|
1921
|
+
}
|
|
1922
|
+
lines.push("", "## \u968E\u5C64 ([ ] New / [~] In Progress / [x] Done\u3002B \u756A\u53F7\u306F\u7BB1\u306E ID\u3002\u51FA\u529B\u540D\u306E * \u306F\u6210\u679C\u7269\u3042\u308A)");
|
|
1923
|
+
treeLines(p, ROOT_ID, 0, lines);
|
|
1924
|
+
if (s.next.length > 0) {
|
|
1925
|
+
lines.push("", "## \u6B21\u306E\u5019\u88DC (\u672A\u7740\u624B\u306E New\u3002\u5FC5\u9808\u306E\u5165\u529B\u304C\u305D\u308D\u3063\u3066\u3044\u308B\u3082\u306E\u304B\u3089)");
|
|
1926
|
+
for (const b of s.next.slice(0, 10)) {
|
|
1927
|
+
const missing = missingRequiredInputs(p, b.id);
|
|
1928
|
+
lines.push(`- ${b.key ?? ""} ${b.title}${missing.length > 0 ? ` (\u5FC5\u9808\u306E\u5165\u529B\u5F85\u3061: ${missing.map((q) => q.name).join(", ")})` : " (\u7740\u624B\u3067\u304D\u308B)"}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
lines.push("");
|
|
1932
|
+
return lines.join("\n");
|
|
1933
|
+
}
|
|
1934
|
+
function blockReport(p, blockId) {
|
|
1935
|
+
const b = p.blocks[blockId];
|
|
1936
|
+
if (!b) return "";
|
|
1937
|
+
const name = (id) => id === ROOT_ID ? "project" : p.blocks[id]?.title ?? "?";
|
|
1938
|
+
const lines = [`# ${b.key ?? ""} ${b.title}`, "", `- \u72B6\u614B: ${b.status}${b.status !== "white" ? ` (\u9032\u6357 ${effectiveProgress(p, blockId)}%)` : ""}`];
|
|
1939
|
+
const cat = categoryOf(b.category);
|
|
1940
|
+
if (cat) lines.push(`- \u30AB\u30C6\u30B4\u30EA: ${cat.label} (${cat.en})`);
|
|
1941
|
+
if (b.issue) lines.push(`- \u8AB2\u984C: ${issueKeyOf(b.issue)} <${b.issue}>`);
|
|
1942
|
+
if (b.startDate || b.dueDate) lines.push(`- \u65E5\u7A0B: ${b.startDate ? "\u958B\u59CB " + b.startDate : ""}${b.startDate && b.dueDate ? " / " : ""}${b.dueDate ? "\u671F\u65E5 " + b.dueDate + (isOverdue(b) ? " (\u8D85\u904E)" : "") : ""}`);
|
|
1943
|
+
if (b.estimateHours !== void 0 || b.actualHours !== void 0) lines.push(`- \u6642\u9593: ${b.estimateHours !== void 0 ? "\u898B\u7A4D " + b.estimateHours + "h" : ""}${b.estimateHours !== void 0 && b.actualHours !== void 0 ? " / " : ""}${b.actualHours !== void 0 ? "\u5B9F\u7E3E " + b.actualHours + "h" : ""}`);
|
|
1944
|
+
if (b.activity) lines.push(`- \u6D3B\u52D5: ${b.activity.actor} ${ACTIVITY_LABEL[b.activity.state]} ${b.activity.note} (${b.activity.since})`);
|
|
1945
|
+
if (b.description) lines.push("", b.description);
|
|
1946
|
+
lines.push("", "## \u5165\u529B");
|
|
1947
|
+
for (const q of portsOf(p, blockId, "in")) {
|
|
1948
|
+
const src = incomingEdges(p, { portId: q.id, side: "outer" }).map((e) => `${name(p.ports[e.from.portId].blockId)}.${p.ports[e.from.portId].name}${e.auto ? " (\u81EA\u52D5)" : ""}`);
|
|
1949
|
+
const desc = effectiveDescription(p, q.id);
|
|
1950
|
+
lines.push(`- ${q.name}${q.required ? "" : " (\u4EFB\u610F)"}${desc ? ": " + desc : ""}${src.length ? " <- " + src.join(", ") : " <- (\u672A\u63A5\u7D9A)"}`);
|
|
1951
|
+
}
|
|
1952
|
+
lines.push("", "## \u51FA\u529B");
|
|
1953
|
+
for (const q of portsOf(p, blockId, "out")) {
|
|
1954
|
+
const dst = outgoingEdges(p, { portId: q.id, side: "outer" }).map((e) => `${name(p.ports[e.to.portId].blockId)}.${p.ports[e.to.portId].name}`);
|
|
1955
|
+
const arts = q.artifacts.map((a) => a.url ? `${a.title} <${a.url}>` : a.title);
|
|
1956
|
+
lines.push(`- ${q.name}${q.description ? ": " + q.description : ""}${dst.length ? " -> " + dst.join(", ") : ""}${arts.length ? " \u6210\u679C\u7269: " + arts.join(", ") : ""}`);
|
|
1957
|
+
}
|
|
1958
|
+
const kids = childrenOf(p, blockId);
|
|
1959
|
+
if (kids.length > 0) {
|
|
1960
|
+
lines.push("", "## \u4E0B\u306E\u968E\u5C64");
|
|
1961
|
+
for (const k of kids) lines.push(`- ${MARK[k.status]} ${k.title} (id: ${k.id})`);
|
|
1962
|
+
}
|
|
1963
|
+
if (b.decisions.length > 0) {
|
|
1964
|
+
lines.push("", "## \u5224\u65AD");
|
|
1965
|
+
for (const d of b.decisions) {
|
|
1966
|
+
const c = candidatesOf(d);
|
|
1967
|
+
lines.push(`- ${d.answer === void 0 ? "[\u672A\u56DE\u7B54]" : "[\u56DE\u7B54\u6E08]"} ${d.question}${d.answer !== void 0 ? " -> " + d.answer : ""} (decision: ${d.id})`);
|
|
1968
|
+
if (d.context) lines.push(` \u5224\u65AD\u6750\u6599: ${d.context.replace(/\n/g, "\n ")}`);
|
|
1969
|
+
if (c.rejected.length > 0) lines.push(` ${d.answer === void 0 ? "\u5019\u88DC" : "\u6B8B\u3057\u305F\u5019\u88DC"}: ${c.rejected.join(" / ")}`);
|
|
1970
|
+
for (const h of d.history ?? []) lines.push(` \u4EE5\u524D\u306E\u7B54\u3048: ${h.answer} (${h.by}${h.note ? "\u3001" + h.note : ""}) ${h.at}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (b.artifacts.length > 0) {
|
|
1974
|
+
lines.push("", "## \u8CC7\u6599");
|
|
1975
|
+
for (const a of b.artifacts) lines.push(`- ${a.url ? `${a.title} <${a.url}>` : a.title}`);
|
|
1976
|
+
}
|
|
1977
|
+
lines.push("");
|
|
1978
|
+
return lines.join("\n");
|
|
1979
|
+
}
|
|
1980
|
+
function logReport(p, n = 20) {
|
|
1981
|
+
const items = p.log.slice(-n).reverse();
|
|
1982
|
+
if (items.length === 0) return "(\u30ED\u30B0\u306F\u307E\u3060\u3042\u308A\u307E\u305B\u3093)\n";
|
|
1983
|
+
return items.map((e) => `- ${shortTime(e.at)} ${actorLabel(e.actor)} ${e.message}`).join("\n") + "\n";
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// cli/git.ts
|
|
1987
|
+
import { execFileSync } from "node:child_process";
|
|
1988
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
1989
|
+
import { dirname as dirname2, relative, resolve as resolve2 } from "node:path";
|
|
1990
|
+
function git(args, cwd) {
|
|
1991
|
+
try {
|
|
1992
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1993
|
+
} catch {
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function remoteToWeb(remote) {
|
|
1998
|
+
let m = remote.match(/^git@([^:]+):(.+?)(\.git)?$/);
|
|
1999
|
+
if (m) return `https://${m[1]}/${m[2]}`;
|
|
2000
|
+
m = remote.match(/^(https?:\/\/[^/]+\/.+?)(\.git)?$/);
|
|
2001
|
+
if (m) return m[1];
|
|
2002
|
+
m = remote.match(/^ssh:\/\/git@([^/]+)\/(.+?)(\.git)?$/);
|
|
2003
|
+
if (m) return `https://${m[1]}/${m[2]}`;
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
function gitRefFor(filePath) {
|
|
2007
|
+
const abs = resolve2(filePath);
|
|
2008
|
+
if (!existsSync2(abs) || !statSync2(abs).isFile()) return null;
|
|
2009
|
+
const root = git(["rev-parse", "--show-toplevel"], dirname2(abs));
|
|
2010
|
+
if (!root) return null;
|
|
2011
|
+
const rel = relative(root, abs).replace(/\\/g, "/");
|
|
2012
|
+
const commit = git(["rev-parse", "HEAD"], root) ?? "";
|
|
2013
|
+
const blob = git(["hash-object", abs], root) ?? "";
|
|
2014
|
+
const repo = git(["config", "--get", "remote.origin.url"], root) ?? "";
|
|
2015
|
+
const web = repo ? remoteToWeb(repo) : null;
|
|
2016
|
+
const url = web && commit ? `${web}/blob/${commit}/${rel}` : "";
|
|
2017
|
+
return { root, repo, path: rel, commit, blob, url };
|
|
2018
|
+
}
|
|
2019
|
+
function currentRef(root, rel) {
|
|
2020
|
+
const commit = git(["rev-parse", "HEAD"], root) ?? "";
|
|
2021
|
+
const blob = git(["hash-object", resolve2(root, rel)], root) ?? "";
|
|
2022
|
+
const repo = git(["config", "--get", "remote.origin.url"], root) ?? "";
|
|
2023
|
+
const web = repo ? remoteToWeb(repo) : null;
|
|
2024
|
+
return { path: rel, commit, blob, url: web && commit ? `${web}/blob/${commit}/${rel}` : "" };
|
|
2025
|
+
}
|
|
2026
|
+
function checkGitRef(ref, cwd) {
|
|
2027
|
+
const root = git(["rev-parse", "--show-toplevel"], cwd);
|
|
2028
|
+
if (!root || !ref.path) return { state: "missing" };
|
|
2029
|
+
if (existsSync2(resolve2(root, ref.path))) return { state: "ok", path: ref.path };
|
|
2030
|
+
if (ref.blob) {
|
|
2031
|
+
const tree = git(["ls-tree", "-r", "HEAD"], root) ?? "";
|
|
2032
|
+
for (const line of tree.split("\n")) {
|
|
2033
|
+
const m = line.match(/^\d+ blob ([0-9a-f]+)\t(.+)$/);
|
|
2034
|
+
if (m && m[1] === ref.blob) return { state: "moved", ...currentRef(root, m[2]) };
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (ref.commit) {
|
|
2038
|
+
const commits = (git(["rev-list", "--reverse", `${ref.commit}..HEAD`], root) ?? "").split("\n").filter(Boolean).slice(0, 2e3);
|
|
2039
|
+
let cur = ref.path;
|
|
2040
|
+
for (const c of commits) {
|
|
2041
|
+
const diff = git(["diff-tree", "-M", "--name-status", "-r", `${c}^`, c], root) ?? "";
|
|
2042
|
+
for (const line of diff.split("\n")) {
|
|
2043
|
+
const m = line.match(/^R\d+\t(.+)\t(.+)$/);
|
|
2044
|
+
if (m && m[1] === cur) cur = m[2];
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
if (cur !== ref.path && existsSync2(resolve2(root, cur))) return { state: "moved", ...currentRef(root, cur) };
|
|
2048
|
+
}
|
|
2049
|
+
return { state: "missing" };
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// src/model/autolayout.ts
|
|
2053
|
+
var GAP_X = 144;
|
|
2054
|
+
var GAP_Y = 96;
|
|
2055
|
+
var MAX_ROW_W = 3e3;
|
|
2056
|
+
function layoutScope(p, scopeId) {
|
|
2057
|
+
let q = structuredClone(p);
|
|
2058
|
+
const kids = childrenOf(q, scopeId);
|
|
2059
|
+
if (kids.length === 0) return q;
|
|
2060
|
+
for (const k of kids) if (childrenOf(q, k.id).length > 0) q = layoutScope(q, k.id);
|
|
2061
|
+
const ids = kids.map((k) => k.id);
|
|
2062
|
+
const succ = /* @__PURE__ */ new Map();
|
|
2063
|
+
for (const id of ids) {
|
|
2064
|
+
const next = /* @__PURE__ */ new Set();
|
|
2065
|
+
for (const port of portsOf(q, id, "out")) {
|
|
2066
|
+
for (const e of outgoingEdges(q, { portId: port.id, side: "outer" })) {
|
|
2067
|
+
if (e.kind !== "sibling") continue;
|
|
2068
|
+
const to = q.ports[e.to.portId]?.blockId;
|
|
2069
|
+
if (to && ids.includes(to)) next.add(to);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
succ.set(id, [...next]);
|
|
2073
|
+
}
|
|
2074
|
+
const layer = /* @__PURE__ */ new Map();
|
|
2075
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2076
|
+
const depth = (id) => {
|
|
2077
|
+
if (layer.has(id)) return layer.get(id);
|
|
2078
|
+
if (visiting.has(id)) return 0;
|
|
2079
|
+
visiting.add(id);
|
|
2080
|
+
let d = 0;
|
|
2081
|
+
for (const prev of ids) if (succ.get(prev).includes(id)) d = Math.max(d, depth(prev) + 1);
|
|
2082
|
+
visiting.delete(id);
|
|
2083
|
+
layer.set(id, d);
|
|
2084
|
+
return d;
|
|
2085
|
+
};
|
|
2086
|
+
for (const id of ids) depth(id);
|
|
2087
|
+
const maxLayer = Math.max(...ids.map((id) => layer.get(id)));
|
|
2088
|
+
const order = /* @__PURE__ */ new Map();
|
|
2089
|
+
const initial = [...kids].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
|
|
2090
|
+
initial.forEach((k, i) => order.set(k.id, i));
|
|
2091
|
+
const pred = /* @__PURE__ */ new Map();
|
|
2092
|
+
for (const id of ids) pred.set(id, ids.filter((prev) => succ.get(prev).includes(id)));
|
|
2093
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
2094
|
+
for (let L = 1; L <= maxLayer; L++) {
|
|
2095
|
+
const members = ids.filter((id) => layer.get(id) === L);
|
|
2096
|
+
const bary = /* @__PURE__ */ new Map();
|
|
2097
|
+
for (const id of members) {
|
|
2098
|
+
const ps = pred.get(id);
|
|
2099
|
+
bary.set(id, ps.length === 0 ? order.get(id) : ps.reduce((acc, q2) => acc + order.get(q2), 0) / ps.length);
|
|
2100
|
+
}
|
|
2101
|
+
members.sort((a, b) => bary.get(a) - bary.get(b) || order.get(a) - order.get(b));
|
|
2102
|
+
members.forEach((id, i) => order.set(id, i));
|
|
2103
|
+
}
|
|
2104
|
+
for (let L = maxLayer - 1; L >= 0; L--) {
|
|
2105
|
+
const members = ids.filter((id) => layer.get(id) === L);
|
|
2106
|
+
const bary = /* @__PURE__ */ new Map();
|
|
2107
|
+
for (const id of members) {
|
|
2108
|
+
const ss = succ.get(id);
|
|
2109
|
+
bary.set(id, ss.length === 0 ? order.get(id) : ss.reduce((acc, q2) => acc + order.get(q2), 0) / ss.length);
|
|
2110
|
+
}
|
|
2111
|
+
members.sort((a, b) => bary.get(a) - bary.get(b) || order.get(a) - order.get(b));
|
|
2112
|
+
members.forEach((id, i) => order.set(id, i));
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const crossing = new Array(Math.max(1, maxLayer)).fill(0);
|
|
2116
|
+
for (const id of ids) {
|
|
2117
|
+
for (const nxt of succ.get(id)) {
|
|
2118
|
+
const a = layer.get(id);
|
|
2119
|
+
const b = layer.get(nxt);
|
|
2120
|
+
for (let g = Math.min(a, b); g < Math.max(a, b); g++) crossing[g]++;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const gapAfter = (L) => Math.max(GAP_X, 72 + 16 * Math.max(0, (crossing[L] ?? 0) - 1) + 16);
|
|
2124
|
+
const startX = scopeId === ROOT_ID ? TERMINAL_W + GAP_X : CHILD_PADDING.left;
|
|
2125
|
+
const startY = scopeId === ROOT_ID ? 40 : childTop(q, scopeId);
|
|
2126
|
+
let x = startX;
|
|
2127
|
+
let rowTop = startY;
|
|
2128
|
+
let maxBottom = startY;
|
|
2129
|
+
let rowBottom = startY;
|
|
2130
|
+
for (let L = 0; L <= maxLayer; L++) {
|
|
2131
|
+
const members = kids.filter((k) => layer.get(k.id) === L).sort((a, b) => order.get(a.id) - order.get(b.id));
|
|
2132
|
+
const colW = Math.max(0, ...members.map((m) => blockSize(q, m.id).width));
|
|
2133
|
+
if (L > 0 && x + colW > startX + MAX_ROW_W) {
|
|
2134
|
+
x = startX;
|
|
2135
|
+
rowTop = rowBottom + GAP_Y * 2;
|
|
2136
|
+
}
|
|
2137
|
+
let y = rowTop;
|
|
2138
|
+
for (const m of members) {
|
|
2139
|
+
const s = blockSize(q, m.id);
|
|
2140
|
+
q.blocks[m.id].position = { x, y };
|
|
2141
|
+
y += s.height + GAP_Y;
|
|
2142
|
+
}
|
|
2143
|
+
rowBottom = Math.max(rowBottom, y - GAP_Y);
|
|
2144
|
+
maxBottom = Math.max(maxBottom, rowBottom);
|
|
2145
|
+
x += colW + gapAfter(L);
|
|
2146
|
+
}
|
|
2147
|
+
if (scopeId === ROOT_ID) {
|
|
2148
|
+
const midY = Math.max(startY, (startY + maxBottom) / 2 - terminalHeight(q, "in") / 2);
|
|
2149
|
+
q.terminals.in = { x: 0, y: midY };
|
|
2150
|
+
let gy = midY + terminalHeight(q, "in") + GAP_Y;
|
|
2151
|
+
for (const gp of q.inputGroups ?? []) {
|
|
2152
|
+
gp.position = { x: 0, y: gy };
|
|
2153
|
+
gy += 44 + Math.max(1, Object.values(q.ports).filter((x2) => x2.blockId === ROOT_ID && x2.groupId === gp.id).length) * 26 + 12 + GAP_Y;
|
|
2154
|
+
}
|
|
2155
|
+
q.terminals.out = { x, y: Math.max(startY, (startY + maxBottom) / 2 - terminalHeight(q, "out") / 2) };
|
|
2156
|
+
}
|
|
2157
|
+
return q;
|
|
2158
|
+
}
|
|
2159
|
+
function layoutAll(p) {
|
|
2160
|
+
return layoutScope(p, ROOT_ID);
|
|
2161
|
+
}
|
|
2162
|
+
function nextFreePosition(p, parentId) {
|
|
2163
|
+
const kids = childrenOf(p, parentId);
|
|
2164
|
+
const startX = parentId === ROOT_ID ? TERMINAL_W + GAP_X : CHILD_PADDING.left;
|
|
2165
|
+
const startY = parentId === ROOT_ID ? 40 : childTop(p, parentId);
|
|
2166
|
+
if (kids.length === 0) return { x: startX, y: startY };
|
|
2167
|
+
let bottom = startY;
|
|
2168
|
+
let leftmost = Infinity;
|
|
2169
|
+
for (const k of kids) {
|
|
2170
|
+
const s = blockSize(p, k.id);
|
|
2171
|
+
bottom = Math.max(bottom, k.position.y + s.height);
|
|
2172
|
+
leftmost = Math.min(leftmost, k.position.x);
|
|
2173
|
+
}
|
|
2174
|
+
return { x: Math.max(startX, leftmost), y: bottom + GAP_Y };
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// cli/main.ts
|
|
2178
|
+
function categoryKeyOf(text2) {
|
|
2179
|
+
const c = findCategory(text2);
|
|
2180
|
+
if (!c) throw new Error(`\u77E5\u3089\u306A\u3044\u30AB\u30C6\u30B4\u30EA\u3067\u3059: ${text2} (\u4F7F\u3048\u308B\u3082\u306E: ${CATEGORIES.map((x) => `${x.en} ${x.label}`).join(", ")})`);
|
|
2181
|
+
return c.key;
|
|
2182
|
+
}
|
|
2183
|
+
function parseArgs(argv2) {
|
|
2184
|
+
const positional = [];
|
|
2185
|
+
const options = {};
|
|
2186
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
2187
|
+
const a = argv2[i];
|
|
2188
|
+
if (a.startsWith("--")) {
|
|
2189
|
+
let key = a.slice(2);
|
|
2190
|
+
let value = true;
|
|
2191
|
+
if (key.includes("=")) {
|
|
2192
|
+
value = key.slice(key.indexOf("=") + 1);
|
|
2193
|
+
key = key.slice(0, key.indexOf("="));
|
|
2194
|
+
} else if (i + 1 < argv2.length && !argv2[i + 1].startsWith("--")) {
|
|
2195
|
+
value = argv2[++i];
|
|
2196
|
+
}
|
|
2197
|
+
const prev = options[key];
|
|
2198
|
+
if (prev === void 0) options[key] = value;
|
|
2199
|
+
else if (Array.isArray(prev)) prev.push(String(value));
|
|
2200
|
+
else options[key] = [String(prev), String(value)];
|
|
2201
|
+
} else {
|
|
2202
|
+
positional.push(a);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return { positional, options };
|
|
2206
|
+
}
|
|
2207
|
+
var str = (v) => typeof v === "string" ? v : Array.isArray(v) ? v[v.length - 1] : void 0;
|
|
2208
|
+
var list = (v) => typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
|
|
2209
|
+
function locateFile(opt2, forInit = false) {
|
|
2210
|
+
if (opt2) return resolve3(opt2);
|
|
2211
|
+
if (process.env.BOXGLOW_FILE) return resolve3(process.env.BOXGLOW_FILE);
|
|
2212
|
+
let dir = process.cwd();
|
|
2213
|
+
for (let i = 0; i < 12; i++) {
|
|
2214
|
+
const candidate = join2(dir, "boxglow.json");
|
|
2215
|
+
if (existsSync3(candidate)) return candidate;
|
|
2216
|
+
const parent = dirname3(dir);
|
|
2217
|
+
if (parent === dir) break;
|
|
2218
|
+
dir = parent;
|
|
2219
|
+
}
|
|
2220
|
+
if (forInit) return join2(process.cwd(), "boxglow.json");
|
|
2221
|
+
throw new Error("boxglow.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (--file \u3067\u6307\u5B9A\u3059\u308B\u304B\u3001`boxglow init` \u3067\u4F5C\u3063\u3066\u304F\u3060\u3055\u3044)");
|
|
2222
|
+
}
|
|
2223
|
+
function actorOf(opt2) {
|
|
2224
|
+
if (opt2) return opt2;
|
|
2225
|
+
if (process.env.BOXGLOW_ACTOR) return process.env.BOXGLOW_ACTOR;
|
|
2226
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE) return "claude-code";
|
|
2227
|
+
if (process.env.CODEX_SANDBOX || process.env.CODEX_HOME) return "codex";
|
|
2228
|
+
return "agent";
|
|
2229
|
+
}
|
|
2230
|
+
function artifactFrom(spec, titleOpt) {
|
|
2231
|
+
const eq = spec.indexOf("=");
|
|
2232
|
+
const isUrlLike = (s) => /^[a-z]+:\/\//i.test(s);
|
|
2233
|
+
let title;
|
|
2234
|
+
let value;
|
|
2235
|
+
if (eq > 0 && !isUrlLike(spec.slice(0, eq))) {
|
|
2236
|
+
title = spec.slice(0, eq);
|
|
2237
|
+
value = spec.slice(eq + 1);
|
|
2238
|
+
} else {
|
|
2239
|
+
value = spec;
|
|
2240
|
+
title = titleOpt ?? spec.split(/[\\/]/).pop() ?? spec;
|
|
2241
|
+
}
|
|
2242
|
+
if (titleOpt) title = titleOpt;
|
|
2243
|
+
if (isUrlLike(value)) return createArtifact(title, value);
|
|
2244
|
+
const ref = gitRefFor(value);
|
|
2245
|
+
if (ref) return createGitArtifact(title, { repo: ref.repo, path: ref.path, commit: ref.commit, blob: ref.blob, url: ref.url });
|
|
2246
|
+
const a = createArtifact(title, "");
|
|
2247
|
+
a.kind = "file";
|
|
2248
|
+
a.path = value;
|
|
2249
|
+
return a;
|
|
2250
|
+
}
|
|
2251
|
+
function load(path) {
|
|
2252
|
+
return fromJSON(readFileSync2(path, "utf8"));
|
|
2253
|
+
}
|
|
2254
|
+
function save(path, p) {
|
|
2255
|
+
writeFileSync2(path, toJSON(resolveAllOverlaps(normalizeCollapsed(p), (q, id) => blockSize(q, id))) + "\n", "utf8");
|
|
2256
|
+
}
|
|
2257
|
+
function mustFind(p, ref, what = "block") {
|
|
2258
|
+
if (!ref) throw new Error(`<${what}> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044 (id \u307E\u305F\u306F\u984C\u540D)`);
|
|
2259
|
+
const r = findBlock(p, ref);
|
|
2260
|
+
if (!r.block) {
|
|
2261
|
+
const hint = r.candidates.length > 0 ? ` \u5019\u88DC: ${r.candidates.map((b) => `${b.title} (id: ${b.id})`).join(", ")}` : "";
|
|
2262
|
+
throw new Error(`\u30D6\u30ED\u30C3\u30AF\u300C${ref}\u300D\u304C${r.candidates.length > 0 ? "1 \u3064\u306B\u6C7A\u307E\u308A\u307E\u305B\u3093\u3002" : "\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002"}${hint}`);
|
|
2263
|
+
}
|
|
2264
|
+
return r.block;
|
|
2265
|
+
}
|
|
2266
|
+
function resolveRef(p, ref) {
|
|
2267
|
+
if (!ref.includes(".")) throw new Error(`\u300C${ref}\u300D\u306F <\u984C\u540D>.<\u30DD\u30FC\u30C8\u540D> \u306E\u5F62\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`);
|
|
2268
|
+
const candidates = [];
|
|
2269
|
+
for (let i = ref.indexOf("."); i >= 0; i = ref.indexOf(".", i + 1)) {
|
|
2270
|
+
const title = ref.slice(0, i);
|
|
2271
|
+
const portName = ref.slice(i + 1);
|
|
2272
|
+
let blockId = null;
|
|
2273
|
+
if (title === "project") blockId = projectBlocks(p)[0]?.id ?? ROOT_ID;
|
|
2274
|
+
else if (title === ROOT_ID) blockId = ROOT_ID;
|
|
2275
|
+
else {
|
|
2276
|
+
const b = findBlock(p, title).block;
|
|
2277
|
+
if (b) blockId = b.id;
|
|
2278
|
+
}
|
|
2279
|
+
if (!blockId || !portName) continue;
|
|
2280
|
+
candidates.push({ blockId, portName, hasPort: portsOf(p, blockId).some((x) => x.name === portName) });
|
|
2281
|
+
}
|
|
2282
|
+
const best = candidates.find((c) => c.hasPort) ?? candidates[0];
|
|
2283
|
+
if (!best) throw new Error(`\u300C${ref}\u300D\u306E\u7BB1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (<\u984C\u540D>.<\u30DD\u30FC\u30C8\u540D> \u306E\u5F62\u3002\u984C\u540D\u306F status \u306B\u51FA\u308B\u984C\u540D\u304B B \u756A\u53F7)`);
|
|
2284
|
+
return { blockId: best.blockId, portName: best.portName };
|
|
2285
|
+
}
|
|
2286
|
+
var sink = (text2) => process.stdout.write(text2);
|
|
2287
|
+
var out = (text2) => sink(text2.endsWith("\n") ? text2 : text2 + "\n");
|
|
2288
|
+
function runCli(argv2) {
|
|
2289
|
+
const prev = sink;
|
|
2290
|
+
let buf = "";
|
|
2291
|
+
sink = (text2) => {
|
|
2292
|
+
buf += text2;
|
|
2293
|
+
};
|
|
2294
|
+
try {
|
|
2295
|
+
main(argv2);
|
|
2296
|
+
return buf;
|
|
2297
|
+
} finally {
|
|
2298
|
+
sink = prev;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
function main(argv2) {
|
|
2302
|
+
const { positional, options } = parseArgs(argv2);
|
|
2303
|
+
const [cmd, ...rest] = positional;
|
|
2304
|
+
if (!cmd || cmd === "help" || options.help) {
|
|
2305
|
+
out(readFileSync2(new URL(import.meta.url)).toString().match(/\/\*\*[\s\S]*?\*\//)?.[0].replace(/^\/\*\*|\*\/$/g, "").replace(/^ \* ?/gm, "") ?? "boxglow");
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const actor = actorOf(str(options.actor));
|
|
2309
|
+
if (cmd === "init") {
|
|
2310
|
+
const path2 = locateFile(str(options.file), true);
|
|
2311
|
+
if (existsSync3(path2) && !options.force) throw new Error(`${path2} \u306F\u65E2\u306B\u3042\u308A\u307E\u3059 (--force \u3067\u4E0A\u66F8\u304D)`);
|
|
2312
|
+
const p2 = createProject(str(options.name) ?? "\u65B0\u3057\u3044\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8");
|
|
2313
|
+
save(path2, p2);
|
|
2314
|
+
out(`\u4F5C\u6210: ${path2}`);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const path = locateFile(str(options.file));
|
|
2318
|
+
let p = load(path);
|
|
2319
|
+
switch (cmd) {
|
|
2320
|
+
case "status": {
|
|
2321
|
+
if (options.json) out(JSON.stringify(p, null, 2));
|
|
2322
|
+
else out(statusReport(p));
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
case "export": {
|
|
2326
|
+
const fmt = str(options.format) ?? "md";
|
|
2327
|
+
const text2 = fmt === "json" ? toJSON(p) + "\n" : projectToMarkdown(p);
|
|
2328
|
+
const target = str(options.out);
|
|
2329
|
+
if (target) {
|
|
2330
|
+
writeFileSync2(target, text2, "utf8");
|
|
2331
|
+
out(`\u66F8\u304D\u51FA\u3057: ${target}`);
|
|
2332
|
+
} else out(text2);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
case "show": {
|
|
2336
|
+
out(blockReport(p, mustFind(p, rest[0]).id));
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
case "setup-agent": {
|
|
2340
|
+
const root = str(options.dir) ? resolve3(str(options.dir)) : dirname3(path);
|
|
2341
|
+
const BEGIN = "<!-- boxglow:begin -->";
|
|
2342
|
+
const END = "<!-- boxglow:end -->";
|
|
2343
|
+
const parts = AGENTS_SNIPPET_default.split(/^---$/m);
|
|
2344
|
+
const body = (parts.length >= 3 ? parts.slice(1, -1).join("---") : AGENTS_SNIPPET_default).trim();
|
|
2345
|
+
const block = `${BEGIN}
|
|
2346
|
+
${body}
|
|
2347
|
+
${END}
|
|
2348
|
+
`;
|
|
2349
|
+
const done = [];
|
|
2350
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
2351
|
+
const f = join2(root, name);
|
|
2352
|
+
const cur = existsSync3(f) ? readFileSync2(f, "utf8") : "";
|
|
2353
|
+
let next;
|
|
2354
|
+
if (cur.includes(BEGIN) && cur.includes(END)) next = cur.slice(0, cur.indexOf(BEGIN)) + block + cur.slice(cur.indexOf(END) + END.length + 1);
|
|
2355
|
+
else next = (cur ? cur.replace(/\s*$/, "\n\n") : "") + block;
|
|
2356
|
+
if (next !== cur) {
|
|
2357
|
+
writeFileSync2(f, next, "utf8");
|
|
2358
|
+
done.push(name);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
const skillDir = join2(root, ".claude", "skills", "boxglow");
|
|
2362
|
+
mkdirSync(skillDir, { recursive: true });
|
|
2363
|
+
writeFileSync2(join2(skillDir, "SKILL.md"), SKILL_default, "utf8");
|
|
2364
|
+
done.push(".claude/skills/boxglow/SKILL.md");
|
|
2365
|
+
const settingsPath = join2(root, ".claude", "settings.json");
|
|
2366
|
+
let settings = {};
|
|
2367
|
+
if (existsSync3(settingsPath)) {
|
|
2368
|
+
try {
|
|
2369
|
+
settings = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
2370
|
+
} catch {
|
|
2371
|
+
settings = {};
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const hooks = settings.hooks ?? {};
|
|
2375
|
+
const start = hooks.SessionStart ?? [];
|
|
2376
|
+
const cmd2 = "npx boxglow status";
|
|
2377
|
+
if (!start.some((h) => (h.hooks ?? []).some((x) => x.command === cmd2))) {
|
|
2378
|
+
start.push({ hooks: [{ type: "command", command: cmd2 }] });
|
|
2379
|
+
hooks.SessionStart = start;
|
|
2380
|
+
settings.hooks = hooks;
|
|
2381
|
+
writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
2382
|
+
done.push(".claude/settings.json (SessionStart: npx boxglow status)");
|
|
2383
|
+
}
|
|
2384
|
+
const mcpPath = join2(root, ".mcp.json");
|
|
2385
|
+
let mcp = {};
|
|
2386
|
+
if (existsSync3(mcpPath)) {
|
|
2387
|
+
try {
|
|
2388
|
+
mcp = JSON.parse(readFileSync2(mcpPath, "utf8"));
|
|
2389
|
+
} catch {
|
|
2390
|
+
mcp = {};
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
mcp.mcpServers = mcp.mcpServers ?? {};
|
|
2394
|
+
if (!mcp.mcpServers.boxglow) {
|
|
2395
|
+
mcp.mcpServers.boxglow = { command: "npx", args: ["boxglow", "mcp"] };
|
|
2396
|
+
writeFileSync2(mcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf8");
|
|
2397
|
+
done.push(".mcp.json (MCP \u30B5\u30FC\u30D0 boxglow)");
|
|
2398
|
+
}
|
|
2399
|
+
out(`\u8A2D\u5B9A\u3057\u307E\u3057\u305F: ${done.join(", ")}
|
|
2400
|
+
AI \u306F\u4F5C\u696D\u306E\u59CB\u307E\u308A\u30FB\u7D42\u308F\u308A\u30FB\u5224\u65AD\u5F85\u3061\u3092 boxglow.json \u306B\u8A18\u9332\u3057\u3001\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6700\u521D\u306B\u8A08\u753B\u3092\u8AAD\u307F\u307E\u3059\u3002\u4EBA\u306F\u753B\u9762 (boxglow.json \u3092\u958B\u304F\u3001\u307E\u305F\u306F npx boxglow serve) \u3067\u898B\u3066\u5224\u65AD\u3057\u3066\u304F\u3060\u3055\u3044`);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
case "project": {
|
|
2404
|
+
const name = rest.join(" ");
|
|
2405
|
+
if (!name) throw new Error("<\u540D\u524D> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2406
|
+
const r = addProjectBlock(p, name);
|
|
2407
|
+
if (str(options.repo)) r.project = updateBlock(r.project, r.blockId, { repo: str(options.repo) });
|
|
2408
|
+
save(path, r.project);
|
|
2409
|
+
out(`\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u3092\u8FFD\u52A0: \u300C${name}\u300D(id: ${r.blockId})\u3002\u5168\u90E8\u3067 ${projectBlocks(r.project).length} \u4EF6`);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
case "export-block": {
|
|
2413
|
+
const b = mustFind(p, rest[0]);
|
|
2414
|
+
const tags = str(options.tags) ? str(options.tags).split(/[,、]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
2415
|
+
const tpl = extractTemplate(p, b.id, { tags });
|
|
2416
|
+
const target = str(options.out) ?? `${b.title.replace(/[\\/:*?"<>|\s]/g, "_")}.boxglow-block.json`;
|
|
2417
|
+
writeFileSync2(target, JSON.stringify(tpl, null, 2) + "\n", "utf8");
|
|
2418
|
+
out(`\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u66F8\u304D\u51FA\u3057: ${target} (${tpl.root.children.length} \u500B\u306E\u5B50)`);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
case "import-block": {
|
|
2422
|
+
if (!rest[0]) throw new Error("<path> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2423
|
+
const tpl = parseTemplate(readFileSync2(rest[0], "utf8"));
|
|
2424
|
+
const parentId = str(options.parent) ? mustFind(p, str(options.parent)).id : defaultTaskParent(p);
|
|
2425
|
+
const r = instantiateTemplate(p, parentId, tpl, actor, nextFreePosition(p, parentId));
|
|
2426
|
+
save(path, r.project);
|
|
2427
|
+
out(`\u633F\u5165: \u300C${tpl.root.title}\u300D(id: ${r.blockId}) \u30C6\u30F3\u30D7\u30EC\u30FC\u30C8 ${tpl.name} v${tpl.version}`);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
case "group": {
|
|
2431
|
+
const name = rest.join(" ");
|
|
2432
|
+
if (!name) throw new Error("<\u540D\u524D> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2433
|
+
const r = addInputGroup(p, name);
|
|
2434
|
+
save(path, r.project);
|
|
2435
|
+
out(`\u5165\u529B\u30B0\u30EB\u30FC\u30D7\u3092\u8FFD\u52A0: \u300C${name}\u300D`);
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
case "group-set": {
|
|
2439
|
+
const port = portsOf(p, ROOT_ID, "in").find((x) => x.name === rest[0]);
|
|
2440
|
+
if (!port) throw new Error(`\u6700\u4E0A\u4F4D\u306E\u5165\u529B\u300C${rest[0] ?? ""}\u300D\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${portsOf(p, ROOT_ID, "in").map((x) => x.name).join(", ")})`);
|
|
2441
|
+
const gname = rest.slice(1).join(" ");
|
|
2442
|
+
const gp = gname === "none" ? null : inputGroupsOf(p).find((g) => g.name === gname);
|
|
2443
|
+
if (gname !== "none" && !gp) throw new Error(`\u30B0\u30EB\u30FC\u30D7\u300C${gname}\u300D\u304C\u3042\u308A\u307E\u305B\u3093 (${inputGroupsOf(p).map((g) => g.name).join(", ")})`);
|
|
2444
|
+
save(path, setInputGroup(p, port.id, gp ? gp.id : null));
|
|
2445
|
+
out(`\u300C${port.name}\u300D\u3092 ${gp ? "\u30B0\u30EB\u30FC\u30D7\u300C" + gp.name + "\u300D" : "Inputs"} \u3078`);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
case "group-export": {
|
|
2449
|
+
const gp = inputGroupsOf(p).find((g) => g.name === rest.join(" "));
|
|
2450
|
+
if (!gp) throw new Error("\u30B0\u30EB\u30FC\u30D7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
|
|
2451
|
+
const target = str(options.out) ?? `${gp.name.replace(/[\\/:*?"<>|\s]/g, "_")}.boxglow-inputs.json`;
|
|
2452
|
+
writeFileSync2(target, exportInputGroup(p, gp.id) + "\n", "utf8");
|
|
2453
|
+
out(`\u66F8\u304D\u51FA\u3057: ${target}`);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
case "group-import": {
|
|
2457
|
+
if (!rest[0]) throw new Error("<path> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2458
|
+
const r = importInputGroup(p, readFileSync2(rest[0], "utf8"));
|
|
2459
|
+
save(path, r.project);
|
|
2460
|
+
out(`\u8AAD\u307F\u8FBC\u307F: \u30B0\u30EB\u30FC\u30D7\u300C${inputGroupsOf(r.project).find((g) => g.id === r.groupId)?.name}\u300D`);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
case "find": {
|
|
2464
|
+
const hits = searchBlocks(p, rest.join(" "), 30);
|
|
2465
|
+
out(hits.length === 0 ? "\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : hits.map((b) => `- ${b.key ?? ""} ${b.title} (id: ${b.id})`).join("\n"));
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
case "layout": {
|
|
2469
|
+
const q = rest[0] ? layoutScope(normalizeCollapsed(p), mustFind(p, rest[0]).id) : layoutAll(normalizeCollapsed(p));
|
|
2470
|
+
save(path, q);
|
|
2471
|
+
out(rest[0] ? `\u6574\u5217: \u300C${mustFind(p, rest[0]).title}\u300D\u306E\u4E2D` : "\u6574\u5217: \u5168\u4F53");
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
case "log": {
|
|
2475
|
+
out(logReport(p, Number(str(options.n) ?? 20)));
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
case "prompt": {
|
|
2479
|
+
const ask = str(options.ask) ?? "plan";
|
|
2480
|
+
out(blockToPrompt(p, mustFind(p, rest[0]).id, ask));
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
case "merge": {
|
|
2484
|
+
const [basePath, oursPath, theirsPath] = rest;
|
|
2485
|
+
if (!basePath || !oursPath || !theirsPath) throw new Error("merge <base> <ours> <theirs> \u306E 3 \u3064\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2486
|
+
const read = (f) => {
|
|
2487
|
+
const text2 = readFileSync2(f, "utf8");
|
|
2488
|
+
return text2.trim() ? fromJSON(text2) : null;
|
|
2489
|
+
};
|
|
2490
|
+
const base = read(basePath);
|
|
2491
|
+
const ours = read(oursPath);
|
|
2492
|
+
const theirs = read(theirsPath);
|
|
2493
|
+
if (!ours || !theirs) throw new Error("ours / theirs \u304C\u8AAD\u3081\u307E\u305B\u3093");
|
|
2494
|
+
const r = mergeProjects(base, ours, theirs);
|
|
2495
|
+
let q = r.project;
|
|
2496
|
+
for (const c of r.conflicts) {
|
|
2497
|
+
q = { ...q, log: [...q.log, { id: `m${Math.random().toString(36).slice(2, 10)}`, at: (/* @__PURE__ */ new Date()).toISOString(), actor: "merge", kind: "note", message: `\u30DE\u30FC\u30B8\u3067\u4E21\u5074\u304C\u5909\u66F4: ${c.path} (\u63A1\u7528: ${JSON.stringify(c.ours)} / \u76F8\u624B: ${JSON.stringify(c.theirs)})` }] };
|
|
2498
|
+
}
|
|
2499
|
+
writeFileSync2(oursPath, toJSON(q) + "\n", "utf8");
|
|
2500
|
+
out(`\u30DE\u30FC\u30B8: \u76F8\u624B\u306E\u5909\u66F4 ${r.merged} \u4EF6\u3092\u53D6\u308A\u8FBC\u307F` + (r.conflicts.length ? `\u3001\u4E21\u5074\u3067\u5909\u66F4\u304C ${r.conflicts.length} \u4EF6 (\u81EA\u5206\u306E\u5024\u3092\u63A1\u7528\u3057\u3001\u76F8\u624B\u306E\u5024\u306F\u30ED\u30B0\u306B\u8A18\u9332)` : ""));
|
|
2501
|
+
if (r.conflicts.length && options.strict) process.exitCode = 1;
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
case "git-setup": {
|
|
2505
|
+
const root = dirname3(path);
|
|
2506
|
+
const attrs = join2(root, ".gitattributes");
|
|
2507
|
+
const line = "boxglow.json merge=boxglow";
|
|
2508
|
+
const cur = existsSync3(attrs) ? readFileSync2(attrs, "utf8") : "";
|
|
2509
|
+
if (!cur.split(/\r?\n/).includes(line)) writeFileSync2(attrs, (cur && !cur.endsWith("\n") ? cur + "\n" : cur) + line + "\n", "utf8");
|
|
2510
|
+
execFileSync2("git", ["config", "merge.boxglow.name", "Boxglow plan merge (box level)"], { cwd: root });
|
|
2511
|
+
execFileSync2("git", ["config", "merge.boxglow.driver", "npx boxglow merge %O %A %B"], { cwd: root });
|
|
2512
|
+
out(`\u767B\u9332\u3057\u307E\u3057\u305F: ${attrs} \u306B\u300C${line}\u300D\u3001git config merge.boxglow.driver = "npx boxglow merge %O %A %B"
|
|
2513
|
+
\u4EE5\u5F8C git merge / pull / rebase \u3067 boxglow.json \u306F\u7BB1\u306E\u5358\u4F4D\u3067\u81EA\u52D5\u30DE\u30FC\u30B8\u3055\u308C\u307E\u3059 (\u30C1\u30FC\u30E0\u5168\u54E1\u304C\u3053\u306E\u30B3\u30DE\u30F3\u30C9\u3092 1 \u56DE\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)`);
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
case "validate": {
|
|
2517
|
+
const problems = [];
|
|
2518
|
+
for (const b of Object.values(p.blocks)) {
|
|
2519
|
+
if (b.id !== ROOT_ID && portsOf(p, b.id, "out").length === 0) problems.push(`\u300C${b.title}\u300D\u306B\u51FA\u529B\u304C\u3042\u308A\u307E\u305B\u3093`);
|
|
2520
|
+
}
|
|
2521
|
+
for (const e of Object.values(p.edges)) {
|
|
2522
|
+
const c = validateConnection(p, e.from, e.to);
|
|
2523
|
+
if (!c.ok) problems.push(`\u7DDA ${e.id}: ${c.reason}`);
|
|
2524
|
+
}
|
|
2525
|
+
const warnings = [];
|
|
2526
|
+
const used = /* @__PURE__ */ new Set();
|
|
2527
|
+
for (const e of Object.values(p.edges)) {
|
|
2528
|
+
used.add(`${e.from.portId}:${e.from.side}`);
|
|
2529
|
+
used.add(`${e.to.portId}:${e.to.side}`);
|
|
2530
|
+
}
|
|
2531
|
+
for (const b of Object.values(p.blocks)) {
|
|
2532
|
+
if (b.id === ROOT_ID) continue;
|
|
2533
|
+
const hasKids = Object.values(p.blocks).some((x) => x.parentId === b.id);
|
|
2534
|
+
for (const q of portsOf(p, b.id)) {
|
|
2535
|
+
if (!used.has(`${q.id}:outer`) && !q.promotedFrom) warnings.push(`${b.key ?? ""} \u300C${b.title}\u300D\u306E${q.direction === "in" ? "\u5165\u529B" : "\u51FA\u529B"}\u300C${q.name}\u300D\u304C\u3069\u3053\u306B\u3082\u3064\u306A\u304C\u3063\u3066\u3044\u307E\u305B\u3093`);
|
|
2536
|
+
if (hasKids && !used.has(`${q.id}:inner`)) warnings.push(`${b.key ?? ""} \u300C${b.title}\u300D\u306E${q.direction === "in" ? "\u5165\u529B" : "\u51FA\u529B"}\u300C${q.name}\u300D\u304C\u4E2D\u306E\u7BB1\u3068\u3064\u306A\u304C\u3063\u3066\u3044\u307E\u305B\u3093`);
|
|
2537
|
+
}
|
|
2538
|
+
if (b.status === "white" && !hasKids && portsOf(p, b.id, "out").every((q) => q.artifacts.length === 0)) warnings.push(`${b.key ?? ""} \u300C${b.title}\u300D\u306F Done \u3067\u3059\u304C\u6210\u679C\u7269\u304C\u3042\u308A\u307E\u305B\u3093 (--artifact \u3067\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044)`);
|
|
2539
|
+
}
|
|
2540
|
+
const lines = [problems.length === 0 ? "\u554F\u984C\u3042\u308A\u307E\u305B\u3093" : problems.map((x) => "- " + x).join("\n")];
|
|
2541
|
+
if (warnings.length > 0) lines.push("", `\u6CE8\u610F (${warnings.length} \u4EF6\u3002\u8A08\u753B\u306E\u7A74):`, ...warnings.map((x) => "- " + x));
|
|
2542
|
+
out(lines.join("\n"));
|
|
2543
|
+
if (problems.length > 0) process.exitCode = 1;
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
case "add": {
|
|
2547
|
+
const title = rest[0];
|
|
2548
|
+
if (!title) throw new Error("<\u984C\u540D> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2549
|
+
const parentId = str(options.parent) ? mustFind(p, str(options.parent)).id : defaultTaskParent(p);
|
|
2550
|
+
const r = addBlock(p, { parentId, title, outputName: str(options.out), actor, position: nextFreePosition(p, parentId) });
|
|
2551
|
+
p = r.project;
|
|
2552
|
+
for (const name of list(options.in)) p = addPort(p, { blockId: r.blockId, direction: "in", name }).project;
|
|
2553
|
+
if (str(options.note)) p = updateBlock(p, r.blockId, { description: str(options.note) });
|
|
2554
|
+
if (str(options.category)) p = setCategory(p, r.blockId, categoryKeyOf(str(options.category)));
|
|
2555
|
+
save(path, p);
|
|
2556
|
+
out(`\u8FFD\u52A0: \u300C${title}\u300D(id: ${r.blockId})`);
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
case "split": {
|
|
2560
|
+
const b = mustFind(p, rest[0]);
|
|
2561
|
+
const specText = str(options["spec-file"]) ? readFileSync2(str(options["spec-file"]), "utf8") : str(options.spec);
|
|
2562
|
+
if (!specText) throw new Error("--spec '<JSON>' \u307E\u305F\u306F --spec-file <path> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2563
|
+
const spec = JSON.parse(specText);
|
|
2564
|
+
const r = splitBlock(p, b.id, spec, actor);
|
|
2565
|
+
save(path, layoutScope(r.project, b.id));
|
|
2566
|
+
out(`\u5206\u89E3: \u300C${b.title}\u300D\u306B ${spec.blocks.length} \u500B\u3092\u8FFD\u52A0` + (r.errors.length ? "\n" + r.errors.map((x) => "- " + x).join("\n") : ""));
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
case "move": {
|
|
2570
|
+
const b = mustFind(p, rest[0]);
|
|
2571
|
+
const target = str(options.parent);
|
|
2572
|
+
if (!target) throw new Error("--parent <block|project> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2573
|
+
const parentId = target === "project" ? defaultTaskParent(p) : mustFind(p, target).id;
|
|
2574
|
+
const q = moveBlockToParent(p, b.id, parentId, nextFreePosition(p, parentId));
|
|
2575
|
+
if (q === p) throw new Error("\u79FB\u305B\u307E\u305B\u3093 (\u81EA\u5206\u306E\u5B50\u5B6B\u306E\u4E2D\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u3001\u540C\u3058\u89AA\u306A\u3069\u306F\u4E0D\u53EF)");
|
|
2576
|
+
save(path, q);
|
|
2577
|
+
out(`\u79FB\u52D5: \u300C${b.title}\u300D\u3092\u300C${q.blocks[parentId].title}\u300D\u306E\u4E2D\u3078 (\u7DDA\u306F\u3064\u306A\u304E\u76F4\u3057\u307E\u3057\u305F)`);
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
case "port": {
|
|
2581
|
+
const target = rest[0] ?? "";
|
|
2582
|
+
const blockId = target === "project" ? projectBlocks(p)[0]?.id ?? ROOT_ID : target === ROOT_ID ? ROOT_ID : mustFind(p, target).id;
|
|
2583
|
+
const added = [];
|
|
2584
|
+
for (const name of list(options.in)) {
|
|
2585
|
+
p = addPort(p, { blockId, direction: "in", name }).project;
|
|
2586
|
+
added.push(`\u5165\u529B ${name}`);
|
|
2587
|
+
}
|
|
2588
|
+
for (const name of list(options.out)) {
|
|
2589
|
+
p = addPort(p, { blockId, direction: "out", name }).project;
|
|
2590
|
+
added.push(`\u51FA\u529B ${name}`);
|
|
2591
|
+
}
|
|
2592
|
+
for (const spec of list(options.rename)) {
|
|
2593
|
+
const eq = spec.indexOf("=");
|
|
2594
|
+
if (eq < 0) throw new Error(`--rename \u306F <\u65E7\u540D>=<\u65B0\u540D> \u306E\u5F62\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${spec}`);
|
|
2595
|
+
const from = spec.slice(0, eq);
|
|
2596
|
+
const to = spec.slice(eq + 1);
|
|
2597
|
+
const port = portsOf(p, blockId).find((x) => x.name === from);
|
|
2598
|
+
if (!port) throw new Error(`\u30DD\u30FC\u30C8\u300C${from}\u300D\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`);
|
|
2599
|
+
p = updatePort(p, port.id, { name: to });
|
|
2600
|
+
added.push(`${from} -> ${to}`);
|
|
2601
|
+
}
|
|
2602
|
+
if (added.length === 0) throw new Error("--in <\u540D\u524D> / --out <\u540D\u524D> / --rename <\u65E7\u540D>=<\u65B0\u540D> \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2603
|
+
save(path, p);
|
|
2604
|
+
out(`\u30DD\u30FC\u30C8: ${p.blocks[blockId]?.title ?? "project"}: ${added.join(", ")}`);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
case "remove": {
|
|
2608
|
+
const b = mustFind(p, rest[0]);
|
|
2609
|
+
if (b.kind === "project") throw new Error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u7BB1\u306F\u6D88\u305B\u307E\u305B\u3093 (\u4E2D\u306E\u7BB1\u3092\u5168\u90E8\u6D88\u3059\u304B\u3001\u30D5\u30A1\u30A4\u30EB\u3054\u3068\u4F5C\u308A\u76F4\u3057\u3066\u304F\u3060\u3055\u3044)");
|
|
2610
|
+
const kids = Object.values(p.blocks).filter((x) => x.parentId === b.id).length;
|
|
2611
|
+
if (kids > 0 && !options.force) throw new Error(`\u300C${b.title}\u300D\u306E\u4E2D\u306B ${kids} \u500B\u306E\u7BB1\u304C\u3042\u308A\u307E\u3059\u3002\u307E\u3068\u3081\u3066\u6D88\u3059\u306A\u3089 --force \u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044`);
|
|
2612
|
+
const q = removeBlock(p, b.id);
|
|
2613
|
+
save(path, q);
|
|
2614
|
+
out(`\u524A\u9664: \u300C${b.title}\u300D` + (kids > 0 ? ` \u3068\u4E2D\u306E ${kids} \u500B\u306E\u7BB1` : "") + " (\u3064\u306A\u304C\u3063\u3066\u3044\u305F\u7DDA\u3082\u5916\u3057\u307E\u3057\u305F)");
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
case "disconnect": {
|
|
2618
|
+
const a = resolveRef(p, rest[0] ?? "");
|
|
2619
|
+
const b = resolveRef(p, rest[1] ?? "");
|
|
2620
|
+
const aPorts = portsOf(p, a.blockId).filter((x) => x.name === a.portName).map((x) => x.id);
|
|
2621
|
+
const bPorts = portsOf(p, b.blockId).filter((x) => x.name === b.portName).map((x) => x.id);
|
|
2622
|
+
const hit = Object.values(p.edges).find((e) => aPorts.includes(e.from.portId) && bPorts.includes(e.to.portId));
|
|
2623
|
+
if (!hit) throw new Error(`\u7DDA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${rest[0]} -> ${rest[1]}`);
|
|
2624
|
+
save(path, disconnect(p, hit.id));
|
|
2625
|
+
out(`\u7DDA\u3092\u5916\u3057\u307E\u3057\u305F: ${rest[0]} -> ${rest[1]}`);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
case "connect": {
|
|
2629
|
+
const a = resolveRef(p, rest[0] ?? "");
|
|
2630
|
+
const b = resolveRef(p, rest[1] ?? "");
|
|
2631
|
+
const fromBlock = p.blocks[a.blockId];
|
|
2632
|
+
const toBlock = p.blocks[b.blockId];
|
|
2633
|
+
const fromIsParent = toBlock.parentId === a.blockId;
|
|
2634
|
+
const toIsParent = fromBlock.parentId === b.blockId;
|
|
2635
|
+
const fromPort = portsOf(p, a.blockId, fromIsParent ? "in" : "out").find((x) => x.name === a.portName);
|
|
2636
|
+
const toPort = portsOf(p, b.blockId, toIsParent ? "out" : "in").find((x) => x.name === b.portName);
|
|
2637
|
+
if (!fromPort) throw new Error(`\u300C${rest[0]}\u300D\u306E\u30DD\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`);
|
|
2638
|
+
if (!toPort) throw new Error(`\u300C${rest[1]}\u300D\u306E\u30DD\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`);
|
|
2639
|
+
const from = { portId: fromPort.id, side: fromIsParent ? "inner" : "outer" };
|
|
2640
|
+
const to = { portId: toPort.id, side: toIsParent ? "inner" : "outer" };
|
|
2641
|
+
const r = connect(p, from, to);
|
|
2642
|
+
if (r.error) throw new Error(r.error);
|
|
2643
|
+
save(path, r.project);
|
|
2644
|
+
out(`\u7D50\u7DDA: ${rest[0]} -> ${rest[1]}`);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
case "start": {
|
|
2648
|
+
const b = mustFind(p, rest[0]);
|
|
2649
|
+
save(path, setActivity(p, b.id, actor, "working", str(options.note) ?? ""));
|
|
2650
|
+
out(`\u958B\u59CB: \u300C${b.title}\u300D(${actor})`);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
case "blocked": {
|
|
2654
|
+
const b = mustFind(p, rest[0]);
|
|
2655
|
+
save(path, setActivity(p, b.id, actor, "blocked", str(options.note) ?? ""));
|
|
2656
|
+
out(`\u8A70\u307E\u308A: \u300C${b.title}\u300D`);
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
case "review": {
|
|
2660
|
+
const b = mustFind(p, rest[0]);
|
|
2661
|
+
save(path, setActivity(p, b.id, actor, "waiting_review", str(options.note) ?? ""));
|
|
2662
|
+
out(`\u78BA\u8A8D\u5F85\u3061: \u300C${b.title}\u300D`);
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
case "leave": {
|
|
2666
|
+
const b = mustFind(p, rest[0]);
|
|
2667
|
+
save(path, clearActivity(p, b.id));
|
|
2668
|
+
out(`\u6D3B\u52D5\u3092\u6D88\u3057\u307E\u3057\u305F: \u300C${b.title}\u300D`);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
case "done": {
|
|
2672
|
+
const b = mustFind(p, rest[0]);
|
|
2673
|
+
const artifacts = list(options.artifact).map((s) => artifactFrom(s));
|
|
2674
|
+
const r = finishBlock(p, b.id, actor, { artifacts, outputName: str(options.output), note: str(options.note) });
|
|
2675
|
+
if (r.error) throw new Error(r.error);
|
|
2676
|
+
save(path, r.project);
|
|
2677
|
+
out(`\u5B8C\u4E86: \u300C${b.title}\u300D${artifacts.length ? " \u6210\u679C\u7269: " + artifacts.map((a) => a.title + (a.kind === "git" ? ` (git ${a.path} @ ${(a.commit ?? "").slice(0, 7)})` : "")).join(", ") : ""}`);
|
|
2678
|
+
if (artifacts.length === 0) out('\u6CE8\u610F: \u6210\u679C\u7269\u304C\u4ED8\u3044\u3066\u3044\u307E\u305B\u3093\u3002--artifact "<\u540D\u524D>=<\u30D1\u30B9\u307E\u305F\u306F URL>" \u3067\u3001\u4EBA\u304C\u5F8C\u304B\u3089\u958B\u3051\u308B\u5177\u4F53\u7684\u306A\u7269\u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044');
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
case "artifact": {
|
|
2682
|
+
const b = mustFind(p, rest[0]);
|
|
2683
|
+
if (!rest[1]) throw new Error("<URL \u307E\u305F\u306F\u30D1\u30B9> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2684
|
+
const a = artifactFrom(rest[1], str(options.title));
|
|
2685
|
+
const outs = portsOf(p, b.id, "out");
|
|
2686
|
+
const target = str(options.output) ? outs.find((o) => o.name === str(options.output)) : outs[0];
|
|
2687
|
+
if (!target) throw new Error("\u51FA\u529B\u30DD\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
|
|
2688
|
+
save(path, updatePort(p, target.id, { artifacts: [...target.artifacts, a] }));
|
|
2689
|
+
out(`\u6210\u679C\u7269\u3092\u4ED8\u3051\u307E\u3057\u305F: \u300C${b.title}\u300D.${target.name} <- ${a.title}${a.kind === "git" ? ` (git ${a.path} @ ${(a.commit ?? "").slice(0, 7)})` : ""}`);
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
case "check": {
|
|
2693
|
+
let changed = 0;
|
|
2694
|
+
const lines = [];
|
|
2695
|
+
for (const port of Object.values(p.ports)) {
|
|
2696
|
+
for (const a of port.artifacts) {
|
|
2697
|
+
if (a.kind !== "git") continue;
|
|
2698
|
+
const owner = p.blocks[port.blockId]?.title ?? "?";
|
|
2699
|
+
if (!a.commit && a.path) {
|
|
2700
|
+
const ref = gitRefFor(resolve3(dirname3(path), a.path));
|
|
2701
|
+
if (ref?.commit) {
|
|
2702
|
+
a.commit = ref.commit;
|
|
2703
|
+
a.blob = ref.blob;
|
|
2704
|
+
if (ref.url) a.url = ref.url;
|
|
2705
|
+
a.state = "ok";
|
|
2706
|
+
a.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2707
|
+
lines.push(`- [\u88DC\u5B8C] ${owner}.${port.name} \u300C${a.title}\u300D ${a.path} @ ${ref.commit.slice(0, 7)}`);
|
|
2708
|
+
changed++;
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
const r = checkGitRef(a, dirname3(path));
|
|
2713
|
+
if (r.state === "moved" && r.path) {
|
|
2714
|
+
lines.push(`- [\u79FB\u52D5] ${owner}.${port.name} \u300C${a.title}\u300D ${a.path} -> ${r.path}`);
|
|
2715
|
+
a.path = r.path;
|
|
2716
|
+
if (r.commit) a.commit = r.commit;
|
|
2717
|
+
if (r.blob) a.blob = r.blob;
|
|
2718
|
+
if (r.url) a.url = r.url;
|
|
2719
|
+
a.state = "moved";
|
|
2720
|
+
changed++;
|
|
2721
|
+
} else if (r.state === "missing") {
|
|
2722
|
+
lines.push(`- [\u898B\u3064\u304B\u308A\u307E\u305B\u3093] ${owner}.${port.name} \u300C${a.title}\u300D ${a.path} (\u30B3\u30DF\u30C3\u30C8 ${(a.commit ?? "").slice(0, 7)} \u304B\u3089\u306F\u53D6\u308A\u51FA\u305B\u307E\u3059: git show ${(a.commit ?? "").slice(0, 7)}:${a.path})`);
|
|
2723
|
+
if (a.state !== "missing") changed++;
|
|
2724
|
+
a.state = "missing";
|
|
2725
|
+
} else {
|
|
2726
|
+
if (a.state !== "ok") changed++;
|
|
2727
|
+
a.state = "ok";
|
|
2728
|
+
}
|
|
2729
|
+
a.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
save(path, p);
|
|
2733
|
+
out(lines.length === 0 ? "Git \u306E\u6210\u679C\u7269\u306F\u3059\u3079\u3066\u898B\u3064\u304B\u308A\u307E\u3057\u305F" : lines.join("\n"));
|
|
2734
|
+
if (changed > 0) out(`${changed} \u4EF6\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F`);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
case "ask": {
|
|
2738
|
+
const b = mustFind(p, rest[0]);
|
|
2739
|
+
const question = rest.slice(1).join(" ");
|
|
2740
|
+
if (!question) throw new Error("<\u8CEA\u554F> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2741
|
+
const opts = str(options.options) ? str(options.options).split("|").map((s) => s.trim()).filter(Boolean) : [];
|
|
2742
|
+
const r = askDecision(p, b.id, actor, question, opts, str(options.context) ?? "");
|
|
2743
|
+
save(path, r.project);
|
|
2744
|
+
out(`\u5224\u65AD\u5F85\u3061: \u300C${b.title}\u300D ${question} (decision: ${r.decisionId})`);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
case "decision": {
|
|
2748
|
+
const b = mustFind(p, rest[0]);
|
|
2749
|
+
const id = str(options.id) ?? [...b.decisions].reverse().find((d) => d.answer === void 0)?.id;
|
|
2750
|
+
if (!id) throw new Error("\u672A\u56DE\u7B54\u306E\u5224\u65AD\u304C\u3042\u308A\u307E\u305B\u3093 (--id \u3067\u6307\u5B9A)");
|
|
2751
|
+
const patch = {};
|
|
2752
|
+
if (str(options.question) !== void 0) patch.question = str(options.question);
|
|
2753
|
+
if (str(options.options) !== void 0) patch.options = str(options.options).split("|").map((s) => s.trim()).filter(Boolean);
|
|
2754
|
+
if (str(options.context) !== void 0) patch.context = str(options.context);
|
|
2755
|
+
const q = updateDecision(p, b.id, id, patch);
|
|
2756
|
+
if (q === p) throw new Error("\u66F8\u304D\u76F4\u305B\u307E\u305B\u3093 (\u56DE\u7B54\u6E08\u307F\u3001\u307E\u305F\u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093)");
|
|
2757
|
+
save(path, q);
|
|
2758
|
+
out(`\u5224\u65AD\u3092\u66F8\u304D\u76F4\u3057\u307E\u3057\u305F: \u300C${b.title}\u300D (decision: ${id})`);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
case "answer": {
|
|
2762
|
+
const b = mustFind(p, rest[0]);
|
|
2763
|
+
const answer = rest.slice(1).join(" ");
|
|
2764
|
+
if (!answer) throw new Error("<\u56DE\u7B54> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
2765
|
+
const id = str(options.id) ?? [...b.decisions].reverse().find((d) => d.answer === void 0)?.id;
|
|
2766
|
+
if (!id) throw new Error("\u672A\u56DE\u7B54\u306E\u5224\u65AD\u304C\u3042\u308A\u307E\u305B\u3093");
|
|
2767
|
+
save(path, answerDecision(p, b.id, id, answer, str(options.by) ?? (actor === "agent" ? "human" : actor)));
|
|
2768
|
+
out(`\u56DE\u7B54: \u300C${b.title}\u300D ${answer}`);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
case "reopen": {
|
|
2772
|
+
const b = mustFind(p, rest[0]);
|
|
2773
|
+
const id = str(options.id) ?? [...b.decisions].reverse().find((d) => d.answer !== void 0)?.id;
|
|
2774
|
+
if (!id) throw new Error("\u3084\u308A\u76F4\u305B\u308B\u5224\u65AD (\u56DE\u7B54\u6E08\u307F) \u304C\u3042\u308A\u307E\u305B\u3093");
|
|
2775
|
+
save(path, reopenDecision(p, b.id, id, actor, str(options.note) ?? ""));
|
|
2776
|
+
out(`\u3084\u308A\u76F4\u3057: \u300C${b.title}\u300D\u306E\u5224\u65AD\u3092\u672A\u56DE\u7B54\u306B\u623B\u3057\u307E\u3057\u305F (\u524D\u306E\u7B54\u3048\u306F\u5C65\u6B74\u306B\u6B8B\u308A\u307E\u3059)`);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
case "set": {
|
|
2780
|
+
const b = mustFind(p, rest[0]);
|
|
2781
|
+
if (str(options.status)) p = setStatus(p, b.id, str(options.status), actor);
|
|
2782
|
+
if (str(options.progress) !== void 0) p = setProgress(p, b.id, str(options.progress) === "auto" ? null : Number(str(options.progress)), actor);
|
|
2783
|
+
const sched = {};
|
|
2784
|
+
const dateOf = (v) => v === void 0 ? void 0 : v === "none" ? null : v;
|
|
2785
|
+
const numOf = (v) => v === void 0 ? void 0 : v === "none" ? null : Number(v);
|
|
2786
|
+
if (str(options.start) !== void 0) sched.startDate = dateOf(str(options.start));
|
|
2787
|
+
if (str(options.due) !== void 0) sched.dueDate = dateOf(str(options.due));
|
|
2788
|
+
if (str(options.estimate) !== void 0) sched.estimateHours = numOf(str(options.estimate));
|
|
2789
|
+
if (str(options.hours) !== void 0) sched.actualHours = numOf(str(options.hours));
|
|
2790
|
+
for (const v of [sched.startDate, sched.dueDate]) if (typeof v === "string" && !/^\d{4}-\d{2}-\d{2}$/.test(v)) throw new Error(`\u65E5\u4ED8\u306F YYYY-MM-DD \u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${v}`);
|
|
2791
|
+
if (Object.keys(sched).length > 0) p = setSchedule(p, b.id, sched, actor);
|
|
2792
|
+
const patch = {};
|
|
2793
|
+
if (str(options.title)) patch.title = str(options.title);
|
|
2794
|
+
if (str(options.note)) patch.description = str(options.note);
|
|
2795
|
+
if (Object.keys(patch).length > 0) p = updateBlock(p, b.id, patch);
|
|
2796
|
+
if (str(options.category) !== void 0) p = setCategory(p, b.id, str(options.category) === "none" ? null : categoryKeyOf(str(options.category)));
|
|
2797
|
+
if (str(options.repo) !== void 0) p = updateBlock(p, b.id, { repo: str(options.repo) === "none" ? void 0 : str(options.repo) });
|
|
2798
|
+
if (str(options.issue) !== void 0) p = updateBlock(p, b.id, { issue: str(options.issue) === "none" ? void 0 : str(options.issue) });
|
|
2799
|
+
save(path, p);
|
|
2800
|
+
out(`\u66F4\u65B0: \u300C${p.blocks[b.id].title}\u300D`);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
default:
|
|
2804
|
+
throw new Error(`\u77E5\u3089\u306A\u3044\u30B3\u30DE\u30F3\u30C9\u3067\u3059: ${cmd} (boxglow help \u3067\u4E00\u89A7)`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
var argv = process.argv.slice(2);
|
|
2808
|
+
if (argv[0] === "mcp") {
|
|
2809
|
+
const { options } = parseArgs(argv.slice(1));
|
|
2810
|
+
if (str(options.file)) process.env.BOXGLOW_FILE = resolve3(str(options.file));
|
|
2811
|
+
startMcp(runCli).catch((e) => {
|
|
2812
|
+
console.error(`[boxglow mcp] ${e instanceof Error ? e.message : String(e)}`);
|
|
2813
|
+
process.exitCode = 1;
|
|
2814
|
+
});
|
|
2815
|
+
} else if (argv[0] === "serve") {
|
|
2816
|
+
const { options } = parseArgs(argv.slice(1));
|
|
2817
|
+
try {
|
|
2818
|
+
const file = locateFile(str(options.file));
|
|
2819
|
+
startServe({ file, port: Number(str(options.port) ?? 4174), dist: fileURLToPath(new URL("../dist/", import.meta.url)), open: !!options.open, log: out });
|
|
2820
|
+
} catch (e) {
|
|
2821
|
+
console.error(`[boxglow serve] ${e instanceof Error ? e.message : String(e)}`);
|
|
2822
|
+
process.exitCode = 1;
|
|
2823
|
+
}
|
|
2824
|
+
} else {
|
|
2825
|
+
try {
|
|
2826
|
+
main(argv);
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
console.error(`[boxglow] ${e instanceof Error ? e.message : String(e)}`);
|
|
2829
|
+
process.exitCode = 1;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
export {
|
|
2833
|
+
runCli
|
|
2834
|
+
};
|