pubuilder 0.6.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 +28 -0
- package/dist/api.d.ts +20 -1
- package/dist/cli.js +656 -478
- package/dist/components/SuspendButton.d.ts +6 -0
- package/dist/components/SuspendConflictPanel.d.ts +5 -0
- package/dist/index.js +811 -594
- package/dist/index.js.map +1 -1
- package/dist/publish-jobs.d.ts +28 -0
- package/dist/server/agent.d.ts +52 -3
- 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/dist/server/snapshot.d.ts +10 -0
- package/dist/store.d.ts +18 -0
- 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 { spawn as
|
|
9
|
-
import { createInterface as
|
|
10
|
-
const
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
for (const [
|
|
17
|
-
if (i.delete(
|
|
18
|
-
for (const
|
|
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
|
-
|
|
21
|
-
const
|
|
22
|
-
return `${
|
|
20
|
+
M(i, f, g);
|
|
21
|
+
const a = i.toString();
|
|
22
|
+
return `${c}${a ? `?${a}` : ""}${o}`;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
24
|
+
function C(t) {
|
|
25
|
+
const e = [], n = /* @__PURE__ */ new Set(), o = (r, s, c) => {
|
|
26
26
|
for (const i of r) {
|
|
27
27
|
if (!i.path) throw new Error("[pubuilder] page.path는 필수입니다");
|
|
28
28
|
if (!i.title) throw new Error(`[pubuilder] "${i.path}"의 title이 없습니다`);
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
29
|
+
if (n.has(i.path)) throw new Error(`[pubuilder] 중복 path: ${i.path}`);
|
|
30
|
+
if (n.add(i.path), e.push({
|
|
31
31
|
id: i.path,
|
|
32
32
|
path: i.path,
|
|
33
33
|
title: i.title,
|
|
34
34
|
external: i.external ?? /^https?:\/\//.test(i.path),
|
|
35
35
|
parentId: s,
|
|
36
|
-
depth:
|
|
36
|
+
depth: c
|
|
37
37
|
}), i.variants?.length) {
|
|
38
38
|
if (i.external ?? /^https?:\/\//.test(i.path))
|
|
39
39
|
throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${i.path}`);
|
|
40
|
-
for (const
|
|
41
|
-
if (!
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
id:
|
|
46
|
-
path:
|
|
47
|
-
title:
|
|
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
49
|
parentId: i.path,
|
|
50
|
-
depth:
|
|
50
|
+
depth: c + 1
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
i.children?.length && o(i.children, i.path,
|
|
54
|
+
i.children?.length && o(i.children, i.path, c + 1);
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
|
-
if (o(
|
|
58
|
-
return { nodes:
|
|
57
|
+
if (o(t.pages, null, 0), e.length === 0) throw new Error("[pubuilder] pages가 비어 있습니다");
|
|
58
|
+
return { nodes: e, viewport: t.viewport ?? wt };
|
|
59
59
|
}
|
|
60
60
|
const v = " ";
|
|
61
|
-
function
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
function
|
|
65
|
-
return JSON.stringify(
|
|
66
|
-
}
|
|
67
|
-
function
|
|
68
|
-
const o = v.repeat(
|
|
69
|
-
|
|
70
|
-
const s =
|
|
71
|
-
if (!
|
|
72
|
-
return r.push(`${o}{ path: ${
|
|
73
|
-
if (r.push(`${o}{`), r.push(`${o}${v}path: ${
|
|
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
74
|
r.push(`${o}${v}variants: [`);
|
|
75
|
-
for (const i of
|
|
75
|
+
for (const i of t.variants)
|
|
76
76
|
r.push(
|
|
77
|
-
`${o}${v}${v}{ title: ${
|
|
77
|
+
`${o}${v}${v}{ title: ${N(i.title)}, query: ${N(i.query)} },`
|
|
78
78
|
);
|
|
79
79
|
r.push(`${o}${v}],`);
|
|
80
80
|
}
|
|
81
|
-
if (
|
|
81
|
+
if (t.children?.length) {
|
|
82
82
|
r.push(`${o}${v}children: [`);
|
|
83
|
-
for (const i of
|
|
83
|
+
for (const i of t.children) r.push(...tt(i, e + 2, n));
|
|
84
84
|
r.push(`${o}${v}],`);
|
|
85
85
|
}
|
|
86
86
|
return r.push(`${o}},`), r;
|
|
87
87
|
}
|
|
88
|
-
function G(
|
|
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 r of
|
|
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
96
|
return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
|
|
97
97
|
`);
|
|
98
98
|
}
|
|
99
|
-
function
|
|
100
|
-
return
|
|
99
|
+
function P(t) {
|
|
100
|
+
return t === "/" ? [] : t.replace(/^\//, "").split("/");
|
|
101
101
|
}
|
|
102
|
-
function
|
|
103
|
-
return
|
|
102
|
+
function St(t, e) {
|
|
103
|
+
return t.length >= e.length ? !1 : t.every((n, o) => n === e[o]);
|
|
104
104
|
}
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
return
|
|
109
|
-
}),
|
|
110
|
-
for (const r of
|
|
111
|
-
const s =
|
|
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
112
|
path: r.routePath,
|
|
113
113
|
title: r.title ?? s.at(-1) ?? "/"
|
|
114
114
|
};
|
|
115
115
|
let i;
|
|
116
|
-
for (const
|
|
117
|
-
|
|
118
|
-
i ? (i.page.children ??= []).push(
|
|
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
119
|
}
|
|
120
|
-
return
|
|
120
|
+
return n;
|
|
121
121
|
}
|
|
122
|
-
function U(
|
|
123
|
-
const
|
|
122
|
+
function U(t, e) {
|
|
123
|
+
const n = P(t), o = P(e);
|
|
124
124
|
for (let r = 0; r < o.length; r++) {
|
|
125
125
|
const s = o[r];
|
|
126
126
|
if (/^\[\[\.\.\..+\]\]$/.test(s))
|
|
127
|
-
return r <=
|
|
127
|
+
return r <= n.length;
|
|
128
128
|
if (/^\[\.\.\..+\]$/.test(s))
|
|
129
|
-
return r <
|
|
130
|
-
if (r >=
|
|
131
|
-
if (!/^\[.+\]$/.test(s) && 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
|
-
(
|
|
147
|
-
),
|
|
148
|
-
(
|
|
149
|
-
),
|
|
150
|
-
(d,
|
|
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 d of
|
|
153
|
-
const
|
|
154
|
-
let
|
|
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
155
|
for (const S of g) {
|
|
156
|
-
if (
|
|
157
|
-
const
|
|
158
|
-
|
|
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
|
|
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: r, segments: s } of
|
|
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
|
|
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
|
|
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 r = o.name.replace(
|
|
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
|
|
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
|
// 아래 예시를 참고해 앱의 화면 구조를 직접 작성해주세요.
|
|
@@ -258,41 +258,41 @@ export default defineIA({
|
|
|
258
258
|
})
|
|
259
259
|
`;
|
|
260
260
|
class m extends Error {
|
|
261
|
-
constructor(
|
|
262
|
-
super(`${
|
|
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
270
|
throw new m("INVALID_FIGMA_URL", "URL 형식이 아니에요");
|
|
271
271
|
}
|
|
272
|
-
if (!/(^|\.)figma\.com$/.test(
|
|
272
|
+
if (!/(^|\.)figma\.com$/.test(e.hostname))
|
|
273
273
|
throw new m("INVALID_FIGMA_URL", "figma.com 링크가 아니에요");
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
const o =
|
|
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
278
|
throw new m("INVALID_FIGMA_URL", "node-id가 없어요. Figma에서 프레임 우클릭 → Copy link로 복사해주세요");
|
|
279
|
-
return { fileKey:
|
|
279
|
+
return { fileKey: n[1], nodeId: o.replace(/-/g, ":") };
|
|
280
280
|
}
|
|
281
|
-
function
|
|
282
|
-
return
|
|
281
|
+
function L(t) {
|
|
282
|
+
return h(t, ".pubuilder", "credentials.json");
|
|
283
283
|
}
|
|
284
|
-
function
|
|
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 =
|
|
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
296
|
`).find((r) => r.trim().startsWith("FIGMA_TOKEN="));
|
|
297
297
|
if (o) {
|
|
298
298
|
const r = o.slice(o.indexOf("=") + 1).trim().replace(/^["']|["']$/g, "");
|
|
@@ -301,75 +301,77 @@ function K(n) {
|
|
|
301
301
|
}
|
|
302
302
|
return null;
|
|
303
303
|
}
|
|
304
|
-
function
|
|
305
|
-
j(
|
|
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 r = { "X-Figma-Token":
|
|
315
|
-
`https://api.figma.com/v1/files/${
|
|
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
316
|
{ headers: r }
|
|
317
317
|
);
|
|
318
318
|
if (s.status === 403) throw new m("FIGMA_API_ERROR", "Figma 토큰이 유효하지 않아요. 스킬함 설정에서 토큰을 확인해주세요", 403);
|
|
319
319
|
if (!s.ok) throw new m("FIGMA_API_ERROR", `Figma API 오류 (${s.status})`, 502);
|
|
320
|
-
const i = (await s.json()).nodes[
|
|
320
|
+
const i = (await s.json()).nodes[t.nodeId]?.document;
|
|
321
321
|
if (!i) throw new m("FIGMA_API_ERROR", "해당 노드를 찾지 못했어요. URL의 node-id를 확인해주세요", 404);
|
|
322
|
-
const
|
|
323
|
-
`https://api.figma.com/v1/images/${
|
|
322
|
+
const a = await o(
|
|
323
|
+
`https://api.figma.com/v1/images/${t.fileKey}?ids=${encodeURIComponent(t.nodeId)}&format=png&scale=2`,
|
|
324
324
|
{ headers: r }
|
|
325
325
|
);
|
|
326
|
-
if (!
|
|
327
|
-
const g = (await
|
|
326
|
+
if (!a.ok) throw new m("FIGMA_API_ERROR", `이미지 export 실패 (${a.status})`, 502);
|
|
327
|
+
const g = (await a.json()).images[t.nodeId];
|
|
328
328
|
if (!g) throw new m("FIGMA_API_ERROR", "이미지 export URL이 비어 있어요", 502);
|
|
329
|
-
const
|
|
330
|
-
j(
|
|
331
|
-
const d =
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
function
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
337
|
-
const
|
|
338
|
-
return { name:
|
|
339
|
-
}
|
|
340
|
-
class
|
|
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
|
-
j(
|
|
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 r of
|
|
365
|
+
for (const r of k(e, { withFileTypes: !0 })) {
|
|
364
366
|
if (!r.isDirectory()) continue;
|
|
365
|
-
const s =
|
|
366
|
-
if (!
|
|
367
|
-
const
|
|
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:
|
|
371
|
+
id: `${n}:${r.name}`,
|
|
372
|
+
name: c.name ?? r.name,
|
|
373
|
+
description: c.description ?? "",
|
|
374
|
+
source: n,
|
|
373
375
|
path: s
|
|
374
376
|
});
|
|
375
377
|
}
|
|
@@ -377,122 +379,204 @@ class Rt {
|
|
|
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((r) => r.id ===
|
|
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 =
|
|
401
|
+
upload(e, n) {
|
|
402
|
+
const o = W(n), r = (o.name ?? e.replace(/\.md$/i, "")).trim();
|
|
400
403
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(r))
|
|
401
404
|
throw new m("INVALID_NAME", `스킬 이름이 유효하지 않아요: ${r}`, 400);
|
|
402
|
-
const s =
|
|
403
|
-
return j(s, { recursive: !0 }),
|
|
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((s) => s.id ===
|
|
409
|
-
const
|
|
410
|
-
|
|
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 });
|
|
411
414
|
const r = this.loadSettings();
|
|
412
|
-
delete r.skills[
|
|
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
|
|
|
429
493
|
---
|
|
430
494
|
|
|
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→프로젝트 토큰 매핑 힌트):
|
|
460
524
|
${r}
|
|
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
|
|
469
|
-
let t;
|
|
534
|
+
function Bt(t) {
|
|
470
535
|
try {
|
|
471
|
-
t
|
|
536
|
+
return Q("git", ["stash", "create"], { cwd: t, encoding: "utf8", stdio: "pipe" }).trim() || "HEAD";
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function st(t, e, n) {
|
|
542
|
+
try {
|
|
543
|
+
return Q("git", ["show", `${e}:${n}`], { cwd: t, encoding: "utf8", stdio: "pipe" });
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
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);
|
|
551
|
+
}
|
|
552
|
+
function Jt(t) {
|
|
553
|
+
let e;
|
|
554
|
+
try {
|
|
555
|
+
e = JSON.parse(t);
|
|
472
556
|
} catch {
|
|
473
557
|
return [];
|
|
474
558
|
}
|
|
475
|
-
if (
|
|
559
|
+
if (e.type === "result")
|
|
476
560
|
return [
|
|
477
|
-
|
|
561
|
+
e.subtype === "success" ? { type: "done", text: String(e.result ?? "완료") } : { type: "error", text: String(e.result ?? e.subtype ?? "실패") }
|
|
478
562
|
];
|
|
479
|
-
if (
|
|
480
|
-
const
|
|
481
|
-
for (const o of
|
|
482
|
-
if (o.type === "text" && o.text &&
|
|
483
|
-
const r = o.input?.file_path ?? o.input?.command ?? "";
|
|
484
|
-
|
|
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 } : {} });
|
|
485
569
|
}
|
|
486
|
-
return
|
|
570
|
+
return n;
|
|
487
571
|
}
|
|
488
572
|
return [];
|
|
489
573
|
}
|
|
490
|
-
const
|
|
491
|
-
run({ prompt:
|
|
574
|
+
const X = 600 * 1e3, Kt = {
|
|
575
|
+
run({ prompt: t, cwd: e, onEvent: n, signal: o }) {
|
|
492
576
|
return new Promise((r) => {
|
|
493
|
-
const s =
|
|
577
|
+
const s = q("claude", [
|
|
494
578
|
"-p",
|
|
495
|
-
|
|
579
|
+
t,
|
|
496
580
|
"--output-format",
|
|
497
581
|
"stream-json",
|
|
498
582
|
"--verbose",
|
|
@@ -504,339 +588,433 @@ const z = 600 * 1e3, kt = {
|
|
|
504
588
|
// 설정을 로드하면 개발자 머신의 관대한 allow 규칙이 화이트리스트를 무력화한다 (e2e에서 확인)
|
|
505
589
|
"--setting-sources",
|
|
506
590
|
""
|
|
507
|
-
], { cwd:
|
|
508
|
-
let
|
|
509
|
-
const i = (
|
|
510
|
-
|
|
511
|
-
},
|
|
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 = () => {
|
|
512
596
|
s.kill("SIGKILL"), i({ type: "error", text: "에이전트가 10분간 응답이 없어 중단했어요" });
|
|
513
597
|
};
|
|
514
|
-
let
|
|
598
|
+
let f = setTimeout(a, X);
|
|
515
599
|
const g = () => {
|
|
516
|
-
|
|
600
|
+
c || (clearTimeout(f), f = setTimeout(a, X));
|
|
517
601
|
};
|
|
518
602
|
o?.addEventListener("abort", () => {
|
|
519
603
|
s.kill("SIGKILL"), i({ type: "error", text: "사용자가 중단했어요" });
|
|
520
|
-
}), s.on("error", (
|
|
604
|
+
}), s.on("error", (l) => {
|
|
521
605
|
i({
|
|
522
606
|
type: "error",
|
|
523
|
-
text:
|
|
607
|
+
text: l.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${l.message}`
|
|
524
608
|
});
|
|
525
609
|
});
|
|
526
|
-
const
|
|
527
|
-
s.stderr.on("data", (
|
|
610
|
+
const b = [];
|
|
611
|
+
s.stderr.on("data", (l) => b.push(String(l)));
|
|
528
612
|
let d = !1;
|
|
529
|
-
|
|
613
|
+
yt({ input: s.stdout }).on("line", (l) => {
|
|
530
614
|
g();
|
|
531
|
-
const
|
|
532
|
-
for (const
|
|
533
|
-
|
|
534
|
-
}), s.on("close", (
|
|
535
|
-
if (
|
|
536
|
-
const
|
|
537
|
-
i({ type: "error", text:
|
|
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) {
|
|
620
|
+
const u = b.join("").trim();
|
|
621
|
+
i({ type: "error", text: u || `claude가 코드 ${l}로 종료됐어요. claude login 상태를 확인해주세요` });
|
|
538
622
|
} else
|
|
539
623
|
i();
|
|
540
624
|
});
|
|
541
625
|
});
|
|
542
626
|
}
|
|
543
|
-
},
|
|
544
|
-
class
|
|
545
|
-
constructor(
|
|
546
|
-
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) => {
|
|
631
|
+
try {
|
|
632
|
+
return E(h(o, r), "utf8");
|
|
633
|
+
} catch {
|
|
634
|
+
return "";
|
|
635
|
+
}
|
|
636
|
+
}), this.writeFile = n.writeFile ?? ((o, r, s) => A(h(o, r), s));
|
|
547
637
|
}
|
|
548
638
|
jobs = /* @__PURE__ */ new Map();
|
|
549
|
-
active = null;
|
|
550
639
|
seq = 0;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
640
|
+
capture;
|
|
641
|
+
readBaseline;
|
|
642
|
+
restoreFile;
|
|
643
|
+
readCurrent;
|
|
644
|
+
writeFile;
|
|
645
|
+
/** suspend 시 파일별 baseline ref를 보관 → resolve의 revert에서 사용 */
|
|
646
|
+
pendingBaseline = /* @__PURE__ */ new Map();
|
|
647
|
+
activeCount() {
|
|
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 = {
|
|
656
|
+
id: o,
|
|
657
|
+
seq: n,
|
|
658
|
+
events: [],
|
|
659
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
660
|
+
controller: r,
|
|
661
|
+
done: Promise.resolve(),
|
|
662
|
+
finished: !1,
|
|
663
|
+
baselineRef: this.capture(e.cwd),
|
|
664
|
+
touchedFiles: /* @__PURE__ */ new Set()
|
|
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);
|
|
669
|
+
}
|
|
670
|
+
s.events.push(i);
|
|
671
|
+
for (const a of s.listeners) a(i);
|
|
556
672
|
};
|
|
557
|
-
return
|
|
558
|
-
|
|
559
|
-
}), this.jobs.set(
|
|
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;
|
|
560
676
|
}
|
|
561
677
|
/** finished job이 상한을 넘으면 가장 오래된 것부터 제거 (active job은 finished=false라 대상에서 제외) */
|
|
562
678
|
evictFinishedJobs() {
|
|
563
|
-
const
|
|
564
|
-
for (;
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
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);
|
|
568
684
|
}
|
|
569
685
|
}
|
|
570
686
|
/** SSE 핸들러가 헤더 전송 전에 job 존재를 확인할 때 사용 (subscribe는 replay 부작용이 있어 분리) */
|
|
571
|
-
has(
|
|
572
|
-
return this.jobs.has(
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
687
|
+
has(e) {
|
|
688
|
+
return this.jobs.has(e);
|
|
689
|
+
}
|
|
690
|
+
/** job이 편집한 파일 목록 (상대경로, 정렬) */
|
|
691
|
+
touchedFiles(e) {
|
|
692
|
+
const n = this.jobs.get(e);
|
|
693
|
+
return n ? [...n.touchedFiles].sort() : [];
|
|
694
|
+
}
|
|
695
|
+
/** job 시작 시점 baseline ref */
|
|
696
|
+
baselineRef(e) {
|
|
697
|
+
return this.jobs.get(e)?.baselineRef ?? null;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* 진행 중(running)인 job을 전부 abort하고, 그들이 편집한 파일을 분류한다.
|
|
701
|
+
* - 자동 롤백: running job만 건드린 파일 → baseline으로 즉시 복원
|
|
702
|
+
* - 충돌: 완료(finished)된 job도 건드린 파일 → 사용자 해결 대상으로 반환
|
|
703
|
+
* 파일별 baseline은 그 파일을 건드린 running job 중 가장 이른(seq 최소) job의 것.
|
|
704
|
+
*/
|
|
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();
|
|
708
|
+
const o = /* @__PURE__ */ new Map();
|
|
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 = [];
|
|
717
|
+
this.pendingBaseline.clear();
|
|
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 };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* 충돌 파일에 대한 사용자 결정을 적용한다.
|
|
724
|
+
* revert → baseline 복원, edit → content로 덮어쓰기, keep → 그대로 둔다.
|
|
725
|
+
*/
|
|
726
|
+
applyResolutions(e, n) {
|
|
727
|
+
for (const o of n)
|
|
728
|
+
if (o.action === "revert") {
|
|
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 ?? "");
|
|
732
|
+
this.pendingBaseline.clear();
|
|
733
|
+
}
|
|
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);
|
|
739
|
+
}
|
|
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();
|
|
744
|
+
}
|
|
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;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
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) => {
|
|
594
754
|
const o = [];
|
|
595
755
|
let r = 0;
|
|
596
|
-
|
|
597
|
-
if (r += s.length, r >
|
|
598
|
-
|
|
756
|
+
t.on("data", (s) => {
|
|
757
|
+
if (r += s.length, r > Xt) {
|
|
758
|
+
n(new m("PAYLOAD_TOO_LARGE", "요청 본문이 너무 커요 (1MB 초과)", 413)), t.destroy();
|
|
599
759
|
return;
|
|
600
760
|
}
|
|
601
761
|
o.push(s);
|
|
602
|
-
}),
|
|
762
|
+
}), t.on("end", () => {
|
|
603
763
|
if (o.length === 0) {
|
|
604
|
-
|
|
764
|
+
e(void 0);
|
|
605
765
|
return;
|
|
606
766
|
}
|
|
607
767
|
try {
|
|
608
|
-
|
|
768
|
+
e(JSON.parse(Buffer.concat(o).toString("utf8")));
|
|
609
769
|
} catch {
|
|
610
|
-
|
|
770
|
+
n(new m("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
|
|
611
771
|
}
|
|
612
|
-
}),
|
|
772
|
+
}), t.on("error", n);
|
|
613
773
|
});
|
|
614
774
|
}
|
|
615
|
-
function
|
|
616
|
-
const o = JSON.stringify(
|
|
617
|
-
|
|
775
|
+
function R(t, e, n) {
|
|
776
|
+
const o = JSON.stringify(n);
|
|
777
|
+
t.writeHead(e, { "content-type": "application/json" }), t.end(o);
|
|
618
778
|
}
|
|
619
|
-
async function
|
|
620
|
-
const
|
|
621
|
-
async function i(
|
|
622
|
-
const
|
|
623
|
-
if (typeof
|
|
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")
|
|
624
784
|
throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
|
|
625
|
-
const x =
|
|
785
|
+
const x = V({ projectRoot: e, homeDir: n, env: process.env });
|
|
626
786
|
if (!x) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
|
|
627
|
-
const { nodeJson:
|
|
628
|
-
selection:
|
|
787
|
+
const { nodeJson: F, imagePath: $ } = await Lt(T, x, h(e, ".pubuilder/cache"), r), O = Ut({
|
|
788
|
+
selection: p,
|
|
629
789
|
outerHTML: S ?? "",
|
|
630
|
-
figma: { url: String(
|
|
790
|
+
figma: { url: String(w), nodeJson: F, imagePath: $ },
|
|
631
791
|
skills: s.enabledSkillContents(),
|
|
632
|
-
instruction: typeof
|
|
792
|
+
instruction: typeof y == "string" ? y.trim() : void 0
|
|
633
793
|
});
|
|
634
|
-
return { status: 202, body: { jobId:
|
|
794
|
+
return { status: 202, body: { jobId: c.start({ prompt: O, cwd: e }) } };
|
|
635
795
|
}
|
|
636
|
-
function
|
|
637
|
-
if (!
|
|
638
|
-
|
|
796
|
+
function a(l, u, p) {
|
|
797
|
+
if (!c.has(p)) throw new m("NOT_FOUND", `job 없음: ${p}`, 404);
|
|
798
|
+
u.writeHead(200, {
|
|
639
799
|
"content-type": "text/event-stream",
|
|
640
800
|
"cache-control": "no-cache",
|
|
641
801
|
connection: "keep-alive"
|
|
642
802
|
});
|
|
643
803
|
let S = () => {
|
|
644
804
|
};
|
|
645
|
-
const
|
|
646
|
-
|
|
805
|
+
const w = (y) => {
|
|
806
|
+
u.write(`data: ${JSON.stringify(y)}
|
|
647
807
|
|
|
648
|
-
`), (
|
|
808
|
+
`), (y.type === "done" || y.type === "error") && (S(), u.end());
|
|
649
809
|
};
|
|
650
|
-
S =
|
|
810
|
+
S = c.subscribe(p, w), l.on("close", S);
|
|
651
811
|
}
|
|
652
|
-
const
|
|
812
|
+
const f = dt((l, u) => {
|
|
653
813
|
(async () => {
|
|
654
814
|
try {
|
|
655
|
-
const
|
|
656
|
-
if (
|
|
657
|
-
|
|
815
|
+
const p = l.headers.origin;
|
|
816
|
+
if (p && !Zt.test(p)) {
|
|
817
|
+
R(u, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
|
|
658
818
|
return;
|
|
659
819
|
}
|
|
660
|
-
if (
|
|
661
|
-
|
|
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") {
|
|
821
|
+
u.writeHead(204), u.end();
|
|
662
822
|
return;
|
|
663
823
|
}
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
666
|
-
|
|
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 });
|
|
667
827
|
return;
|
|
668
828
|
}
|
|
669
|
-
if (
|
|
670
|
-
|
|
829
|
+
if (y === "GET" && w === "/api/skills") {
|
|
830
|
+
R(u, 200, s.list());
|
|
671
831
|
return;
|
|
672
832
|
}
|
|
673
|
-
if (
|
|
674
|
-
const
|
|
675
|
-
|
|
833
|
+
if (y === "POST" && w === "/api/skills") {
|
|
834
|
+
const $ = await D(l), O = s.upload($.filename, $.content);
|
|
835
|
+
R(u, 201, O);
|
|
676
836
|
return;
|
|
677
837
|
}
|
|
678
|
-
const
|
|
679
|
-
if (
|
|
680
|
-
const
|
|
681
|
-
s.setEnabled(
|
|
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();
|
|
682
842
|
return;
|
|
683
843
|
}
|
|
684
|
-
if (
|
|
685
|
-
const
|
|
686
|
-
s.remove(
|
|
844
|
+
if (T && y === "DELETE") {
|
|
845
|
+
const $ = decodeURIComponent(T[1]);
|
|
846
|
+
s.remove($), u.writeHead(204), u.end();
|
|
687
847
|
return;
|
|
688
848
|
}
|
|
689
|
-
if (
|
|
690
|
-
const
|
|
691
|
-
|
|
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 });
|
|
692
852
|
return;
|
|
693
853
|
}
|
|
694
|
-
if (
|
|
695
|
-
const
|
|
696
|
-
|
|
854
|
+
if (y === "PUT" && w === "/api/settings/figma-token") {
|
|
855
|
+
const $ = await D(l);
|
|
856
|
+
jt(n, $.token), u.writeHead(204), u.end();
|
|
697
857
|
return;
|
|
698
858
|
}
|
|
699
|
-
if (
|
|
700
|
-
|
|
859
|
+
if (y === "DELETE" && w === "/api/settings/figma-token") {
|
|
860
|
+
kt(n), u.writeHead(204), u.end();
|
|
701
861
|
return;
|
|
702
862
|
}
|
|
703
|
-
if (
|
|
704
|
-
const { status:
|
|
705
|
-
|
|
863
|
+
if (y === "POST" && w === "/api/publish") {
|
|
864
|
+
const { status: $, body: O } = await i(l);
|
|
865
|
+
R(u, $, O);
|
|
706
866
|
return;
|
|
707
867
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
h(c, l, x[1]);
|
|
868
|
+
if (y === "POST" && w === "/api/publish/suspend") {
|
|
869
|
+
R(u, 200, c.suspendRunning(e));
|
|
711
870
|
return;
|
|
712
871
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
872
|
+
if (y === "POST" && w === "/api/publish/suspend/resolve") {
|
|
873
|
+
const $ = await D(l);
|
|
874
|
+
if (!Array.isArray($?.resolutions))
|
|
875
|
+
throw new m("INVALID_JSON", "resolutions 배열이 필요해요", 400);
|
|
876
|
+
c.applyResolutions(e, $.resolutions), u.writeHead(204), u.end();
|
|
716
877
|
return;
|
|
717
878
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
I(l, u.status, { code: u.code, message: u.message });
|
|
879
|
+
const x = w.match(/^\/api\/publish\/([^/]+)\/events$/);
|
|
880
|
+
if (x && y === "GET") {
|
|
881
|
+
a(l, u, x[1]);
|
|
722
882
|
return;
|
|
723
883
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
884
|
+
const F = w.match(/^\/api\/publish\/([^/]+)$/);
|
|
885
|
+
if (F && y === "DELETE") {
|
|
886
|
+
c.cancel(F[1]), u.writeHead(204), u.end();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
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 });
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const S = p instanceof Error ? p.message : String(p);
|
|
896
|
+
R(u, 500, { code: "INTERNAL", message: S });
|
|
726
897
|
}
|
|
727
898
|
})();
|
|
728
|
-
}), g =
|
|
729
|
-
await new Promise((
|
|
730
|
-
const
|
|
899
|
+
}), g = t.port ?? zt;
|
|
900
|
+
await new Promise((l) => f.listen(g, "127.0.0.1", l));
|
|
901
|
+
const b = f.address();
|
|
731
902
|
return {
|
|
732
|
-
port: typeof
|
|
903
|
+
port: typeof b == "object" && b ? b.port : g,
|
|
733
904
|
close() {
|
|
734
|
-
|
|
905
|
+
f.close();
|
|
735
906
|
}
|
|
736
907
|
};
|
|
737
908
|
}
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
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
|
+
}
|
|
741
915
|
}
|
|
742
|
-
function
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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;
|
|
919
|
+
}
|
|
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)
|
|
747
925
|
throw new Error("--port 값이 유효하지 않아요 (0-65535)");
|
|
748
|
-
return
|
|
926
|
+
return n;
|
|
749
927
|
}
|
|
750
|
-
async function
|
|
751
|
-
const
|
|
928
|
+
async function qt(t, e) {
|
|
929
|
+
const n = e.indexOf("--"), o = n === -1 ? [] : e.slice(n + 1);
|
|
752
930
|
if (o.length === 0)
|
|
753
931
|
return console.error("사용법: pubuilder dev [--port N] -- <dev 명령> (예: pubuilder dev -- next dev)"), 1;
|
|
754
|
-
const r = await
|
|
932
|
+
const r = await at(t, { port: ct(e.slice(0, n === -1 ? void 0 : n)) });
|
|
755
933
|
return new Promise((s) => {
|
|
756
|
-
const
|
|
757
|
-
process.on("SIGINT", i), process.on("SIGTERM",
|
|
758
|
-
const
|
|
759
|
-
process.off("SIGINT", i), process.off("SIGTERM",
|
|
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();
|
|
760
938
|
};
|
|
761
|
-
|
|
762
|
-
console.error(`[pubuilder] 명령 실행 실패: ${g.message}`),
|
|
763
|
-
}),
|
|
764
|
-
|
|
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);
|
|
765
943
|
});
|
|
766
944
|
});
|
|
767
945
|
}
|
|
768
|
-
const
|
|
769
|
-
function
|
|
770
|
-
const
|
|
771
|
-
if (!
|
|
946
|
+
const te = [Ot, xt];
|
|
947
|
+
function ee(t) {
|
|
948
|
+
const e = h(t, "package.json");
|
|
949
|
+
if (!I(e)) return !1;
|
|
772
950
|
try {
|
|
773
|
-
const
|
|
774
|
-
return !!(
|
|
951
|
+
const n = JSON.parse(E(e, "utf8"));
|
|
952
|
+
return !!(n.dependencies?.next ?? n.devDependencies?.next);
|
|
775
953
|
} catch {
|
|
776
954
|
return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
|
|
777
955
|
}
|
|
778
956
|
}
|
|
779
|
-
function
|
|
780
|
-
if (!
|
|
781
|
-
const
|
|
782
|
-
if (
|
|
783
|
-
const
|
|
784
|
-
for (const r of
|
|
785
|
-
for (const s of r.scan(
|
|
786
|
-
const
|
|
787
|
-
if (
|
|
788
|
-
console.warn(`[pubuilder] path 충돌: ${s.routePath} (${
|
|
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} 무시)`);
|
|
789
967
|
continue;
|
|
790
968
|
}
|
|
791
|
-
|
|
969
|
+
n.set(s.routePath, r.name), o.push(s);
|
|
792
970
|
}
|
|
793
971
|
return o;
|
|
794
972
|
}
|
|
795
|
-
async function
|
|
796
|
-
const
|
|
797
|
-
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))
|
|
798
976
|
throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
|
|
799
|
-
return
|
|
800
|
-
}
|
|
801
|
-
async function
|
|
802
|
-
const
|
|
803
|
-
if (
|
|
804
|
-
return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !
|
|
805
|
-
if (
|
|
806
|
-
const i = await
|
|
807
|
-
|
|
808
|
-
for (const
|
|
809
|
-
for (const
|
|
810
|
-
return
|
|
811
|
-
}
|
|
812
|
-
const s = { pages:
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
return
|
|
816
|
-
}
|
|
817
|
-
async function
|
|
818
|
-
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;
|
|
819
997
|
try {
|
|
820
|
-
return
|
|
821
|
-
})) :
|
|
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);
|
|
822
1000
|
} catch (o) {
|
|
823
1001
|
return console.error(`[pubuilder] 실패: ${o instanceof Error ? o.message : String(o)}`), 1;
|
|
824
1002
|
}
|
|
825
1003
|
}
|
|
826
|
-
function
|
|
827
|
-
const
|
|
828
|
-
if (!
|
|
1004
|
+
function se() {
|
|
1005
|
+
const t = process.argv[1];
|
|
1006
|
+
if (!t) return !1;
|
|
829
1007
|
try {
|
|
830
|
-
return
|
|
1008
|
+
return lt(t) === Y(import.meta.url);
|
|
831
1009
|
} catch {
|
|
832
1010
|
return !1;
|
|
833
1011
|
}
|
|
834
1012
|
}
|
|
835
|
-
|
|
836
|
-
(
|
|
1013
|
+
se() && ie(process.argv.slice(2)).then(
|
|
1014
|
+
(t) => process.exit(t),
|
|
837
1015
|
() => process.exit(1)
|
|
838
1016
|
);
|
|
839
1017
|
export {
|
|
840
|
-
|
|
841
|
-
|
|
1018
|
+
ie as main,
|
|
1019
|
+
re as runScan
|
|
842
1020
|
};
|