pubuilder 0.7.0 → 0.8.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/CHANGELOG.md +11 -0
- package/dist/api.d.ts +1 -1
- package/dist/cli.js +657 -584
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server/design-ir.d.ts +46 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/skills.d.ts +5 -2
- package/package.json +2 -1
- package/skills/publish/SKILL.md +63 -0
package/dist/cli.js
CHANGED
|
@@ -1,243 +1,243 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync as
|
|
3
|
-
import { join as
|
|
4
|
-
import { fileURLToPath as
|
|
5
|
-
import { createJiti as
|
|
6
|
-
import { createServer as
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { execFileSync as
|
|
9
|
-
import { createInterface as
|
|
10
|
-
const
|
|
11
|
-
function M(
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
for (const [
|
|
17
|
-
if (
|
|
18
|
-
for (const b of
|
|
2
|
+
import { readFileSync as E, existsSync as I, readdirSync as k, mkdirSync as j, writeFileSync as A, rmSync as _, realpathSync as lt } from "node:fs";
|
|
3
|
+
import { join as h, isAbsolute as ut, relative as ft, dirname as pt } from "node:path";
|
|
4
|
+
import { fileURLToPath as Y } from "node:url";
|
|
5
|
+
import { createJiti as ht } from "jiti";
|
|
6
|
+
import { createServer as dt } from "node:http";
|
|
7
|
+
import { homedir as gt, constants as mt } from "node:os";
|
|
8
|
+
import { execFileSync as Q, spawn as q } from "node:child_process";
|
|
9
|
+
import { createInterface as yt } from "node:readline";
|
|
10
|
+
const wt = { width: 1440, height: 900 };
|
|
11
|
+
function M(t, e, n) {
|
|
12
|
+
n != null && t.append(e, String(n));
|
|
13
|
+
}
|
|
14
|
+
function bt(t, e) {
|
|
15
|
+
const n = t.indexOf("#"), o = n >= 0 ? t.slice(n) : "", r = n >= 0 ? t.slice(0, n) : t, s = r.indexOf("?"), c = s >= 0 ? r.slice(0, s) : r, i = new URLSearchParams(s >= 0 ? r.slice(s + 1) : "");
|
|
16
|
+
for (const [f, g] of Object.entries(e.query))
|
|
17
|
+
if (i.delete(f), Array.isArray(g))
|
|
18
|
+
for (const b of g) M(i, f, b);
|
|
19
19
|
else
|
|
20
|
-
M(
|
|
21
|
-
const
|
|
22
|
-
return `${
|
|
23
|
-
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
if (!
|
|
28
|
-
if (!
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
id:
|
|
32
|
-
path:
|
|
33
|
-
title:
|
|
34
|
-
external:
|
|
35
|
-
parentId:
|
|
36
|
-
depth:
|
|
37
|
-
}),
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${
|
|
40
|
-
for (const
|
|
41
|
-
if (!
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
id:
|
|
46
|
-
path:
|
|
47
|
-
title:
|
|
20
|
+
M(i, f, g);
|
|
21
|
+
const a = i.toString();
|
|
22
|
+
return `${c}${a ? `?${a}` : ""}${o}`;
|
|
23
|
+
}
|
|
24
|
+
function C(t) {
|
|
25
|
+
const e = [], n = /* @__PURE__ */ new Set(), o = (r, s, c) => {
|
|
26
|
+
for (const i of r) {
|
|
27
|
+
if (!i.path) throw new Error("[pubuilder] page.path는 필수입니다");
|
|
28
|
+
if (!i.title) throw new Error(`[pubuilder] "${i.path}"의 title이 없습니다`);
|
|
29
|
+
if (n.has(i.path)) throw new Error(`[pubuilder] 중복 path: ${i.path}`);
|
|
30
|
+
if (n.add(i.path), e.push({
|
|
31
|
+
id: i.path,
|
|
32
|
+
path: i.path,
|
|
33
|
+
title: i.title,
|
|
34
|
+
external: i.external ?? /^https?:\/\//.test(i.path),
|
|
35
|
+
parentId: s,
|
|
36
|
+
depth: c
|
|
37
|
+
}), i.variants?.length) {
|
|
38
|
+
if (i.external ?? /^https?:\/\//.test(i.path))
|
|
39
|
+
throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${i.path}`);
|
|
40
|
+
for (const a of i.variants) {
|
|
41
|
+
if (!a.title) throw new Error(`[pubuilder] "${i.path}" variant의 title이 없습니다`);
|
|
42
|
+
const f = bt(i.path, a);
|
|
43
|
+
if (n.has(f)) throw new Error(`[pubuilder] 중복 path: ${f}`);
|
|
44
|
+
n.add(f), e.push({
|
|
45
|
+
id: f,
|
|
46
|
+
path: f,
|
|
47
|
+
title: a.title,
|
|
48
48
|
external: !1,
|
|
49
|
-
parentId:
|
|
50
|
-
depth:
|
|
49
|
+
parentId: i.path,
|
|
50
|
+
depth: c + 1
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
i.children?.length && o(i.children, i.path, c + 1);
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
|
-
if (o(
|
|
58
|
-
return { nodes:
|
|
59
|
-
}
|
|
60
|
-
const
|
|
61
|
-
function
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
function N(
|
|
65
|
-
return JSON.stringify(
|
|
66
|
-
}
|
|
67
|
-
function
|
|
68
|
-
const o =
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
return
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
`${o}${
|
|
57
|
+
if (o(t.pages, null, 0), e.length === 0) throw new Error("[pubuilder] pages가 비어 있습니다");
|
|
58
|
+
return { nodes: e, viewport: t.viewport ?? wt };
|
|
59
|
+
}
|
|
60
|
+
const v = " ";
|
|
61
|
+
function $t(t) {
|
|
62
|
+
return t.split("/").map((e) => /^\[{1,2}\.{0,3}.*\]{1,2}$/.test(e) && e.startsWith("[") ? "1" : e).join("/");
|
|
63
|
+
}
|
|
64
|
+
function N(t) {
|
|
65
|
+
return JSON.stringify(t);
|
|
66
|
+
}
|
|
67
|
+
function tt(t, e, n) {
|
|
68
|
+
const o = v.repeat(e), r = [];
|
|
69
|
+
t.path.includes("[") && r.push(`${o}// TODO: 샘플 경로로 교체 (예: ${$t(t.path)})`);
|
|
70
|
+
const s = n.has(t.path) ? " // TODO: title 확인" : "";
|
|
71
|
+
if (!t.children?.length && !t.variants?.length && !t.external)
|
|
72
|
+
return r.push(`${o}{ path: ${N(t.path)}, title: ${N(t.title)} },${s}`), r;
|
|
73
|
+
if (r.push(`${o}{`), r.push(`${o}${v}path: ${N(t.path)},`), r.push(`${o}${v}title: ${N(t.title)},${s}`), t.external && r.push(`${o}${v}external: true,`), t.variants?.length) {
|
|
74
|
+
r.push(`${o}${v}variants: [`);
|
|
75
|
+
for (const i of t.variants)
|
|
76
|
+
r.push(
|
|
77
|
+
`${o}${v}${v}{ title: ${N(i.title)}, query: ${N(i.query)} },`
|
|
78
78
|
);
|
|
79
|
-
|
|
79
|
+
r.push(`${o}${v}],`);
|
|
80
80
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
for (const
|
|
84
|
-
|
|
81
|
+
if (t.children?.length) {
|
|
82
|
+
r.push(`${o}${v}children: [`);
|
|
83
|
+
for (const i of t.children) r.push(...tt(i, e + 2, n));
|
|
84
|
+
r.push(`${o}${v}],`);
|
|
85
85
|
}
|
|
86
|
-
return
|
|
86
|
+
return r.push(`${o}},`), r;
|
|
87
87
|
}
|
|
88
|
-
function
|
|
89
|
-
const
|
|
88
|
+
function G(t, e = {}) {
|
|
89
|
+
const n = e.newPaths ?? /* @__PURE__ */ new Set(), o = [
|
|
90
90
|
'import { defineIA } from "pubuilder/config"',
|
|
91
91
|
"",
|
|
92
92
|
"export default defineIA({"
|
|
93
93
|
];
|
|
94
|
-
|
|
95
|
-
for (const
|
|
96
|
-
return o.push(`${
|
|
94
|
+
t.viewport && o.push(`${v}viewport: ${N(t.viewport)},`), o.push(`${v}pages: [`);
|
|
95
|
+
for (const r of t.pages) o.push(...tt(r, 2, n));
|
|
96
|
+
return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
|
|
97
97
|
`);
|
|
98
98
|
}
|
|
99
|
-
function
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
function
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
return
|
|
109
|
-
}),
|
|
110
|
-
for (const
|
|
111
|
-
const
|
|
112
|
-
path:
|
|
113
|
-
title:
|
|
99
|
+
function P(t) {
|
|
100
|
+
return t === "/" ? [] : t.replace(/^\//, "").split("/");
|
|
101
|
+
}
|
|
102
|
+
function St(t, e) {
|
|
103
|
+
return t.length >= e.length ? !1 : t.every((n, o) => n === e[o]);
|
|
104
|
+
}
|
|
105
|
+
function vt(t) {
|
|
106
|
+
const e = [...t].sort((r, s) => {
|
|
107
|
+
const c = P(r.routePath).length - P(s.routePath).length;
|
|
108
|
+
return c !== 0 ? c : r.routePath.localeCompare(s.routePath);
|
|
109
|
+
}), n = [], o = [];
|
|
110
|
+
for (const r of e) {
|
|
111
|
+
const s = P(r.routePath), c = {
|
|
112
|
+
path: r.routePath,
|
|
113
|
+
title: r.title ?? s.at(-1) ?? "/"
|
|
114
114
|
};
|
|
115
|
-
let
|
|
116
|
-
for (const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
for (let
|
|
125
|
-
const
|
|
126
|
-
if (/^\[\[\.\.\..+\]\]$/.test(
|
|
127
|
-
return
|
|
128
|
-
if (/^\[\.\.\..+\]$/.test(
|
|
129
|
-
return
|
|
130
|
-
if (
|
|
131
|
-
if (!/^\[.+\]$/.test(
|
|
115
|
+
let i;
|
|
116
|
+
for (const a of o)
|
|
117
|
+
St(a.segments, s) && (!i || a.segments.length > i.segments.length) && (i = a);
|
|
118
|
+
i ? (i.page.children ??= []).push(c) : n.push(c), o.push({ segments: s, page: c });
|
|
119
|
+
}
|
|
120
|
+
return n;
|
|
121
|
+
}
|
|
122
|
+
function U(t, e) {
|
|
123
|
+
const n = P(t), o = P(e);
|
|
124
|
+
for (let r = 0; r < o.length; r++) {
|
|
125
|
+
const s = o[r];
|
|
126
|
+
if (/^\[\[\.\.\..+\]\]$/.test(s))
|
|
127
|
+
return r <= n.length;
|
|
128
|
+
if (/^\[\.\.\..+\]$/.test(s))
|
|
129
|
+
return r < n.length;
|
|
130
|
+
if (r >= n.length) return !1;
|
|
131
|
+
if (!/^\[.+\]$/.test(s) && s !== n[r])
|
|
132
132
|
return !1;
|
|
133
133
|
}
|
|
134
|
-
return o.length ===
|
|
134
|
+
return o.length === n.length;
|
|
135
135
|
}
|
|
136
|
-
function
|
|
137
|
-
for (const
|
|
138
|
-
|
|
139
|
-
return
|
|
136
|
+
function et(t, e = []) {
|
|
137
|
+
for (const n of t)
|
|
138
|
+
e.push({ page: n, segments: P(n.path) }), n.children && et(n.children, e);
|
|
139
|
+
return e;
|
|
140
140
|
}
|
|
141
|
-
function
|
|
142
|
-
return
|
|
141
|
+
function B(t) {
|
|
142
|
+
return t.external ?? /^https?:\/\//.test(t.path);
|
|
143
143
|
}
|
|
144
|
-
function
|
|
145
|
-
const
|
|
146
|
-
(l) => l.page.path ===
|
|
147
|
-
),
|
|
148
|
-
(l) => l.routePath ===
|
|
149
|
-
),
|
|
150
|
-
(
|
|
144
|
+
function It(t, e) {
|
|
145
|
+
const n = structuredClone(t), o = et(n.pages), r = o.filter((d) => !B(d.page)), s = (d) => r.some(
|
|
146
|
+
(l) => l.page.path === d.routePath || U(l.page.path, d.routePath)
|
|
147
|
+
), c = e.filter((d) => !s(d)), i = (d) => e.some(
|
|
148
|
+
(l) => l.routePath === d.page.path || U(d.page.path, l.routePath)
|
|
149
|
+
), a = r.filter((d) => !i(d)).map((d) => d.page.path), f = [], g = [...o], b = [...c].sort(
|
|
150
|
+
(d, l) => P(d.routePath).length - P(l.routePath).length
|
|
151
151
|
);
|
|
152
|
-
for (const
|
|
153
|
-
const l =
|
|
154
|
-
let
|
|
155
|
-
for (const S of
|
|
156
|
-
if (
|
|
157
|
-
const
|
|
158
|
-
|
|
152
|
+
for (const d of b) {
|
|
153
|
+
const l = P(d.routePath), u = { path: d.routePath, title: d.title ?? l.at(-1) ?? "/" };
|
|
154
|
+
let p;
|
|
155
|
+
for (const S of g) {
|
|
156
|
+
if (B(S.page)) continue;
|
|
157
|
+
const w = S.segments;
|
|
158
|
+
w.length >= l.length || w.every((y, T) => y === l[T]) && (!p || w.length > p.segments.length) && (p = S);
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
p ? (p.page.children ??= []).push(u) : n.pages.push(u), g.push({ page: u, segments: l }), f.push(d.routePath);
|
|
161
161
|
}
|
|
162
|
-
return { config:
|
|
162
|
+
return { config: n, added: f, removed: a };
|
|
163
163
|
}
|
|
164
|
-
const
|
|
165
|
-
function
|
|
166
|
-
for (const
|
|
167
|
-
if (
|
|
164
|
+
const Rt = /^page\.(tsx|jsx|ts|js)$/, At = /^\(.+\)$/, Pt = /^\(\.{1,3}\)/;
|
|
165
|
+
function H(t) {
|
|
166
|
+
for (const e of ["app", "src/app"])
|
|
167
|
+
if (I(h(t, e))) return h(t, e);
|
|
168
168
|
return null;
|
|
169
169
|
}
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
for (const
|
|
173
|
-
if (
|
|
174
|
-
|
|
170
|
+
function Tt(t) {
|
|
171
|
+
const e = [];
|
|
172
|
+
for (const n of t) {
|
|
173
|
+
if (n.startsWith("@") || n.startsWith("_") || Pt.test(n)) return null;
|
|
174
|
+
At.test(n) || e.push(n);
|
|
175
175
|
}
|
|
176
|
-
return `/${
|
|
176
|
+
return `/${e.join("/")}`;
|
|
177
177
|
}
|
|
178
|
-
function
|
|
179
|
-
const
|
|
178
|
+
function Et(t) {
|
|
179
|
+
const e = t.match(
|
|
180
180
|
/export\s+const\s+metadata\s*(?::\s*[A-Za-z_$][\w$.]*\s*)?=\s*\{(?:[^{}]*?[,\s])?title\s*:\s*(['"])((?:\\.|(?!\1)[^\\])*)\1/
|
|
181
181
|
);
|
|
182
|
-
return
|
|
182
|
+
return e ? e[2] : void 0;
|
|
183
183
|
}
|
|
184
|
-
function
|
|
185
|
-
for (const o of k(
|
|
186
|
-
o.isDirectory() ?
|
|
184
|
+
function nt(t, e, n) {
|
|
185
|
+
for (const o of k(t, { withFileTypes: !0 }))
|
|
186
|
+
o.isDirectory() ? nt(h(t, o.name), [...e, o.name], n) : Rt.test(o.name) && n.push({ file: h(t, o.name), segments: e });
|
|
187
187
|
}
|
|
188
|
-
const
|
|
188
|
+
const Ot = {
|
|
189
189
|
name: "next-app",
|
|
190
|
-
detect(
|
|
191
|
-
return
|
|
190
|
+
detect(t) {
|
|
191
|
+
return H(t) !== null;
|
|
192
192
|
},
|
|
193
|
-
scan(
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
196
|
-
const
|
|
197
|
-
|
|
193
|
+
scan(t) {
|
|
194
|
+
const e = H(t);
|
|
195
|
+
if (!e) return [];
|
|
196
|
+
const n = [];
|
|
197
|
+
nt(e, [], n);
|
|
198
198
|
const o = [];
|
|
199
|
-
for (const { file:
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
routePath:
|
|
203
|
-
isDynamic: /\[.+\]/.test(
|
|
204
|
-
title:
|
|
199
|
+
for (const { file: r, segments: s } of n) {
|
|
200
|
+
const c = Tt(s);
|
|
201
|
+
c !== null && o.push({
|
|
202
|
+
routePath: c,
|
|
203
|
+
isDynamic: /\[.+\]/.test(c),
|
|
204
|
+
title: Et(E(r, "utf8"))
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
return o;
|
|
208
208
|
}
|
|
209
|
-
},
|
|
210
|
-
function K(
|
|
211
|
-
for (const
|
|
212
|
-
if (
|
|
209
|
+
}, J = /\.(tsx|jsx|ts|js)$/, Nt = /* @__PURE__ */ new Set(["_app", "_document", "_error", "404", "500"]);
|
|
210
|
+
function K(t) {
|
|
211
|
+
for (const e of ["pages", "src/pages"])
|
|
212
|
+
if (I(h(t, e))) return h(t, e);
|
|
213
213
|
return null;
|
|
214
214
|
}
|
|
215
|
-
function
|
|
216
|
-
for (const o of k(
|
|
215
|
+
function ot(t, e, n) {
|
|
216
|
+
for (const o of k(t, { withFileTypes: !0 })) {
|
|
217
217
|
if (o.isDirectory()) {
|
|
218
|
-
if (
|
|
219
|
-
|
|
218
|
+
if (e.length === 0 && o.name === "api") continue;
|
|
219
|
+
ot(h(t, o.name), [...e, o.name], n);
|
|
220
220
|
continue;
|
|
221
221
|
}
|
|
222
|
-
if (!
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
225
|
-
const
|
|
226
|
-
|
|
222
|
+
if (!J.test(o.name)) continue;
|
|
223
|
+
const r = o.name.replace(J, "");
|
|
224
|
+
if (Nt.has(r)) continue;
|
|
225
|
+
const c = `/${(r === "index" ? e : [...e, r]).join("/")}`;
|
|
226
|
+
n.push({ routePath: c, isDynamic: /\[.+\]/.test(c) });
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
|
-
const
|
|
229
|
+
const xt = {
|
|
230
230
|
name: "next-pages",
|
|
231
|
-
detect(
|
|
232
|
-
return K(
|
|
231
|
+
detect(t) {
|
|
232
|
+
return K(t) !== null;
|
|
233
233
|
},
|
|
234
|
-
scan(
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
237
|
-
const
|
|
238
|
-
return
|
|
234
|
+
scan(t) {
|
|
235
|
+
const e = K(t);
|
|
236
|
+
if (!e) return [];
|
|
237
|
+
const n = [];
|
|
238
|
+
return ot(e, [], n), n;
|
|
239
239
|
}
|
|
240
|
-
},
|
|
240
|
+
}, Dt = `import { defineIA } from "pubuilder/config"
|
|
241
241
|
|
|
242
242
|
// pubuilder scan은 아직 이 프로젝트의 라우팅 방식을 지원하지 않아요 (v1은 Next.js 전용).
|
|
243
243
|
// 아래 예시를 참고해 앱의 화면 구조를 직접 작성해주세요.
|
|
@@ -257,172 +257,236 @@ export default defineIA({
|
|
|
257
257
|
],
|
|
258
258
|
})
|
|
259
259
|
`;
|
|
260
|
-
class
|
|
261
|
-
constructor(
|
|
262
|
-
super(`${
|
|
260
|
+
class m extends Error {
|
|
261
|
+
constructor(e, n, o = 400) {
|
|
262
|
+
super(`${e}: ${n}`), this.code = e, this.status = o;
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
-
function
|
|
266
|
-
let
|
|
265
|
+
function Ft(t) {
|
|
266
|
+
let e;
|
|
267
267
|
try {
|
|
268
|
-
|
|
268
|
+
e = new URL(t);
|
|
269
269
|
} catch {
|
|
270
|
-
throw new
|
|
270
|
+
throw new m("INVALID_FIGMA_URL", "URL 형식이 아니에요");
|
|
271
271
|
}
|
|
272
|
-
if (!/(^|\.)figma\.com$/.test(
|
|
273
|
-
throw new
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
const o =
|
|
272
|
+
if (!/(^|\.)figma\.com$/.test(e.hostname))
|
|
273
|
+
throw new m("INVALID_FIGMA_URL", "figma.com 링크가 아니에요");
|
|
274
|
+
const n = e.pathname.match(/^\/(?:design|file)\/([A-Za-z0-9]+)(?:\/|$)/);
|
|
275
|
+
if (!n) throw new m("INVALID_FIGMA_URL", "fileKey를 찾지 못했어요");
|
|
276
|
+
const o = e.searchParams.get("node-id");
|
|
277
277
|
if (!o)
|
|
278
|
-
throw new
|
|
279
|
-
return { fileKey:
|
|
278
|
+
throw new m("INVALID_FIGMA_URL", "node-id가 없어요. Figma에서 프레임 우클릭 → Copy link로 복사해주세요");
|
|
279
|
+
return { fileKey: n[1], nodeId: o.replace(/-/g, ":") };
|
|
280
280
|
}
|
|
281
|
-
function L(
|
|
282
|
-
return
|
|
281
|
+
function L(t) {
|
|
282
|
+
return h(t, ".pubuilder", "credentials.json");
|
|
283
283
|
}
|
|
284
|
-
function V(
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
284
|
+
function V(t) {
|
|
285
|
+
const e = L(t.homeDir);
|
|
286
|
+
if (I(e))
|
|
287
287
|
try {
|
|
288
|
-
const o = JSON.parse(
|
|
288
|
+
const o = JSON.parse(E(e, "utf8"));
|
|
289
289
|
if (o.figmaToken) return o.figmaToken;
|
|
290
290
|
} catch {
|
|
291
291
|
}
|
|
292
|
-
if (
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
const o =
|
|
296
|
-
`).find((
|
|
292
|
+
if (t.env.FIGMA_TOKEN) return t.env.FIGMA_TOKEN;
|
|
293
|
+
const n = h(t.projectRoot, ".env");
|
|
294
|
+
if (I(n)) {
|
|
295
|
+
const o = E(n, "utf8").split(`
|
|
296
|
+
`).find((r) => r.trim().startsWith("FIGMA_TOKEN="));
|
|
297
297
|
if (o) {
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
298
|
+
const r = o.slice(o.indexOf("=") + 1).trim().replace(/^["']|["']$/g, "");
|
|
299
|
+
if (r) return r;
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
return null;
|
|
303
303
|
}
|
|
304
|
-
function
|
|
305
|
-
|
|
304
|
+
function jt(t, e) {
|
|
305
|
+
j(h(t, ".pubuilder"), { recursive: !0 }), A(L(t), JSON.stringify({ figmaToken: e }, null, 2), { mode: 384 });
|
|
306
306
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
307
|
+
function kt(t) {
|
|
308
|
+
_(L(t), { force: !0 });
|
|
309
309
|
}
|
|
310
|
-
function
|
|
311
|
-
return
|
|
310
|
+
function _t(t) {
|
|
311
|
+
return t.length <= 5 ? "*".repeat(t.length) : `${t.slice(0, 5)}${"*".repeat(Math.max(4, t.length - 5))}`;
|
|
312
312
|
}
|
|
313
|
-
async function
|
|
314
|
-
const
|
|
315
|
-
`https://api.figma.com/v1/files/${
|
|
316
|
-
{ headers:
|
|
313
|
+
async function Lt(t, e, n, o = fetch) {
|
|
314
|
+
const r = { "X-Figma-Token": e }, s = await o(
|
|
315
|
+
`https://api.figma.com/v1/files/${t.fileKey}/nodes?ids=${encodeURIComponent(t.nodeId)}`,
|
|
316
|
+
{ headers: r }
|
|
317
317
|
);
|
|
318
|
-
if (
|
|
319
|
-
if (!
|
|
320
|
-
const
|
|
321
|
-
if (!
|
|
322
|
-
const
|
|
323
|
-
`https://api.figma.com/v1/images/${
|
|
324
|
-
{ headers:
|
|
318
|
+
if (s.status === 403) throw new m("FIGMA_API_ERROR", "Figma 토큰이 유효하지 않아요. 스킬함 설정에서 토큰을 확인해주세요", 403);
|
|
319
|
+
if (!s.ok) throw new m("FIGMA_API_ERROR", `Figma API 오류 (${s.status})`, 502);
|
|
320
|
+
const i = (await s.json()).nodes[t.nodeId]?.document;
|
|
321
|
+
if (!i) throw new m("FIGMA_API_ERROR", "해당 노드를 찾지 못했어요. URL의 node-id를 확인해주세요", 404);
|
|
322
|
+
const a = await o(
|
|
323
|
+
`https://api.figma.com/v1/images/${t.fileKey}?ids=${encodeURIComponent(t.nodeId)}&format=png&scale=2`,
|
|
324
|
+
{ headers: r }
|
|
325
325
|
);
|
|
326
|
-
if (!
|
|
327
|
-
const
|
|
328
|
-
if (!
|
|
329
|
-
const b = await (await o(
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
function W(
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
337
|
-
const
|
|
338
|
-
return { name:
|
|
339
|
-
}
|
|
340
|
-
class
|
|
326
|
+
if (!a.ok) throw new m("FIGMA_API_ERROR", `이미지 export 실패 (${a.status})`, 502);
|
|
327
|
+
const g = (await a.json()).images[t.nodeId];
|
|
328
|
+
if (!g) throw new m("FIGMA_API_ERROR", "이미지 export URL이 비어 있어요", 502);
|
|
329
|
+
const b = await (await o(g)).arrayBuffer();
|
|
330
|
+
j(n, { recursive: !0 });
|
|
331
|
+
const d = t.nodeId.replace(/[^A-Za-z0-9_-]/g, "-"), l = h(n, `${t.fileKey}-${d}.png`);
|
|
332
|
+
return A(l, Buffer.from(b)), { nodeJson: i, imagePath: l };
|
|
333
|
+
}
|
|
334
|
+
function W(t) {
|
|
335
|
+
const e = t.match(/^---\n([\s\S]*?)\n---/);
|
|
336
|
+
if (!e) return {};
|
|
337
|
+
const n = (o) => e[1].match(new RegExp(`^${o}:\\s*(.+)$`, "m"))?.[1].trim();
|
|
338
|
+
return { name: n("name"), description: n("description") };
|
|
339
|
+
}
|
|
340
|
+
class Mt {
|
|
341
341
|
projectRoot;
|
|
342
342
|
homeDir;
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
/** 패키지에 번들되어 배포되는 기본 스킬 디렉토리 (없으면 스캔 안 함) */
|
|
344
|
+
builtinDir;
|
|
345
|
+
constructor(e) {
|
|
346
|
+
this.projectRoot = e.projectRoot, this.homeDir = e.homeDir, this.builtinDir = e.builtinDir;
|
|
345
347
|
}
|
|
346
348
|
settingsPath() {
|
|
347
|
-
return
|
|
349
|
+
return h(this.projectRoot, ".pubuilder", "settings.json");
|
|
348
350
|
}
|
|
349
351
|
loadSettings() {
|
|
350
|
-
if (!
|
|
352
|
+
if (!I(this.settingsPath())) return { skills: {} };
|
|
351
353
|
try {
|
|
352
|
-
return JSON.parse(
|
|
354
|
+
return JSON.parse(E(this.settingsPath(), "utf8"));
|
|
353
355
|
} catch {
|
|
354
356
|
return { skills: {} };
|
|
355
357
|
}
|
|
356
358
|
}
|
|
357
|
-
saveSettings(
|
|
358
|
-
|
|
359
|
+
saveSettings(e) {
|
|
360
|
+
j(h(this.projectRoot, ".pubuilder"), { recursive: !0 }), A(this.settingsPath(), JSON.stringify(e, null, 2));
|
|
359
361
|
}
|
|
360
|
-
scanDir(
|
|
361
|
-
if (!
|
|
362
|
+
scanDir(e, n) {
|
|
363
|
+
if (!I(e)) return [];
|
|
362
364
|
const o = [];
|
|
363
|
-
for (const
|
|
364
|
-
if (!
|
|
365
|
-
const
|
|
366
|
-
if (!
|
|
367
|
-
const
|
|
365
|
+
for (const r of k(e, { withFileTypes: !0 })) {
|
|
366
|
+
if (!r.isDirectory()) continue;
|
|
367
|
+
const s = h(e, r.name, "SKILL.md");
|
|
368
|
+
if (!I(s)) continue;
|
|
369
|
+
const c = W(E(s, "utf8"));
|
|
368
370
|
o.push({
|
|
369
|
-
id: `${
|
|
370
|
-
name:
|
|
371
|
-
description:
|
|
372
|
-
source:
|
|
373
|
-
path:
|
|
371
|
+
id: `${n}:${r.name}`,
|
|
372
|
+
name: c.name ?? r.name,
|
|
373
|
+
description: c.description ?? "",
|
|
374
|
+
source: n,
|
|
375
|
+
path: s
|
|
374
376
|
});
|
|
375
377
|
}
|
|
376
378
|
return o;
|
|
377
379
|
}
|
|
378
380
|
scanAll() {
|
|
379
381
|
return [
|
|
380
|
-
...this.scanDir(
|
|
381
|
-
...this.scanDir(
|
|
382
|
-
...this.scanDir(
|
|
382
|
+
...this.builtinDir ? this.scanDir(this.builtinDir, "builtin") : [],
|
|
383
|
+
...this.scanDir(h(this.projectRoot, ".claude", "skills"), "project"),
|
|
384
|
+
...this.scanDir(h(this.homeDir, ".claude", "skills"), "global"),
|
|
385
|
+
...this.scanDir(h(this.projectRoot, ".pubuilder", "skills"), "uploaded")
|
|
383
386
|
];
|
|
384
387
|
}
|
|
385
|
-
/** 기본값: project·uploaded on, global off (스펙 결정) */
|
|
388
|
+
/** 기본값: builtin·project·uploaded on, global off (스펙 결정) */
|
|
386
389
|
list() {
|
|
387
|
-
const
|
|
388
|
-
return this.scanAll().map(({ path:
|
|
390
|
+
const e = this.loadSettings();
|
|
391
|
+
return this.scanAll().map(({ path: n, ...o }) => ({
|
|
389
392
|
...o,
|
|
390
|
-
enabled:
|
|
393
|
+
enabled: e.skills[o.id] ?? o.source !== "global"
|
|
391
394
|
}));
|
|
392
395
|
}
|
|
393
|
-
setEnabled(
|
|
394
|
-
if (!this.scanAll().some((
|
|
396
|
+
setEnabled(e, n) {
|
|
397
|
+
if (!this.scanAll().some((r) => r.id === e)) throw new m("NOT_FOUND", `스킬 없음: ${e}`, 404);
|
|
395
398
|
const o = this.loadSettings();
|
|
396
|
-
o.skills[
|
|
399
|
+
o.skills[e] = n, this.saveSettings(o);
|
|
397
400
|
}
|
|
398
|
-
upload(
|
|
399
|
-
const o = W(
|
|
400
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(
|
|
401
|
-
throw new
|
|
402
|
-
const
|
|
403
|
-
return
|
|
401
|
+
upload(e, n) {
|
|
402
|
+
const o = W(n), r = (o.name ?? e.replace(/\.md$/i, "")).trim();
|
|
403
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(r))
|
|
404
|
+
throw new m("INVALID_NAME", `스킬 이름이 유효하지 않아요: ${r}`, 400);
|
|
405
|
+
const s = h(this.projectRoot, ".pubuilder", "skills", r);
|
|
406
|
+
return j(s, { recursive: !0 }), A(h(s, "SKILL.md"), n), { id: `uploaded:${r}`, name: r, description: o.description ?? "", source: "uploaded", enabled: !0 };
|
|
404
407
|
}
|
|
405
408
|
/** 삭제는 uploaded만 — 유저의 .claude/skills를 지우는 사고 방지 (스펙) */
|
|
406
|
-
remove(
|
|
407
|
-
if (!
|
|
408
|
-
if (!this.scanAll().some((
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
delete
|
|
409
|
+
remove(e) {
|
|
410
|
+
if (!e.startsWith("uploaded:")) throw new m("FORBIDDEN", "업로드된 스킬만 삭제할 수 있어요", 403);
|
|
411
|
+
if (!this.scanAll().some((s) => s.id === e)) throw new m("NOT_FOUND", `스킬 없음: ${e}`, 404);
|
|
412
|
+
const n = e.slice(9), o = h(this.projectRoot, ".pubuilder", "skills", n);
|
|
413
|
+
_(o, { recursive: !0 });
|
|
414
|
+
const r = this.loadSettings();
|
|
415
|
+
delete r.skills[e], this.saveSettings(r);
|
|
413
416
|
}
|
|
414
417
|
enabledSkillContents() {
|
|
415
|
-
const
|
|
416
|
-
return this.scanAll().filter((
|
|
418
|
+
const e = this.loadSettings();
|
|
419
|
+
return this.scanAll().filter((n) => e.skills[n.id] ?? n.source !== "global").map((n) => ({ name: n.name, content: E(n.path, "utf8") }));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function rt(t) {
|
|
423
|
+
if (!t || t.visible === !1 || t.type !== "SOLID" || !t.color) return null;
|
|
424
|
+
const e = (o) => Math.round(o * 255), n = t.opacity ?? t.color.a ?? 1;
|
|
425
|
+
return `rgba(${e(t.color.r)}, ${e(t.color.g)}, ${e(t.color.b)}, ${n})`;
|
|
426
|
+
}
|
|
427
|
+
function z(t) {
|
|
428
|
+
return Array.isArray(t) ? t.map(rt).filter((e) => e !== null) : [];
|
|
429
|
+
}
|
|
430
|
+
function Ct(t) {
|
|
431
|
+
if (t.layoutMode !== "VERTICAL" && t.layoutMode !== "HORIZONTAL") return;
|
|
432
|
+
const e = { mode: t.layoutMode === "VERTICAL" ? "col" : "row" };
|
|
433
|
+
t.itemSpacing && (e.gap = t.itemSpacing);
|
|
434
|
+
const n = [
|
|
435
|
+
t.paddingTop ?? 0,
|
|
436
|
+
t.paddingRight ?? 0,
|
|
437
|
+
t.paddingBottom ?? 0,
|
|
438
|
+
t.paddingLeft ?? 0
|
|
439
|
+
];
|
|
440
|
+
return n.some((o) => o !== 0) && (e.padding = n), t.primaryAxisAlignItems && (e.justify = t.primaryAxisAlignItems), t.counterAxisAlignItems && (e.align = t.counterAxisAlignItems), e;
|
|
441
|
+
}
|
|
442
|
+
function it(t, e) {
|
|
443
|
+
const n = { name: t.name ?? "", type: t.type ?? "" };
|
|
444
|
+
t.absoluteBoundingBox && (n.size = { w: Math.round(t.absoluteBoundingBox.width), h: Math.round(t.absoluteBoundingBox.height) });
|
|
445
|
+
const o = Ct(t);
|
|
446
|
+
o && (n.layout = o), typeof t.cornerRadius == "number" ? n.radius = t.cornerRadius : Array.isArray(t.rectangleCornerRadii) && (n.radius = t.rectangleCornerRadii);
|
|
447
|
+
const r = z(t.fills);
|
|
448
|
+
if (r.length) {
|
|
449
|
+
n.fills = r;
|
|
450
|
+
for (const a of r) e.set(a, (e.get(a) ?? 0) + 1);
|
|
451
|
+
}
|
|
452
|
+
const s = z(t.strokes);
|
|
453
|
+
s.length && (n.strokes = s);
|
|
454
|
+
const c = (t.effects ?? []).filter((a) => a.visible !== !1 && (a.type === "DROP_SHADOW" || a.type === "INNER_SHADOW")).map((a) => {
|
|
455
|
+
const f = a.color ? rt({ type: "SOLID", color: a.color, opacity: a.color.a }) : null;
|
|
456
|
+
return `${a.type === "INNER_SHADOW" ? "inner " : ""}shadow ${a.offset?.x ?? 0} ${a.offset?.y ?? 0} ${a.radius ?? 0}${f ? ` ${f}` : ""}`;
|
|
457
|
+
});
|
|
458
|
+
if (c.length && (n.effects = c), t.type === "TEXT" && typeof t.characters == "string") {
|
|
459
|
+
const a = t.style ?? {};
|
|
460
|
+
n.text = {
|
|
461
|
+
chars: t.characters,
|
|
462
|
+
...a.fontFamily ? { font: a.fontFamily } : {},
|
|
463
|
+
...a.fontSize !== void 0 ? { size: a.fontSize } : {},
|
|
464
|
+
...a.fontWeight !== void 0 ? { weight: a.fontWeight } : {},
|
|
465
|
+
...a.lineHeightPx !== void 0 ? { lineHeight: Math.round(a.lineHeightPx) } : {},
|
|
466
|
+
...a.letterSpacing !== void 0 ? { letterSpacing: a.letterSpacing } : {}
|
|
467
|
+
};
|
|
417
468
|
}
|
|
469
|
+
if (t.componentProperties) {
|
|
470
|
+
const a = {};
|
|
471
|
+
for (const [f, g] of Object.entries(t.componentProperties))
|
|
472
|
+
a[f.split("#")[0]] = String(g?.value ?? "");
|
|
473
|
+
Object.keys(a).length && (n.component = a);
|
|
474
|
+
}
|
|
475
|
+
const i = (t.children ?? []).filter((a) => a.visible !== !1 && a.opacity !== 0).map((a) => it(a, e));
|
|
476
|
+
return i.length && (n.children = i), n;
|
|
477
|
+
}
|
|
478
|
+
function Gt(t) {
|
|
479
|
+
const e = /* @__PURE__ */ new Map(), n = it(t ?? {}, e), o = [...e.entries()].sort((r, s) => s[1] - r[1]).map(([r, s]) => ({ rgba: r, count: s }));
|
|
480
|
+
return { root: n, colors: o };
|
|
418
481
|
}
|
|
419
|
-
function
|
|
420
|
-
const { selection:
|
|
482
|
+
function Ut(t) {
|
|
483
|
+
const { selection: e, figma: n } = t, o = Gt(n.nodeJson), r = o.colors.length ? o.colors.map((i) => ` ${i.rgba} ×${i.count}`).join(`
|
|
484
|
+
`) : " (없음)", s = t.skills.length ? `
|
|
421
485
|
# 적용할 스킬 지침
|
|
422
486
|
|
|
423
487
|
다른 스킬은 사용하지 말고, 아래에 명시된 스킬 지침만 따르세요.
|
|
424
488
|
|
|
425
|
-
${
|
|
489
|
+
${t.skills.map((i) => `## 스킬: ${i.name}
|
|
426
490
|
|
|
427
491
|
${i.content}`).join(`
|
|
428
492
|
|
|
@@ -431,86 +495,88 @@ ${i.content}`).join(`
|
|
|
431
495
|
`)}
|
|
432
496
|
` : `
|
|
433
497
|
다른 스킬은 스스로 판단해 사용하지 마세요. 아래에 명시된 스킬 지침만 따르세요 (이번 요청에는 없음).
|
|
434
|
-
`,
|
|
498
|
+
`, c = t.instruction ? `
|
|
435
499
|
# 사용자의 후속 답변
|
|
436
500
|
|
|
437
|
-
${
|
|
501
|
+
${t.instruction}
|
|
438
502
|
|
|
439
503
|
이전 실행에서 이미 변경된 현재 작업 트리를 확인하고, 위 답변에 따라 중단된 작업을 이어서 완료하세요. 같은 선택지를 다시 질문하지 마세요.
|
|
440
504
|
` : "";
|
|
441
505
|
return `이 프로젝트의 특정 UI 블록을 Figma 시안대로 수정하는 퍼블리싱 작업입니다.
|
|
442
506
|
|
|
443
507
|
# 대상 블록
|
|
444
|
-
- 페이지 라우트: ${
|
|
445
|
-
- CSS selector (렌더된 DOM 기준): ${
|
|
446
|
-
- 태그/라벨: <${
|
|
508
|
+
- 페이지 라우트: ${e.pagePath}
|
|
509
|
+
- CSS selector (렌더된 DOM 기준): ${e.selector}
|
|
510
|
+
- 태그/라벨: <${e.tag}> "${e.label}" (선택 단위: ${e.granularity})
|
|
447
511
|
- 렌더된 outerHTML 스냅샷 (소스 파일을 찾는 단서):
|
|
448
512
|
\`\`\`html
|
|
449
|
-
${
|
|
513
|
+
${t.outerHTML}
|
|
450
514
|
\`\`\`
|
|
451
515
|
|
|
452
516
|
# Figma 시안
|
|
453
|
-
- 링크: ${
|
|
454
|
-
- 렌더 이미지 (Read 도구로 반드시 열어 육안 확인): ${
|
|
455
|
-
- 노드
|
|
517
|
+
- 링크: ${n.url}
|
|
518
|
+
- 렌더 이미지 (Read 도구로 반드시 열어 육안 확인): ${n.imagePath}
|
|
519
|
+
- Design IR (규칙 기반 추출 — 퍼블리싱에 필요한 스타일 필드만, 숨김 노드 제외):
|
|
456
520
|
\`\`\`json
|
|
457
|
-
${JSON.stringify(
|
|
521
|
+
${JSON.stringify(o.root, null, 2)}
|
|
458
522
|
\`\`\`
|
|
459
|
-
|
|
523
|
+
- 색상 사용 빈도 (hex→프로젝트 토큰 매핑 힌트):
|
|
524
|
+
${r}
|
|
460
525
|
${s}
|
|
526
|
+
${c}
|
|
461
527
|
# 작업 순서
|
|
462
528
|
1. 위 라우트와 outerHTML 스냅샷을 단서로, 이 블록을 렌더링하는 소스 컴포넌트 파일을 찾으세요.
|
|
463
|
-
2. Figma 렌더 이미지를 Read 도구로 열어 시안을 육안으로 확인하세요.
|
|
529
|
+
2. Figma 렌더 이미지를 Read 도구로 열어 시안을 육안으로 확인하세요. Design IR의 수치(색상/간격/타이포)를 우선 근거로 쓰되, IR에만 있고 이미지에 보이지 않는 요소는 구현하지 마세요.
|
|
464
530
|
3. 해당 블록만 시안대로 수정하세요. 블록 바깥의 코드는 건드리지 마세요.
|
|
465
531
|
4. 수정 후 타입체크(pnpm type-check)가 있으면 실행해 통과를 확인하세요.
|
|
466
532
|
5. 마지막 메시지에 수정한 파일 경로를 나열하세요.`;
|
|
467
533
|
}
|
|
468
|
-
function
|
|
534
|
+
function Bt(t) {
|
|
469
535
|
try {
|
|
470
|
-
return
|
|
536
|
+
return Q("git", ["stash", "create"], { cwd: t, encoding: "utf8", stdio: "pipe" }).trim() || "HEAD";
|
|
471
537
|
} catch {
|
|
472
538
|
return null;
|
|
473
539
|
}
|
|
474
540
|
}
|
|
475
|
-
function
|
|
541
|
+
function st(t, e, n) {
|
|
476
542
|
try {
|
|
477
|
-
return
|
|
543
|
+
return Q("git", ["show", `${e}:${n}`], { cwd: t, encoding: "utf8", stdio: "pipe" });
|
|
478
544
|
} catch {
|
|
479
545
|
return null;
|
|
480
546
|
}
|
|
481
547
|
}
|
|
482
|
-
function
|
|
483
|
-
const o =
|
|
484
|
-
o === null ?
|
|
548
|
+
function Ht(t, e, n) {
|
|
549
|
+
const o = st(t, e, n), r = h(t, n);
|
|
550
|
+
o === null ? I(r) && _(r) : A(r, o);
|
|
485
551
|
}
|
|
486
|
-
function
|
|
487
|
-
let
|
|
552
|
+
function Jt(t) {
|
|
553
|
+
let e;
|
|
488
554
|
try {
|
|
489
|
-
|
|
555
|
+
e = JSON.parse(t);
|
|
490
556
|
} catch {
|
|
491
557
|
return [];
|
|
492
558
|
}
|
|
493
|
-
if (
|
|
559
|
+
if (e.type === "result")
|
|
494
560
|
return [
|
|
495
|
-
|
|
561
|
+
e.subtype === "success" ? { type: "done", text: String(e.result ?? "완료") } : { type: "error", text: String(e.result ?? e.subtype ?? "실패") }
|
|
496
562
|
];
|
|
497
|
-
if (
|
|
498
|
-
const
|
|
499
|
-
for (const o of
|
|
500
|
-
if (o.type === "text" && o.text &&
|
|
501
|
-
const
|
|
502
|
-
|
|
563
|
+
if (e.type === "assistant") {
|
|
564
|
+
const n = [];
|
|
565
|
+
for (const o of e.message?.content ?? [])
|
|
566
|
+
if (o.type === "text" && o.text && n.push({ type: "log", text: o.text }), o.type === "tool_use") {
|
|
567
|
+
const r = typeof o.input?.file_path == "string" ? o.input.file_path : void 0, s = o.input?.file_path ?? o.input?.command ?? "";
|
|
568
|
+
n.push({ type: "tool", text: `${o.name}: ${s}`.trim(), ...r ? { file: r } : {} });
|
|
503
569
|
}
|
|
504
|
-
return
|
|
570
|
+
return n;
|
|
505
571
|
}
|
|
506
572
|
return [];
|
|
507
573
|
}
|
|
508
|
-
const
|
|
509
|
-
run({ prompt:
|
|
510
|
-
return new Promise((
|
|
511
|
-
const
|
|
574
|
+
const X = 600 * 1e3, Kt = {
|
|
575
|
+
run({ prompt: t, cwd: e, onEvent: n, signal: o }) {
|
|
576
|
+
return new Promise((r) => {
|
|
577
|
+
const s = q("claude", [
|
|
512
578
|
"-p",
|
|
513
|
-
|
|
579
|
+
t,
|
|
514
580
|
"--output-format",
|
|
515
581
|
"stream-json",
|
|
516
582
|
"--verbose",
|
|
@@ -522,52 +588,52 @@ const z = 600 * 1e3, Gt = {
|
|
|
522
588
|
// 설정을 로드하면 개발자 머신의 관대한 allow 규칙이 화이트리스트를 무력화한다 (e2e에서 확인)
|
|
523
589
|
"--setting-sources",
|
|
524
590
|
""
|
|
525
|
-
], { cwd:
|
|
526
|
-
let
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
},
|
|
530
|
-
|
|
591
|
+
], { cwd: e, env: process.env });
|
|
592
|
+
let c = !1;
|
|
593
|
+
const i = (l) => {
|
|
594
|
+
c || (c = !0, clearTimeout(f), l && n(l), r());
|
|
595
|
+
}, a = () => {
|
|
596
|
+
s.kill("SIGKILL"), i({ type: "error", text: "에이전트가 10분간 응답이 없어 중단했어요" });
|
|
531
597
|
};
|
|
532
|
-
let
|
|
533
|
-
const
|
|
534
|
-
|
|
598
|
+
let f = setTimeout(a, X);
|
|
599
|
+
const g = () => {
|
|
600
|
+
c || (clearTimeout(f), f = setTimeout(a, X));
|
|
535
601
|
};
|
|
536
602
|
o?.addEventListener("abort", () => {
|
|
537
|
-
|
|
538
|
-
}),
|
|
539
|
-
|
|
603
|
+
s.kill("SIGKILL"), i({ type: "error", text: "사용자가 중단했어요" });
|
|
604
|
+
}), s.on("error", (l) => {
|
|
605
|
+
i({
|
|
540
606
|
type: "error",
|
|
541
607
|
text: l.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${l.message}`
|
|
542
608
|
});
|
|
543
609
|
});
|
|
544
610
|
const b = [];
|
|
545
|
-
|
|
546
|
-
let
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const u =
|
|
550
|
-
for (const
|
|
551
|
-
|
|
552
|
-
}),
|
|
553
|
-
if (l !== 0 && !
|
|
611
|
+
s.stderr.on("data", (l) => b.push(String(l)));
|
|
612
|
+
let d = !1;
|
|
613
|
+
yt({ input: s.stdout }).on("line", (l) => {
|
|
614
|
+
g();
|
|
615
|
+
const u = Jt(l);
|
|
616
|
+
for (const p of u)
|
|
617
|
+
n(p), (p.type === "done" || p.type === "error") && (d = !0);
|
|
618
|
+
}), s.on("close", (l) => {
|
|
619
|
+
if (l !== 0 && !d) {
|
|
554
620
|
const u = b.join("").trim();
|
|
555
|
-
|
|
621
|
+
i({ type: "error", text: u || `claude가 코드 ${l}로 종료됐어요. claude login 상태를 확인해주세요` });
|
|
556
622
|
} else
|
|
557
|
-
|
|
623
|
+
i();
|
|
558
624
|
});
|
|
559
625
|
});
|
|
560
626
|
}
|
|
561
|
-
},
|
|
562
|
-
class
|
|
563
|
-
constructor(
|
|
564
|
-
this.adapter =
|
|
627
|
+
}, Vt = 20, Z = 3;
|
|
628
|
+
class Wt {
|
|
629
|
+
constructor(e, n = {}) {
|
|
630
|
+
this.adapter = e, this.capture = n.captureBaseline ?? Bt, this.readBaseline = n.readBaseline ?? st, this.restoreFile = n.restoreFile ?? Ht, this.readCurrent = n.readCurrent ?? ((o, r) => {
|
|
565
631
|
try {
|
|
566
|
-
return
|
|
632
|
+
return E(h(o, r), "utf8");
|
|
567
633
|
} catch {
|
|
568
634
|
return "";
|
|
569
635
|
}
|
|
570
|
-
}), this.writeFile =
|
|
636
|
+
}), this.writeFile = n.writeFile ?? ((o, r, s) => A(h(o, r), s));
|
|
571
637
|
}
|
|
572
638
|
jobs = /* @__PURE__ */ new Map();
|
|
573
639
|
seq = 0;
|
|
@@ -579,56 +645,56 @@ class Ct {
|
|
|
579
645
|
/** suspend 시 파일별 baseline ref를 보관 → resolve의 revert에서 사용 */
|
|
580
646
|
pendingBaseline = /* @__PURE__ */ new Map();
|
|
581
647
|
activeCount() {
|
|
582
|
-
let
|
|
583
|
-
for (const
|
|
584
|
-
return
|
|
585
|
-
}
|
|
586
|
-
start(
|
|
587
|
-
if (this.activeCount() >=
|
|
588
|
-
throw new
|
|
589
|
-
const
|
|
648
|
+
let e = 0;
|
|
649
|
+
for (const n of this.jobs.values()) n.finished || e++;
|
|
650
|
+
return e;
|
|
651
|
+
}
|
|
652
|
+
start(e) {
|
|
653
|
+
if (this.activeCount() >= Z)
|
|
654
|
+
throw new m("BUSY", `동시 퍼블리싱은 최대 ${Z}개예요. 하나가 끝나면 다시 시도해주세요`, 409);
|
|
655
|
+
const n = ++this.seq, o = `job-${n}`, r = new AbortController(), s = {
|
|
590
656
|
id: o,
|
|
591
|
-
seq:
|
|
657
|
+
seq: n,
|
|
592
658
|
events: [],
|
|
593
659
|
listeners: /* @__PURE__ */ new Set(),
|
|
594
|
-
controller:
|
|
660
|
+
controller: r,
|
|
595
661
|
done: Promise.resolve(),
|
|
596
662
|
finished: !1,
|
|
597
|
-
baselineRef: this.capture(
|
|
663
|
+
baselineRef: this.capture(e.cwd),
|
|
598
664
|
touchedFiles: /* @__PURE__ */ new Set()
|
|
599
|
-
},
|
|
600
|
-
if (
|
|
601
|
-
const
|
|
602
|
-
|
|
665
|
+
}, c = (i) => {
|
|
666
|
+
if (i.type === "tool" && i.file) {
|
|
667
|
+
const a = ut(i.file) ? ft(e.cwd, i.file) : i.file;
|
|
668
|
+
a && !a.startsWith("..") && s.touchedFiles.add(a);
|
|
603
669
|
}
|
|
604
|
-
|
|
605
|
-
for (const
|
|
670
|
+
s.events.push(i);
|
|
671
|
+
for (const a of s.listeners) a(i);
|
|
606
672
|
};
|
|
607
|
-
return
|
|
608
|
-
|
|
609
|
-
}), this.jobs.set(o,
|
|
673
|
+
return s.done = this.adapter.run({ ...e, onEvent: c, signal: r.signal }).catch((i) => c({ type: "error", text: i instanceof Error ? i.message : String(i) })).finally(() => {
|
|
674
|
+
s.finished = !0, this.evictFinishedJobs();
|
|
675
|
+
}), this.jobs.set(o, s), o;
|
|
610
676
|
}
|
|
611
677
|
/** finished job이 상한을 넘으면 가장 오래된 것부터 제거 (active job은 finished=false라 대상에서 제외) */
|
|
612
678
|
evictFinishedJobs() {
|
|
613
|
-
const
|
|
614
|
-
for (;
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
this.jobs.delete(
|
|
679
|
+
const e = [...this.jobs.entries()].filter(([, n]) => n.finished).map(([n]) => n);
|
|
680
|
+
for (; e.length > Vt; ) {
|
|
681
|
+
const n = e.shift();
|
|
682
|
+
if (n === void 0) break;
|
|
683
|
+
this.jobs.delete(n);
|
|
618
684
|
}
|
|
619
685
|
}
|
|
620
686
|
/** SSE 핸들러가 헤더 전송 전에 job 존재를 확인할 때 사용 (subscribe는 replay 부작용이 있어 분리) */
|
|
621
|
-
has(
|
|
622
|
-
return this.jobs.has(
|
|
687
|
+
has(e) {
|
|
688
|
+
return this.jobs.has(e);
|
|
623
689
|
}
|
|
624
690
|
/** job이 편집한 파일 목록 (상대경로, 정렬) */
|
|
625
|
-
touchedFiles(
|
|
626
|
-
const
|
|
627
|
-
return
|
|
691
|
+
touchedFiles(e) {
|
|
692
|
+
const n = this.jobs.get(e);
|
|
693
|
+
return n ? [...n.touchedFiles].sort() : [];
|
|
628
694
|
}
|
|
629
695
|
/** job 시작 시점 baseline ref */
|
|
630
|
-
baselineRef(
|
|
631
|
-
return this.jobs.get(
|
|
696
|
+
baselineRef(e) {
|
|
697
|
+
return this.jobs.get(e)?.baselineRef ?? null;
|
|
632
698
|
}
|
|
633
699
|
/**
|
|
634
700
|
* 진행 중(running)인 job을 전부 abort하고, 그들이 편집한 파일을 분류한다.
|
|
@@ -636,99 +702,99 @@ class Ct {
|
|
|
636
702
|
* - 충돌: 완료(finished)된 job도 건드린 파일 → 사용자 해결 대상으로 반환
|
|
637
703
|
* 파일별 baseline은 그 파일을 건드린 running job 중 가장 이른(seq 최소) job의 것.
|
|
638
704
|
*/
|
|
639
|
-
suspendRunning(
|
|
640
|
-
const
|
|
641
|
-
for (const
|
|
705
|
+
suspendRunning(e) {
|
|
706
|
+
const n = [...this.jobs.values()].filter((i) => !i.finished).sort((i, a) => i.seq - a.seq);
|
|
707
|
+
for (const i of n) i.controller.abort();
|
|
642
708
|
const o = /* @__PURE__ */ new Map();
|
|
643
|
-
for (const
|
|
644
|
-
for (const
|
|
645
|
-
o.has(
|
|
646
|
-
const
|
|
647
|
-
for (const
|
|
648
|
-
if (
|
|
649
|
-
for (const
|
|
650
|
-
const
|
|
709
|
+
for (const i of n)
|
|
710
|
+
for (const a of i.touchedFiles)
|
|
711
|
+
o.has(a) || o.set(a, i.baselineRef);
|
|
712
|
+
const r = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const i of this.jobs.values())
|
|
714
|
+
if (i.finished)
|
|
715
|
+
for (const a of i.touchedFiles) r.add(a);
|
|
716
|
+
const s = [], c = [];
|
|
651
717
|
this.pendingBaseline.clear();
|
|
652
|
-
for (const [
|
|
653
|
-
|
|
654
|
-
return { autoReverted:
|
|
718
|
+
for (const [i, a] of o)
|
|
719
|
+
a !== null && (this.pendingBaseline.set(i, a), r.has(i) ? c.push({ file: i, baseline: this.readBaseline(e, a, i) ?? "", current: this.readCurrent(e, i) }) : (this.restoreFile(e, a, i), s.push(i)));
|
|
720
|
+
return { autoReverted: s, conflicts: c };
|
|
655
721
|
}
|
|
656
722
|
/**
|
|
657
723
|
* 충돌 파일에 대한 사용자 결정을 적용한다.
|
|
658
724
|
* revert → baseline 복원, edit → content로 덮어쓰기, keep → 그대로 둔다.
|
|
659
725
|
*/
|
|
660
|
-
applyResolutions(
|
|
661
|
-
for (const o of
|
|
726
|
+
applyResolutions(e, n) {
|
|
727
|
+
for (const o of n)
|
|
662
728
|
if (o.action === "revert") {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
} else o.action === "edit" && this.writeFile(
|
|
729
|
+
const r = this.pendingBaseline.get(o.file);
|
|
730
|
+
r && this.restoreFile(e, r, o.file);
|
|
731
|
+
} else o.action === "edit" && this.writeFile(e, o.file, o.content ?? "");
|
|
666
732
|
this.pendingBaseline.clear();
|
|
667
733
|
}
|
|
668
|
-
subscribe(
|
|
669
|
-
const o = this.jobs.get(
|
|
670
|
-
if (!o) throw new
|
|
671
|
-
for (const
|
|
672
|
-
return o.listeners.add(
|
|
734
|
+
subscribe(e, n) {
|
|
735
|
+
const o = this.jobs.get(e);
|
|
736
|
+
if (!o) throw new m("NOT_FOUND", `job 없음: ${e}`, 404);
|
|
737
|
+
for (const r of o.events) n(r);
|
|
738
|
+
return o.listeners.add(n), () => o.listeners.delete(n);
|
|
673
739
|
}
|
|
674
|
-
cancel(
|
|
675
|
-
const
|
|
676
|
-
if (!
|
|
677
|
-
|
|
740
|
+
cancel(e) {
|
|
741
|
+
const n = this.jobs.get(e);
|
|
742
|
+
if (!n) throw new m("NOT_FOUND", `job 없음: ${e}`, 404);
|
|
743
|
+
n.controller.abort();
|
|
678
744
|
}
|
|
679
|
-
waitFor(
|
|
680
|
-
const
|
|
681
|
-
if (!
|
|
682
|
-
return
|
|
745
|
+
waitFor(e) {
|
|
746
|
+
const n = this.jobs.get(e);
|
|
747
|
+
if (!n) throw new m("NOT_FOUND", `job 없음: ${e}`, 404);
|
|
748
|
+
return n.done;
|
|
683
749
|
}
|
|
684
750
|
}
|
|
685
|
-
const
|
|
686
|
-
function
|
|
687
|
-
return new Promise((
|
|
751
|
+
const zt = 4816, Xt = 1024 * 1024, Zt = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
|
752
|
+
function D(t) {
|
|
753
|
+
return new Promise((e, n) => {
|
|
688
754
|
const o = [];
|
|
689
|
-
let
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
|
|
755
|
+
let r = 0;
|
|
756
|
+
t.on("data", (s) => {
|
|
757
|
+
if (r += s.length, r > Xt) {
|
|
758
|
+
n(new m("PAYLOAD_TOO_LARGE", "요청 본문이 너무 커요 (1MB 초과)", 413)), t.destroy();
|
|
693
759
|
return;
|
|
694
760
|
}
|
|
695
|
-
o.push(
|
|
696
|
-
}),
|
|
761
|
+
o.push(s);
|
|
762
|
+
}), t.on("end", () => {
|
|
697
763
|
if (o.length === 0) {
|
|
698
|
-
|
|
764
|
+
e(void 0);
|
|
699
765
|
return;
|
|
700
766
|
}
|
|
701
767
|
try {
|
|
702
|
-
|
|
768
|
+
e(JSON.parse(Buffer.concat(o).toString("utf8")));
|
|
703
769
|
} catch {
|
|
704
|
-
|
|
770
|
+
n(new m("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
|
|
705
771
|
}
|
|
706
|
-
}),
|
|
772
|
+
}), t.on("error", n);
|
|
707
773
|
});
|
|
708
774
|
}
|
|
709
|
-
function
|
|
710
|
-
const o = JSON.stringify(
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
async function
|
|
714
|
-
const
|
|
715
|
-
async function
|
|
716
|
-
const u = await
|
|
717
|
-
if (typeof
|
|
718
|
-
throw new
|
|
719
|
-
const
|
|
720
|
-
if (!
|
|
721
|
-
const { nodeJson:
|
|
722
|
-
selection:
|
|
775
|
+
function R(t, e, n) {
|
|
776
|
+
const o = JSON.stringify(n);
|
|
777
|
+
t.writeHead(e, { "content-type": "application/json" }), t.end(o);
|
|
778
|
+
}
|
|
779
|
+
async function Yt(t) {
|
|
780
|
+
const e = t.projectRoot, n = t.homeDir ?? gt(), o = t.adapter ?? Kt, r = t.figmaFetch ?? fetch, s = new Mt({ projectRoot: e, homeDir: n, builtinDir: t.builtinDir }), c = new Wt(o);
|
|
781
|
+
async function i(l) {
|
|
782
|
+
const u = await D(l), { selection: p, outerHTML: S, figmaUrl: w, instruction: y } = u ?? {}, T = Ft(String(w ?? ""));
|
|
783
|
+
if (typeof p != "object" || p === null || typeof p.pagePath != "string" || typeof p.selector != "string" || typeof S != "string")
|
|
784
|
+
throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
|
|
785
|
+
const x = V({ projectRoot: e, homeDir: n, env: process.env });
|
|
786
|
+
if (!x) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
|
|
787
|
+
const { nodeJson: F, imagePath: $ } = await Lt(T, x, h(e, ".pubuilder/cache"), r), O = Ut({
|
|
788
|
+
selection: p,
|
|
723
789
|
outerHTML: S ?? "",
|
|
724
|
-
figma: { url: String(
|
|
725
|
-
skills:
|
|
726
|
-
instruction: typeof
|
|
790
|
+
figma: { url: String(w), nodeJson: F, imagePath: $ },
|
|
791
|
+
skills: s.enabledSkillContents(),
|
|
792
|
+
instruction: typeof y == "string" ? y.trim() : void 0
|
|
727
793
|
});
|
|
728
|
-
return { status: 202, body: { jobId:
|
|
794
|
+
return { status: 202, body: { jobId: c.start({ prompt: O, cwd: e }) } };
|
|
729
795
|
}
|
|
730
|
-
function
|
|
731
|
-
if (!
|
|
796
|
+
function a(l, u, p) {
|
|
797
|
+
if (!c.has(p)) throw new m("NOT_FOUND", `job 없음: ${p}`, 404);
|
|
732
798
|
u.writeHead(200, {
|
|
733
799
|
"content-type": "text/event-stream",
|
|
734
800
|
"cache-control": "no-cache",
|
|
@@ -736,212 +802,219 @@ async function Kt(n) {
|
|
|
736
802
|
});
|
|
737
803
|
let S = () => {
|
|
738
804
|
};
|
|
739
|
-
const
|
|
740
|
-
u.write(`data: ${JSON.stringify(
|
|
805
|
+
const w = (y) => {
|
|
806
|
+
u.write(`data: ${JSON.stringify(y)}
|
|
741
807
|
|
|
742
|
-
`), (
|
|
808
|
+
`), (y.type === "done" || y.type === "error") && (S(), u.end());
|
|
743
809
|
};
|
|
744
|
-
S =
|
|
810
|
+
S = c.subscribe(p, w), l.on("close", S);
|
|
745
811
|
}
|
|
746
|
-
const
|
|
812
|
+
const f = dt((l, u) => {
|
|
747
813
|
(async () => {
|
|
748
814
|
try {
|
|
749
|
-
const
|
|
750
|
-
if (
|
|
751
|
-
|
|
815
|
+
const p = l.headers.origin;
|
|
816
|
+
if (p && !Zt.test(p)) {
|
|
817
|
+
R(u, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
|
|
752
818
|
return;
|
|
753
819
|
}
|
|
754
|
-
if (
|
|
820
|
+
if (p && (u.setHeader("Access-Control-Allow-Origin", p), u.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE"), u.setHeader("Access-Control-Allow-Headers", "content-type")), l.method === "OPTIONS") {
|
|
755
821
|
u.writeHead(204), u.end();
|
|
756
822
|
return;
|
|
757
823
|
}
|
|
758
|
-
const
|
|
759
|
-
if (
|
|
760
|
-
|
|
824
|
+
const w = new URL(l.url ?? "/", "http://localhost").pathname, y = l.method ?? "GET";
|
|
825
|
+
if (y === "GET" && w === "/api/health") {
|
|
826
|
+
R(u, 200, { ok: !0 });
|
|
761
827
|
return;
|
|
762
828
|
}
|
|
763
|
-
if (
|
|
764
|
-
|
|
829
|
+
if (y === "GET" && w === "/api/skills") {
|
|
830
|
+
R(u, 200, s.list());
|
|
765
831
|
return;
|
|
766
832
|
}
|
|
767
|
-
if (
|
|
768
|
-
const $ = await
|
|
769
|
-
|
|
833
|
+
if (y === "POST" && w === "/api/skills") {
|
|
834
|
+
const $ = await D(l), O = s.upload($.filename, $.content);
|
|
835
|
+
R(u, 201, O);
|
|
770
836
|
return;
|
|
771
837
|
}
|
|
772
|
-
const
|
|
773
|
-
if (
|
|
774
|
-
const $ = decodeURIComponent(
|
|
775
|
-
|
|
838
|
+
const T = w.match(/^\/api\/skills\/([^/]+)$/);
|
|
839
|
+
if (T && y === "PATCH") {
|
|
840
|
+
const $ = decodeURIComponent(T[1]), O = await D(l);
|
|
841
|
+
s.setEnabled($, O.enabled), u.writeHead(204), u.end();
|
|
776
842
|
return;
|
|
777
843
|
}
|
|
778
|
-
if (
|
|
779
|
-
const $ = decodeURIComponent(
|
|
780
|
-
|
|
844
|
+
if (T && y === "DELETE") {
|
|
845
|
+
const $ = decodeURIComponent(T[1]);
|
|
846
|
+
s.remove($), u.writeHead(204), u.end();
|
|
781
847
|
return;
|
|
782
848
|
}
|
|
783
|
-
if (
|
|
784
|
-
const $ = V({ projectRoot:
|
|
785
|
-
|
|
849
|
+
if (y === "GET" && w === "/api/settings/figma-token") {
|
|
850
|
+
const $ = V({ projectRoot: e, homeDir: n, env: process.env });
|
|
851
|
+
R(u, 200, { configured: $ != null, masked: $ ? _t($) : null });
|
|
786
852
|
return;
|
|
787
853
|
}
|
|
788
|
-
if (
|
|
789
|
-
const $ = await
|
|
790
|
-
|
|
854
|
+
if (y === "PUT" && w === "/api/settings/figma-token") {
|
|
855
|
+
const $ = await D(l);
|
|
856
|
+
jt(n, $.token), u.writeHead(204), u.end();
|
|
791
857
|
return;
|
|
792
858
|
}
|
|
793
|
-
if (
|
|
794
|
-
|
|
859
|
+
if (y === "DELETE" && w === "/api/settings/figma-token") {
|
|
860
|
+
kt(n), u.writeHead(204), u.end();
|
|
795
861
|
return;
|
|
796
862
|
}
|
|
797
|
-
if (
|
|
798
|
-
const { status: $, body: O } = await
|
|
799
|
-
|
|
863
|
+
if (y === "POST" && w === "/api/publish") {
|
|
864
|
+
const { status: $, body: O } = await i(l);
|
|
865
|
+
R(u, $, O);
|
|
800
866
|
return;
|
|
801
867
|
}
|
|
802
|
-
if (
|
|
803
|
-
|
|
868
|
+
if (y === "POST" && w === "/api/publish/suspend") {
|
|
869
|
+
R(u, 200, c.suspendRunning(e));
|
|
804
870
|
return;
|
|
805
871
|
}
|
|
806
|
-
if (
|
|
807
|
-
const $ = await
|
|
872
|
+
if (y === "POST" && w === "/api/publish/suspend/resolve") {
|
|
873
|
+
const $ = await D(l);
|
|
808
874
|
if (!Array.isArray($?.resolutions))
|
|
809
|
-
throw new
|
|
810
|
-
|
|
875
|
+
throw new m("INVALID_JSON", "resolutions 배열이 필요해요", 400);
|
|
876
|
+
c.applyResolutions(e, $.resolutions), u.writeHead(204), u.end();
|
|
811
877
|
return;
|
|
812
878
|
}
|
|
813
|
-
const
|
|
814
|
-
if (
|
|
815
|
-
|
|
879
|
+
const x = w.match(/^\/api\/publish\/([^/]+)\/events$/);
|
|
880
|
+
if (x && y === "GET") {
|
|
881
|
+
a(l, u, x[1]);
|
|
816
882
|
return;
|
|
817
883
|
}
|
|
818
|
-
const
|
|
819
|
-
if (
|
|
820
|
-
|
|
884
|
+
const F = w.match(/^\/api\/publish\/([^/]+)$/);
|
|
885
|
+
if (F && y === "DELETE") {
|
|
886
|
+
c.cancel(F[1]), u.writeHead(204), u.end();
|
|
821
887
|
return;
|
|
822
888
|
}
|
|
823
|
-
|
|
824
|
-
} catch (
|
|
825
|
-
if (
|
|
826
|
-
|
|
889
|
+
R(u, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
|
|
890
|
+
} catch (p) {
|
|
891
|
+
if (p instanceof m) {
|
|
892
|
+
R(u, p.status, { code: p.code, message: p.message });
|
|
827
893
|
return;
|
|
828
894
|
}
|
|
829
|
-
const S =
|
|
830
|
-
|
|
895
|
+
const S = p instanceof Error ? p.message : String(p);
|
|
896
|
+
R(u, 500, { code: "INTERNAL", message: S });
|
|
831
897
|
}
|
|
832
898
|
})();
|
|
833
|
-
}),
|
|
834
|
-
await new Promise((l) =>
|
|
835
|
-
const b =
|
|
899
|
+
}), g = t.port ?? zt;
|
|
900
|
+
await new Promise((l) => f.listen(g, "127.0.0.1", l));
|
|
901
|
+
const b = f.address();
|
|
836
902
|
return {
|
|
837
|
-
port: typeof b == "object" && b ? b.port :
|
|
903
|
+
port: typeof b == "object" && b ? b.port : g,
|
|
838
904
|
close() {
|
|
839
|
-
|
|
905
|
+
f.close();
|
|
840
906
|
}
|
|
841
907
|
};
|
|
842
908
|
}
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
909
|
+
function Qt() {
|
|
910
|
+
const t = pt(Y(import.meta.url));
|
|
911
|
+
for (const e of [["..", "skills"], ["..", "..", "skills"]]) {
|
|
912
|
+
const n = h(t, ...e);
|
|
913
|
+
if (I(h(n, "publish", "SKILL.md"))) return n;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function at(t, e) {
|
|
917
|
+
const n = await Yt({ projectRoot: t, port: e.port ?? 4816, builtinDir: Qt() });
|
|
918
|
+
return console.log(`[pubuilder] 컴패니언 서버 실행 중: http://localhost:${n.port}`), console.log("[pubuilder] 미니맵의 퍼블리싱 패널이 이 서버에 연결됩니다. Ctrl+C로 종료."), n;
|
|
846
919
|
}
|
|
847
|
-
function
|
|
848
|
-
const
|
|
849
|
-
if (
|
|
850
|
-
const
|
|
851
|
-
if (!Number.isInteger(
|
|
920
|
+
function ct(t) {
|
|
921
|
+
const e = t.indexOf("--port");
|
|
922
|
+
if (e === -1) return;
|
|
923
|
+
const n = Number(t[e + 1]);
|
|
924
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535)
|
|
852
925
|
throw new Error("--port 값이 유효하지 않아요 (0-65535)");
|
|
853
|
-
return
|
|
926
|
+
return n;
|
|
854
927
|
}
|
|
855
|
-
async function
|
|
856
|
-
const
|
|
928
|
+
async function qt(t, e) {
|
|
929
|
+
const n = e.indexOf("--"), o = n === -1 ? [] : e.slice(n + 1);
|
|
857
930
|
if (o.length === 0)
|
|
858
931
|
return console.error("사용법: pubuilder dev [--port N] -- <dev 명령> (예: pubuilder dev -- next dev)"), 1;
|
|
859
|
-
const
|
|
860
|
-
return new Promise((
|
|
861
|
-
const
|
|
862
|
-
process.on("SIGINT",
|
|
863
|
-
const
|
|
864
|
-
process.off("SIGINT",
|
|
932
|
+
const r = await at(t, { port: ct(e.slice(0, n === -1 ? void 0 : n)) });
|
|
933
|
+
return new Promise((s) => {
|
|
934
|
+
const c = q(o[0], o.slice(1), { cwd: t, stdio: "inherit", env: process.env }), i = () => c.kill("SIGINT"), a = () => c.kill("SIGTERM");
|
|
935
|
+
process.on("SIGINT", i), process.on("SIGTERM", a);
|
|
936
|
+
const f = () => {
|
|
937
|
+
process.off("SIGINT", i), process.off("SIGTERM", a), r.close();
|
|
865
938
|
};
|
|
866
|
-
|
|
867
|
-
console.error(`[pubuilder] 명령 실행 실패: ${
|
|
868
|
-
}),
|
|
869
|
-
|
|
939
|
+
c.on("error", (g) => {
|
|
940
|
+
console.error(`[pubuilder] 명령 실행 실패: ${g.message}`), f(), s(1);
|
|
941
|
+
}), c.on("close", (g, b) => {
|
|
942
|
+
f(), s(b ? 128 + (mt.signals[b] ?? 0) : g ?? 0);
|
|
870
943
|
});
|
|
871
944
|
});
|
|
872
945
|
}
|
|
873
|
-
const
|
|
874
|
-
function
|
|
875
|
-
const
|
|
876
|
-
if (!
|
|
946
|
+
const te = [Ot, xt];
|
|
947
|
+
function ee(t) {
|
|
948
|
+
const e = h(t, "package.json");
|
|
949
|
+
if (!I(e)) return !1;
|
|
877
950
|
try {
|
|
878
|
-
const
|
|
879
|
-
return !!(
|
|
951
|
+
const n = JSON.parse(E(e, "utf8"));
|
|
952
|
+
return !!(n.dependencies?.next ?? n.devDependencies?.next);
|
|
880
953
|
} catch {
|
|
881
954
|
return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
|
|
882
955
|
}
|
|
883
956
|
}
|
|
884
|
-
function
|
|
885
|
-
if (!
|
|
886
|
-
const
|
|
887
|
-
if (
|
|
888
|
-
const
|
|
889
|
-
for (const
|
|
890
|
-
for (const
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
893
|
-
console.warn(`[pubuilder] path 충돌: ${
|
|
957
|
+
function ne(t) {
|
|
958
|
+
if (!ee(t)) return null;
|
|
959
|
+
const e = te.filter((r) => r.detect(t));
|
|
960
|
+
if (e.length === 0) return null;
|
|
961
|
+
const n = /* @__PURE__ */ new Map(), o = [];
|
|
962
|
+
for (const r of e)
|
|
963
|
+
for (const s of r.scan(t)) {
|
|
964
|
+
const c = n.get(s.routePath);
|
|
965
|
+
if (c) {
|
|
966
|
+
console.warn(`[pubuilder] path 충돌: ${s.routePath} (${c} 우선, ${r.name} 무시)`);
|
|
894
967
|
continue;
|
|
895
968
|
}
|
|
896
|
-
|
|
969
|
+
n.set(s.routePath, r.name), o.push(s);
|
|
897
970
|
}
|
|
898
971
|
return o;
|
|
899
972
|
}
|
|
900
|
-
async function
|
|
901
|
-
const
|
|
902
|
-
if (!
|
|
973
|
+
async function oe(t) {
|
|
974
|
+
const n = await ht(import.meta.url).import(t, { default: !0 });
|
|
975
|
+
if (!n || !Array.isArray(n.pages))
|
|
903
976
|
throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
|
|
904
|
-
return
|
|
905
|
-
}
|
|
906
|
-
async function
|
|
907
|
-
const
|
|
908
|
-
if (
|
|
909
|
-
return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !
|
|
910
|
-
if (
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
for (const b of
|
|
914
|
-
for (const b of
|
|
915
|
-
return
|
|
916
|
-
}
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
return
|
|
921
|
-
}
|
|
922
|
-
async function
|
|
923
|
-
const [
|
|
977
|
+
return n;
|
|
978
|
+
}
|
|
979
|
+
async function re(t, e) {
|
|
980
|
+
const n = ne(t), o = h(t, "ia.config.ts");
|
|
981
|
+
if (n === null)
|
|
982
|
+
return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !I(o) && !e.dryRun && (A(o, Dt), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
|
|
983
|
+
if (I(o)) {
|
|
984
|
+
const i = await oe(o), { config: a, added: f, removed: g } = It(i, n);
|
|
985
|
+
C(a), e.dryRun || A(o, G(a, { newPaths: new Set(f) }));
|
|
986
|
+
for (const b of f) console.log(`[pubuilder] + 추가됨: ${b}`);
|
|
987
|
+
for (const b of g) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${b}`);
|
|
988
|
+
return f.length === 0 && g.length === 0 && console.log("[pubuilder] 변경 없음"), e.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: f, removed: g };
|
|
989
|
+
}
|
|
990
|
+
const s = { pages: vt(n) };
|
|
991
|
+
C(s);
|
|
992
|
+
const c = n.map((i) => i.routePath);
|
|
993
|
+
return e.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${c.length}개 라우트. 파일은 수정하지 않았어요.`) : (A(o, G(s, { newPaths: new Set(c) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${c.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: c };
|
|
994
|
+
}
|
|
995
|
+
async function ie(t) {
|
|
996
|
+
const [e, ...n] = t;
|
|
924
997
|
try {
|
|
925
|
-
return
|
|
926
|
-
})) :
|
|
998
|
+
return e === "scan" ? (await re(process.cwd(), { dryRun: n.includes("--dry-run") })).kind === "unsupported" ? 1 : 0 : e === "serve" ? (await at(process.cwd(), { port: ct(n) }), await new Promise(() => {
|
|
999
|
+
})) : e === "dev" ? await qt(process.cwd(), n) : (console.error("사용법: pubuilder scan [--dry-run] | pubuilder serve [--port N] | pubuilder dev [--port N] -- <명령>"), 1);
|
|
927
1000
|
} catch (o) {
|
|
928
1001
|
return console.error(`[pubuilder] 실패: ${o instanceof Error ? o.message : String(o)}`), 1;
|
|
929
1002
|
}
|
|
930
1003
|
}
|
|
931
|
-
function
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
1004
|
+
function se() {
|
|
1005
|
+
const t = process.argv[1];
|
|
1006
|
+
if (!t) return !1;
|
|
934
1007
|
try {
|
|
935
|
-
return
|
|
1008
|
+
return lt(t) === Y(import.meta.url);
|
|
936
1009
|
} catch {
|
|
937
1010
|
return !1;
|
|
938
1011
|
}
|
|
939
1012
|
}
|
|
940
|
-
|
|
941
|
-
(
|
|
1013
|
+
se() && ie(process.argv.slice(2)).then(
|
|
1014
|
+
(t) => process.exit(t),
|
|
942
1015
|
() => process.exit(1)
|
|
943
1016
|
);
|
|
944
1017
|
export {
|
|
945
|
-
|
|
946
|
-
|
|
1018
|
+
ie as main,
|
|
1019
|
+
re as runScan
|
|
947
1020
|
};
|