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