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