pubuilder 0.5.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 +90 -0
- package/dist/api.d.ts +20 -0
- package/dist/cli.js +492 -379
- package/dist/components/SuspendButton.d.ts +6 -0
- package/dist/components/SuspendConflictPanel.d.ts +5 -0
- package/dist/index.js +931 -545
- 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/prompt.d.ts +2 -0
- package/dist/server/snapshot.d.ts +10 -0
- package/dist/store.d.ts +18 -0
- package/package.json +3 -2
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
60
|
const P = " ";
|
|
61
|
-
function
|
|
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 = P.repeat(t),
|
|
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}${P}${P}{ title: ${
|
|
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(`${P}viewport: ${
|
|
95
|
-
for (const
|
|
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
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 (
|
|
157
|
-
const
|
|
158
|
-
|
|
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
|
+
const y = S.segments;
|
|
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 (v(
|
|
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 (v(
|
|
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 =
|
|
284
|
+
function V(n) {
|
|
285
|
+
const t = L(n.homeDir);
|
|
286
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 =
|
|
293
|
+
const e = d(n.projectRoot, ".env");
|
|
294
294
|
if (v(e)) {
|
|
295
|
-
const o =
|
|
296
|
-
`).find((
|
|
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
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
361
|
if (!v(t)) return [];
|
|
362
362
|
const o = [];
|
|
363
|
-
for (const
|
|
364
|
-
if (!
|
|
365
|
-
const
|
|
366
|
-
if (!v(
|
|
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,53 @@ 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
|
+
# 사용자의 후속 답변
|
|
436
|
+
|
|
437
|
+
${n.instruction}
|
|
438
|
+
|
|
439
|
+
이전 실행에서 이미 변경된 현재 작업 트리를 확인하고, 위 답변에 따라 중단된 작업을 이어서 완료하세요. 같은 선택지를 다시 질문하지 마세요.
|
|
440
|
+
` : "";
|
|
435
441
|
return `이 프로젝트의 특정 UI 블록을 Figma 시안대로 수정하는 퍼블리싱 작업입니다.
|
|
436
442
|
|
|
437
443
|
# 대상 블록
|
|
@@ -451,6 +457,7 @@ ${n.outerHTML}
|
|
|
451
457
|
${JSON.stringify(e.nodeJson, null, 2)}
|
|
452
458
|
\`\`\`
|
|
453
459
|
${o}
|
|
460
|
+
${s}
|
|
454
461
|
# 작업 순서
|
|
455
462
|
1. 위 라우트와 outerHTML 스냅샷을 단서로, 이 블록을 렌더링하는 소스 컴포넌트 파일을 찾으세요.
|
|
456
463
|
2. Figma 렌더 이미지를 Read 도구로 열어 시안을 육안으로 확인하세요. 노드 JSON의 수치(색상/간격/타이포)를 우선 근거로 쓰되, JSON에만 있고 이미지에 보이지 않는 요소는 구현하지 마세요.
|
|
@@ -458,7 +465,25 @@ ${o}
|
|
|
458
465
|
4. 수정 후 타입체크(pnpm type-check)가 있으면 실행해 통과를 확인하세요.
|
|
459
466
|
5. 마지막 메시지에 수정한 파일 경로를 나열하세요.`;
|
|
460
467
|
}
|
|
461
|
-
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) {
|
|
462
487
|
let t;
|
|
463
488
|
try {
|
|
464
489
|
t = JSON.parse(n);
|
|
@@ -473,17 +498,17 @@ function xt(n) {
|
|
|
473
498
|
const e = [];
|
|
474
499
|
for (const o of t.message?.content ?? [])
|
|
475
500
|
if (o.type === "text" && o.text && e.push({ type: "log", text: o.text }), o.type === "tool_use") {
|
|
476
|
-
const
|
|
477
|
-
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 } : {} });
|
|
478
503
|
}
|
|
479
504
|
return e;
|
|
480
505
|
}
|
|
481
506
|
return [];
|
|
482
507
|
}
|
|
483
|
-
const z = 600 * 1e3,
|
|
508
|
+
const z = 600 * 1e3, Gt = {
|
|
484
509
|
run({ prompt: n, cwd: t, onEvent: e, signal: o }) {
|
|
485
|
-
return new Promise((
|
|
486
|
-
const
|
|
510
|
+
return new Promise((s) => {
|
|
511
|
+
const i = Y("claude", [
|
|
487
512
|
"-p",
|
|
488
513
|
n,
|
|
489
514
|
"--output-format",
|
|
@@ -499,62 +524,94 @@ const z = 600 * 1e3, kt = {
|
|
|
499
524
|
""
|
|
500
525
|
], { cwd: t, env: process.env });
|
|
501
526
|
let a = !1;
|
|
502
|
-
const
|
|
503
|
-
a || (a = !0, clearTimeout(p),
|
|
504
|
-
},
|
|
505
|
-
|
|
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분간 응답이 없어 중단했어요" });
|
|
506
531
|
};
|
|
507
|
-
let p = setTimeout(
|
|
508
|
-
const
|
|
509
|
-
a || (clearTimeout(p), p = setTimeout(
|
|
532
|
+
let p = setTimeout(c, z);
|
|
533
|
+
const m = () => {
|
|
534
|
+
a || (clearTimeout(p), p = setTimeout(c, z));
|
|
510
535
|
};
|
|
511
536
|
o?.addEventListener("abort", () => {
|
|
512
|
-
|
|
513
|
-
}),
|
|
514
|
-
|
|
537
|
+
i.kill("SIGKILL"), r({ type: "error", text: "사용자가 중단했어요" });
|
|
538
|
+
}), i.on("error", (l) => {
|
|
539
|
+
r({
|
|
515
540
|
type: "error",
|
|
516
|
-
text:
|
|
541
|
+
text: l.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${l.message}`
|
|
517
542
|
});
|
|
518
543
|
});
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
let
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
for (const
|
|
526
|
-
e(
|
|
527
|
-
}),
|
|
528
|
-
if (
|
|
529
|
-
const
|
|
530
|
-
|
|
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 상태를 확인해주세요` });
|
|
531
556
|
} else
|
|
532
|
-
|
|
557
|
+
r();
|
|
533
558
|
});
|
|
534
559
|
});
|
|
535
560
|
}
|
|
536
|
-
},
|
|
537
|
-
class
|
|
538
|
-
constructor(t) {
|
|
539
|
-
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));
|
|
540
571
|
}
|
|
541
572
|
jobs = /* @__PURE__ */ new Map();
|
|
542
|
-
active = null;
|
|
543
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
|
+
}
|
|
544
586
|
start(t) {
|
|
545
|
-
if (this.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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);
|
|
549
606
|
};
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
}), 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;
|
|
553
610
|
}
|
|
554
611
|
/** finished job이 상한을 넘으면 가장 오래된 것부터 제거 (active job은 finished=false라 대상에서 제외) */
|
|
555
612
|
evictFinishedJobs() {
|
|
556
613
|
const t = [...this.jobs.entries()].filter(([, e]) => e.finished).map(([e]) => e);
|
|
557
|
-
for (; t.length >
|
|
614
|
+
for (; t.length > Ut; ) {
|
|
558
615
|
const e = t.shift();
|
|
559
616
|
if (e === void 0) break;
|
|
560
617
|
this.jobs.delete(e);
|
|
@@ -564,34 +621,78 @@ class jt {
|
|
|
564
621
|
has(t) {
|
|
565
622
|
return this.jobs.has(t);
|
|
566
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
|
+
}
|
|
567
668
|
subscribe(t, e) {
|
|
568
669
|
const o = this.jobs.get(t);
|
|
569
|
-
if (!o) throw new
|
|
570
|
-
for (const
|
|
670
|
+
if (!o) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
|
|
671
|
+
for (const s of o.events) e(s);
|
|
571
672
|
return o.listeners.add(e), () => o.listeners.delete(e);
|
|
572
673
|
}
|
|
573
674
|
cancel(t) {
|
|
574
675
|
const e = this.jobs.get(t);
|
|
575
|
-
if (!e) throw new
|
|
676
|
+
if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
|
|
576
677
|
e.controller.abort();
|
|
577
678
|
}
|
|
578
679
|
waitFor(t) {
|
|
579
680
|
const e = this.jobs.get(t);
|
|
580
|
-
if (!e) throw new
|
|
681
|
+
if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
|
|
581
682
|
return e.done;
|
|
582
683
|
}
|
|
583
684
|
}
|
|
584
|
-
const
|
|
585
|
-
function
|
|
685
|
+
const Jt = 4816, Bt = 1024 * 1024, Ht = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
|
686
|
+
function j(n) {
|
|
586
687
|
return new Promise((t, e) => {
|
|
587
688
|
const o = [];
|
|
588
|
-
let
|
|
589
|
-
n.on("data", (
|
|
590
|
-
if (
|
|
591
|
-
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();
|
|
592
693
|
return;
|
|
593
694
|
}
|
|
594
|
-
o.push(
|
|
695
|
+
o.push(i);
|
|
595
696
|
}), n.on("end", () => {
|
|
596
697
|
if (o.length === 0) {
|
|
597
698
|
t(void 0);
|
|
@@ -600,138 +701,150 @@ function _(n) {
|
|
|
600
701
|
try {
|
|
601
702
|
t(JSON.parse(Buffer.concat(o).toString("utf8")));
|
|
602
703
|
} catch {
|
|
603
|
-
e(new
|
|
704
|
+
e(new g("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
|
|
604
705
|
}
|
|
605
706
|
}), n.on("error", e);
|
|
606
707
|
});
|
|
607
708
|
}
|
|
608
|
-
function
|
|
709
|
+
function I(n, t, e) {
|
|
609
710
|
const o = JSON.stringify(e);
|
|
610
711
|
n.writeHead(t, { "content-type": "application/json" }), n.end(o);
|
|
611
712
|
}
|
|
612
|
-
async function
|
|
613
|
-
const t = n.projectRoot, e = n.homeDir ??
|
|
614
|
-
async function
|
|
615
|
-
const
|
|
616
|
-
if (typeof
|
|
617
|
-
throw new
|
|
618
|
-
const
|
|
619
|
-
if (!
|
|
620
|
-
const { nodeJson: x, imagePath:
|
|
621
|
-
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,
|
|
622
723
|
outerHTML: S ?? "",
|
|
623
|
-
figma: { url: String(
|
|
624
|
-
skills:
|
|
724
|
+
figma: { url: String(y), nodeJson: x, imagePath: $ },
|
|
725
|
+
skills: i.enabledSkillContents(),
|
|
726
|
+
instruction: typeof w == "string" ? w.trim() : void 0
|
|
625
727
|
});
|
|
626
|
-
return { status: 202, body: { jobId: a.start({ prompt:
|
|
728
|
+
return { status: 202, body: { jobId: a.start({ prompt: O, cwd: t }) } };
|
|
627
729
|
}
|
|
628
|
-
function
|
|
629
|
-
if (!a.has(
|
|
630
|
-
|
|
730
|
+
function c(l, u, f) {
|
|
731
|
+
if (!a.has(f)) throw new g("NOT_FOUND", `job 없음: ${f}`, 404);
|
|
732
|
+
u.writeHead(200, {
|
|
631
733
|
"content-type": "text/event-stream",
|
|
632
734
|
"cache-control": "no-cache",
|
|
633
735
|
connection: "keep-alive"
|
|
634
736
|
});
|
|
635
737
|
let S = () => {
|
|
636
738
|
};
|
|
637
|
-
const
|
|
638
|
-
|
|
739
|
+
const y = (w) => {
|
|
740
|
+
u.write(`data: ${JSON.stringify(w)}
|
|
639
741
|
|
|
640
|
-
`), (
|
|
742
|
+
`), (w.type === "done" || w.type === "error") && (S(), u.end());
|
|
641
743
|
};
|
|
642
|
-
S = a.subscribe(
|
|
744
|
+
S = a.subscribe(f, y), l.on("close", S);
|
|
643
745
|
}
|
|
644
|
-
const p =
|
|
746
|
+
const p = ut((l, u) => {
|
|
645
747
|
(async () => {
|
|
646
748
|
try {
|
|
647
|
-
const
|
|
648
|
-
if (
|
|
649
|
-
|
|
749
|
+
const f = l.headers.origin;
|
|
750
|
+
if (f && !Ht.test(f)) {
|
|
751
|
+
I(u, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
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();
|
|
650
756
|
return;
|
|
651
757
|
}
|
|
652
|
-
|
|
653
|
-
|
|
758
|
+
const y = new URL(l.url ?? "/", "http://localhost").pathname, w = l.method ?? "GET";
|
|
759
|
+
if (w === "GET" && y === "/api/health") {
|
|
760
|
+
I(u, 200, { ok: !0 });
|
|
654
761
|
return;
|
|
655
762
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
T(l, 200, { ok: !0 });
|
|
763
|
+
if (w === "GET" && y === "/api/skills") {
|
|
764
|
+
I(u, 200, i.list());
|
|
659
765
|
return;
|
|
660
766
|
}
|
|
661
|
-
if (
|
|
662
|
-
|
|
767
|
+
if (w === "POST" && y === "/api/skills") {
|
|
768
|
+
const $ = await j(l), O = i.upload($.filename, $.content);
|
|
769
|
+
I(u, 201, O);
|
|
663
770
|
return;
|
|
664
771
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
772
|
+
const E = y.match(/^\/api\/skills\/([^/]+)$/);
|
|
773
|
+
if (E && w === "PATCH") {
|
|
774
|
+
const $ = decodeURIComponent(E[1]), O = await j(l);
|
|
775
|
+
i.setEnabled($, O.enabled), u.writeHead(204), u.end();
|
|
668
776
|
return;
|
|
669
777
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
s.setEnabled(b, N.enabled), l.writeHead(204), l.end();
|
|
778
|
+
if (E && w === "DELETE") {
|
|
779
|
+
const $ = decodeURIComponent(E[1]);
|
|
780
|
+
i.remove($), u.writeHead(204), u.end();
|
|
674
781
|
return;
|
|
675
782
|
}
|
|
676
|
-
if (
|
|
677
|
-
const
|
|
678
|
-
|
|
783
|
+
if (w === "GET" && y === "/api/settings/figma-token") {
|
|
784
|
+
const $ = V({ projectRoot: t, homeDir: e, env: process.env });
|
|
785
|
+
I(u, 200, { configured: $ != null, masked: $ ? jt($) : null });
|
|
679
786
|
return;
|
|
680
787
|
}
|
|
681
|
-
if (
|
|
682
|
-
const
|
|
683
|
-
|
|
788
|
+
if (w === "PUT" && y === "/api/settings/figma-token") {
|
|
789
|
+
const $ = await j(l);
|
|
790
|
+
Nt(e, $.token), u.writeHead(204), u.end();
|
|
684
791
|
return;
|
|
685
792
|
}
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
Tt(e, b.token), l.writeHead(204), l.end();
|
|
793
|
+
if (w === "DELETE" && y === "/api/settings/figma-token") {
|
|
794
|
+
Ft(e), u.writeHead(204), u.end();
|
|
689
795
|
return;
|
|
690
796
|
}
|
|
691
|
-
if (
|
|
692
|
-
|
|
797
|
+
if (w === "POST" && y === "/api/publish") {
|
|
798
|
+
const { status: $, body: O } = await r(l);
|
|
799
|
+
I(u, $, O);
|
|
693
800
|
return;
|
|
694
801
|
}
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
T(l, b, N);
|
|
802
|
+
if (w === "POST" && y === "/api/publish/suspend") {
|
|
803
|
+
I(u, 200, a.suspendRunning(t));
|
|
698
804
|
return;
|
|
699
805
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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();
|
|
703
811
|
return;
|
|
704
812
|
}
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
707
|
-
|
|
813
|
+
const F = y.match(/^\/api\/publish\/([^/]+)\/events$/);
|
|
814
|
+
if (F && w === "GET") {
|
|
815
|
+
c(l, u, F[1]);
|
|
708
816
|
return;
|
|
709
817
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
T(l, u.status, { code: u.code, message: u.message });
|
|
818
|
+
const x = y.match(/^\/api\/publish\/([^/]+)$/);
|
|
819
|
+
if (x && w === "DELETE") {
|
|
820
|
+
a.cancel(x[1]), u.writeHead(204), u.end();
|
|
714
821
|
return;
|
|
715
822
|
}
|
|
716
|
-
|
|
717
|
-
|
|
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 });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const S = f instanceof Error ? f.message : String(f);
|
|
830
|
+
I(u, 500, { code: "INTERNAL", message: S });
|
|
718
831
|
}
|
|
719
832
|
})();
|
|
720
|
-
}),
|
|
721
|
-
await new Promise((
|
|
722
|
-
const
|
|
833
|
+
}), m = n.port ?? Jt;
|
|
834
|
+
await new Promise((l) => p.listen(m, "127.0.0.1", l));
|
|
835
|
+
const b = p.address();
|
|
723
836
|
return {
|
|
724
|
-
port: typeof
|
|
837
|
+
port: typeof b == "object" && b ? b.port : m,
|
|
725
838
|
close() {
|
|
726
839
|
p.close();
|
|
727
840
|
}
|
|
728
841
|
};
|
|
729
842
|
}
|
|
730
|
-
async function
|
|
731
|
-
const e = await
|
|
843
|
+
async function ot(n, t) {
|
|
844
|
+
const e = await Kt({ projectRoot: n, port: t.port ?? 4816 });
|
|
732
845
|
return console.log(`[pubuilder] 컴패니언 서버 실행 중: http://localhost:${e.port}`), console.log("[pubuilder] 미니맵의 퍼블리싱 패널이 이 서버에 연결됩니다. Ctrl+C로 종료."), e;
|
|
733
846
|
}
|
|
734
|
-
function
|
|
847
|
+
function st(n) {
|
|
735
848
|
const t = n.indexOf("--port");
|
|
736
849
|
if (t === -1) return;
|
|
737
850
|
const e = Number(n[t + 1]);
|
|
@@ -739,96 +852,96 @@ function et(n) {
|
|
|
739
852
|
throw new Error("--port 값이 유효하지 않아요 (0-65535)");
|
|
740
853
|
return e;
|
|
741
854
|
}
|
|
742
|
-
async function
|
|
855
|
+
async function Vt(n, t) {
|
|
743
856
|
const e = t.indexOf("--"), o = e === -1 ? [] : t.slice(e + 1);
|
|
744
857
|
if (o.length === 0)
|
|
745
858
|
return console.error("사용법: pubuilder dev [--port N] -- <dev 명령> (예: pubuilder dev -- next dev)"), 1;
|
|
746
|
-
const
|
|
747
|
-
return new Promise((
|
|
748
|
-
const a =
|
|
749
|
-
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);
|
|
750
863
|
const p = () => {
|
|
751
|
-
process.off("SIGINT",
|
|
864
|
+
process.off("SIGINT", r), process.off("SIGTERM", c), s.close();
|
|
752
865
|
};
|
|
753
|
-
a.on("error", (
|
|
754
|
-
console.error(`[pubuilder] 명령 실행 실패: ${
|
|
755
|
-
}), a.on("close", (
|
|
756
|
-
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);
|
|
757
870
|
});
|
|
758
871
|
});
|
|
759
872
|
}
|
|
760
|
-
const
|
|
761
|
-
function
|
|
762
|
-
const t =
|
|
873
|
+
const Wt = [Tt, Et];
|
|
874
|
+
function zt(n) {
|
|
875
|
+
const t = d(n, "package.json");
|
|
763
876
|
if (!v(t)) return !1;
|
|
764
877
|
try {
|
|
765
|
-
const e = JSON.parse(
|
|
878
|
+
const e = JSON.parse(A(t, "utf8"));
|
|
766
879
|
return !!(e.dependencies?.next ?? e.devDependencies?.next);
|
|
767
880
|
} catch {
|
|
768
881
|
return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
|
|
769
882
|
}
|
|
770
883
|
}
|
|
771
|
-
function
|
|
772
|
-
if (!
|
|
773
|
-
const t =
|
|
884
|
+
function Xt(n) {
|
|
885
|
+
if (!zt(n)) return null;
|
|
886
|
+
const t = Wt.filter((s) => s.detect(n));
|
|
774
887
|
if (t.length === 0) return null;
|
|
775
888
|
const e = /* @__PURE__ */ new Map(), o = [];
|
|
776
|
-
for (const
|
|
777
|
-
for (const
|
|
778
|
-
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);
|
|
779
892
|
if (a) {
|
|
780
|
-
console.warn(`[pubuilder] path 충돌: ${
|
|
893
|
+
console.warn(`[pubuilder] path 충돌: ${i.routePath} (${a} 우선, ${s.name} 무시)`);
|
|
781
894
|
continue;
|
|
782
895
|
}
|
|
783
|
-
e.set(
|
|
896
|
+
e.set(i.routePath, s.name), o.push(i);
|
|
784
897
|
}
|
|
785
898
|
return o;
|
|
786
899
|
}
|
|
787
|
-
async function
|
|
788
|
-
const e = await
|
|
900
|
+
async function Zt(n) {
|
|
901
|
+
const e = await lt(import.meta.url).import(n, { default: !0 });
|
|
789
902
|
if (!e || !Array.isArray(e.pages))
|
|
790
903
|
throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
|
|
791
904
|
return e;
|
|
792
905
|
}
|
|
793
|
-
async function
|
|
794
|
-
const e =
|
|
906
|
+
async function Yt(n, t) {
|
|
907
|
+
const e = Xt(n), o = d(n, "ia.config.ts");
|
|
795
908
|
if (e === null)
|
|
796
|
-
return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !v(o) && !t.dryRun && (
|
|
909
|
+
return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !v(o) && !t.dryRun && (T(o, At), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
|
|
797
910
|
if (v(o)) {
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
for (const
|
|
801
|
-
for (const
|
|
802
|
-
return p.length === 0 &&
|
|
803
|
-
}
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
const a = e.map((
|
|
807
|
-
return t.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (
|
|
808
|
-
}
|
|
809
|
-
async function
|
|
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) {
|
|
810
923
|
const [t, ...e] = n;
|
|
811
924
|
try {
|
|
812
|
-
return t === "scan" ? (await
|
|
813
|
-
})) : 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);
|
|
814
927
|
} catch (o) {
|
|
815
928
|
return console.error(`[pubuilder] 실패: ${o instanceof Error ? o.message : String(o)}`), 1;
|
|
816
929
|
}
|
|
817
930
|
}
|
|
818
|
-
function
|
|
931
|
+
function qt() {
|
|
819
932
|
const n = process.argv[1];
|
|
820
933
|
if (!n) return !1;
|
|
821
934
|
try {
|
|
822
|
-
return
|
|
935
|
+
return rt(n) === ct(import.meta.url);
|
|
823
936
|
} catch {
|
|
824
937
|
return !1;
|
|
825
938
|
}
|
|
826
939
|
}
|
|
827
|
-
|
|
940
|
+
qt() && Qt(process.argv.slice(2)).then(
|
|
828
941
|
(n) => process.exit(n),
|
|
829
942
|
() => process.exit(1)
|
|
830
943
|
);
|
|
831
944
|
export {
|
|
832
|
-
|
|
833
|
-
|
|
945
|
+
Qt as main,
|
|
946
|
+
Yt as runScan
|
|
834
947
|
};
|