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/dist/cli.js CHANGED
@@ -1,243 +1,243 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync as O, existsSync as v, readdirSync as D, mkdirSync as j, writeFileSync as R, rmSync as W, realpathSync as nt } from "node:fs";
3
- import { join as f } from "node:path";
4
- import { fileURLToPath as ot } from "node:url";
5
- import { createJiti as rt } from "jiti";
6
- import { createServer as st } from "node:http";
7
- import { homedir as it, constants as at } from "node:os";
8
- import { spawn as X } from "node:child_process";
9
- import { createInterface as ct } from "node:readline";
10
- const lt = { width: 1440, height: 900 };
11
- function L(n, t, e) {
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 ut(n, t) {
15
- const e = n.indexOf("#"), o = e >= 0 ? n.slice(e) : "", r = e >= 0 ? n.slice(0, e) : n, s = r.indexOf("?"), a = s >= 0 ? r.slice(0, s) : r, i = new URLSearchParams(s >= 0 ? r.slice(s + 1) : "");
16
- for (const [p, g] of Object.entries(t.query))
17
- if (i.delete(p), Array.isArray(g))
18
- for (const w of g) L(i, p, w);
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
- L(i, p, g);
21
- const h = i.toString();
22
- return `${a}${h ? `?${h}` : ""}${o}`;
23
- }
24
- function M(n) {
25
- const t = [], e = /* @__PURE__ */ new Set(), o = (r, s, a) => {
26
- for (const i of r) {
27
- if (!i.path) throw new Error("[pubuilder] page.path는 필수입니다");
28
- if (!i.title) throw new Error(`[pubuilder] "${i.path}"의 title이 없습니다`);
29
- if (e.has(i.path)) throw new Error(`[pubuilder] 중복 path: ${i.path}`);
30
- if (e.add(i.path), t.push({
31
- id: i.path,
32
- path: i.path,
33
- title: i.title,
34
- external: i.external ?? /^https?:\/\//.test(i.path),
35
- parentId: s,
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
- }), i.variants?.length) {
38
- if (i.external ?? /^https?:\/\//.test(i.path))
39
- throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${i.path}`);
40
- for (const h of i.variants) {
41
- if (!h.title) throw new Error(`[pubuilder] "${i.path}" variant의 title이 없습니다`);
42
- const p = ut(i.path, h);
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: h.title,
47
+ title: c.title,
48
48
  external: !1,
49
- parentId: i.path,
49
+ parentId: r.path,
50
50
  depth: a + 1
51
51
  });
52
52
  }
53
53
  }
54
- i.children?.length && o(i.children, i.path, a + 1);
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 ?? lt };
58
+ return { nodes: t, viewport: n.viewport ?? dt };
59
59
  }
60
60
  const P = " ";
61
- function pt(n) {
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 A(n) {
64
+ function N(n) {
65
65
  return JSON.stringify(n);
66
66
  }
67
- function Z(n, t, e) {
68
- const o = P.repeat(t), r = [];
69
- n.path.includes("[") && r.push(`${o}// TODO: 샘플 경로로 교체 (예: ${pt(n.path)})`);
70
- const s = e.has(n.path) ? " // TODO: title 확인" : "";
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 r.push(`${o}{ path: ${A(n.path)}, title: ${A(n.title)} },${s}`), r;
73
- if (r.push(`${o}{`), r.push(`${o}${P}path: ${A(n.path)},`), r.push(`${o}${P}title: ${A(n.title)},${s}`), n.external && r.push(`${o}${P}external: true,`), n.variants?.length) {
74
- r.push(`${o}${P}variants: [`);
75
- for (const i of n.variants)
76
- r.push(
77
- `${o}${P}${P}{ title: ${A(i.title)}, query: ${A(i.query)} },`
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
- r.push(`${o}${P}],`);
79
+ s.push(`${o}${P}],`);
80
80
  }
81
81
  if (n.children?.length) {
82
- r.push(`${o}${P}children: [`);
83
- for (const i of n.children) r.push(...Z(i, t + 2, e));
84
- r.push(`${o}${P}],`);
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 r.push(`${o}},`), r;
86
+ return s.push(`${o}},`), s;
87
87
  }
88
- function G(n, t = {}) {
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: ${A(n.viewport)},`), o.push(`${P}pages: [`);
95
- for (const r of n.pages) o.push(...Z(r, 2, e));
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 E(n) {
99
+ function R(n) {
100
100
  return n === "/" ? [] : n.replace(/^\//, "").split("/");
101
101
  }
102
- function ht(n, t) {
102
+ function wt(n, t) {
103
103
  return n.length >= t.length ? !1 : n.every((e, o) => e === t[o]);
104
104
  }
105
- function dt(n) {
106
- const t = [...n].sort((r, s) => {
107
- const a = E(r.routePath).length - E(s.routePath).length;
108
- return a !== 0 ? a : r.routePath.localeCompare(s.routePath);
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 r of t) {
111
- const s = E(r.routePath), a = {
112
- path: r.routePath,
113
- title: r.title ?? s.at(-1) ?? "/"
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 i;
116
- for (const h of o)
117
- ht(h.segments, s) && (!i || h.segments.length > i.segments.length) && (i = h);
118
- i ? (i.page.children ??= []).push(a) : e.push(a), o.push({ segments: s, page: a });
115
+ 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 U(n, t) {
123
- const e = E(n), o = E(t);
124
- for (let r = 0; r < o.length; r++) {
125
- const s = o[r];
126
- if (/^\[\[\.\.\..+\]\]$/.test(s))
127
- return r <= e.length;
128
- if (/^\[\.\.\..+\]$/.test(s))
129
- return r < e.length;
130
- if (r >= e.length) return !1;
131
- if (!/^\[.+\]$/.test(s) && s !== e[r])
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 Y(n, t = []) {
136
+ function q(n, t = []) {
137
137
  for (const e of n)
138
- t.push({ page: e, segments: E(e.path) }), e.children && Y(e.children, t);
138
+ t.push({ page: e, segments: R(e.path) }), e.children && q(e.children, t);
139
139
  return t;
140
140
  }
141
- function C(n) {
141
+ function J(n) {
142
142
  return n.external ?? /^https?:\/\//.test(n.path);
143
143
  }
144
- function ft(n, t) {
145
- const e = structuredClone(n), o = Y(e.pages), r = o.filter((d) => !C(d.page)), s = (d) => r.some(
146
- (c) => c.page.path === d.routePath || U(c.page.path, d.routePath)
147
- ), a = t.filter((d) => !s(d)), i = (d) => t.some(
148
- (c) => c.routePath === d.page.path || U(d.page.path, c.routePath)
149
- ), h = r.filter((d) => !i(d)).map((d) => d.page.path), p = [], g = [...o], w = [...a].sort(
150
- (d, c) => E(d.routePath).length - E(c.routePath).length
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 d of w) {
153
- const c = E(d.routePath), l = { path: d.routePath, title: d.title ?? c.at(-1) ?? "/" };
154
- let u;
155
- for (const S of g) {
156
- if (C(S.page)) continue;
157
- const $ = S.segments;
158
- $.length >= c.length || $.every((y, I) => y === c[I]) && (!u || $.length > u.segments.length) && (u = S);
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
- u ? (u.page.children ??= []).push(l) : e.pages.push(l), g.push({ page: l, segments: c }), p.push(d.routePath);
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: h };
162
+ return { config: e, added: p, removed: c };
163
163
  }
164
- const mt = /^page\.(tsx|jsx|ts|js)$/, gt = /^\(.+\)$/, wt = /^\(\.{1,3}\)/;
165
- function J(n) {
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(f(n, t))) return f(n, t);
167
+ if (v(d(n, t))) return d(n, t);
168
168
  return null;
169
169
  }
170
- function $t(n) {
170
+ function vt(n) {
171
171
  const t = [];
172
172
  for (const e of n) {
173
- if (e.startsWith("@") || e.startsWith("_") || wt.test(e)) return null;
174
- gt.test(e) || t.push(e);
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 yt(n) {
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 Q(n, t, e) {
185
- for (const o of D(n, { withFileTypes: !0 }))
186
- o.isDirectory() ? Q(f(n, o.name), [...t, o.name], e) : mt.test(o.name) && e.push({ file: f(n, o.name), segments: t });
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 bt = {
188
+ const Tt = {
189
189
  name: "next-app",
190
190
  detect(n) {
191
- return J(n) !== null;
191
+ return B(n) !== null;
192
192
  },
193
193
  scan(n) {
194
- const t = J(n);
194
+ const t = B(n);
195
195
  if (!t) return [];
196
196
  const e = [];
197
- Q(t, [], e);
197
+ tt(t, [], e);
198
198
  const o = [];
199
- for (const { file: r, segments: s } of e) {
200
- const a = $t(s);
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: yt(O(r, "utf8"))
204
+ title: It(A(s, "utf8"))
205
205
  });
206
206
  }
207
207
  return o;
208
208
  }
209
- }, H = /\.(tsx|jsx|ts|js)$/, St = /* @__PURE__ */ new Set(["_app", "_document", "_error", "404", "500"]);
210
- function B(n) {
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(f(n, t))) return f(n, t);
212
+ if (v(d(n, t))) return d(n, t);
213
213
  return null;
214
214
  }
215
- function q(n, t, e) {
216
- for (const o of D(n, { withFileTypes: !0 })) {
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
- q(f(n, o.name), [...t, o.name], e);
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 r = o.name.replace(H, "");
224
- if (St.has(r)) continue;
225
- const a = `/${(r === "index" ? t : [...t, r]).join("/")}`;
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 Pt = {
229
+ const Et = {
230
230
  name: "next-pages",
231
231
  detect(n) {
232
- return B(n) !== null;
232
+ return K(n) !== null;
233
233
  },
234
234
  scan(n) {
235
- const t = B(n);
235
+ const t = K(n);
236
236
  if (!t) return [];
237
237
  const e = [];
238
- return q(t, [], e), e;
238
+ return et(t, [], e), e;
239
239
  }
240
- }, vt = `import { defineIA } from "pubuilder/config"
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 m extends Error {
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 It(n) {
265
+ function Ot(n) {
266
266
  let t;
267
267
  try {
268
268
  t = new URL(n);
269
269
  } catch {
270
- throw new m("INVALID_FIGMA_URL", "URL 형식이 아니에요");
270
+ throw new g("INVALID_FIGMA_URL", "URL 형식이 아니에요");
271
271
  }
272
272
  if (!/(^|\.)figma\.com$/.test(t.hostname))
273
- throw new m("INVALID_FIGMA_URL", "figma.com 링크가 아니에요");
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 m("INVALID_FIGMA_URL", "fileKey를 찾지 못했어요");
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 m("INVALID_FIGMA_URL", "node-id가 없어요. Figma에서 프레임 우클릭 → Copy link로 복사해주세요");
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 F(n) {
282
- return f(n, ".pubuilder", "credentials.json");
281
+ function L(n) {
282
+ return d(n, ".pubuilder", "credentials.json");
283
283
  }
284
- function K(n) {
285
- const t = F(n.homeDir);
284
+ function V(n) {
285
+ const t = L(n.homeDir);
286
286
  if (v(t))
287
287
  try {
288
- const o = JSON.parse(O(t, "utf8"));
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 = f(n.projectRoot, ".env");
293
+ const e = d(n.projectRoot, ".env");
294
294
  if (v(e)) {
295
- const o = O(e, "utf8").split(`
296
- `).find((r) => r.trim().startsWith("FIGMA_TOKEN="));
295
+ const o = A(e, "utf8").split(`
296
+ `).find((s) => s.trim().startsWith("FIGMA_TOKEN="));
297
297
  if (o) {
298
- const r = o.slice(o.indexOf("=") + 1).trim().replace(/^["']|["']$/g, "");
299
- if (r) return r;
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 Tt(n, t) {
305
- j(f(n, ".pubuilder"), { recursive: !0 }), R(F(n), JSON.stringify({ figmaToken: t }, null, 2), { mode: 384 });
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 Et(n) {
308
- W(F(n), { force: !0 });
307
+ function Ft(n) {
308
+ D(L(n), { force: !0 });
309
309
  }
310
- function At(n) {
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 Ot(n, t, e, o = fetch) {
314
- const r = { "X-Figma-Token": t }, s = await o(
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: r }
316
+ { headers: s }
317
317
  );
318
- if (s.status === 403) throw new m("FIGMA_API_ERROR", "Figma 토큰이 유효하지 않아요. 스킬함 설정에서 토큰을 확인해주세요", 403);
319
- if (!s.ok) throw new m("FIGMA_API_ERROR", `Figma API 오류 (${s.status})`, 502);
320
- const i = (await s.json()).nodes[n.nodeId]?.document;
321
- if (!i) throw new m("FIGMA_API_ERROR", "해당 노드를 찾지 못했어요. URL의 node-id를 확인해주세요", 404);
322
- const h = await o(
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: r }
324
+ { headers: s }
325
325
  );
326
- if (!h.ok) throw new m("FIGMA_API_ERROR", `이미지 export 실패 (${h.status})`, 502);
327
- const g = (await h.json()).images[n.nodeId];
328
- if (!g) throw new m("FIGMA_API_ERROR", "이미지 export URL이 비어 있어요", 502);
329
- const w = await (await o(g)).arrayBuffer();
330
- j(e, { recursive: !0 });
331
- const d = n.nodeId.replace(/[^A-Za-z0-9_-]/g, "-"), c = f(e, `${n.fileKey}-${d}.png`);
332
- return R(c, Buffer.from(w)), { nodeJson: i, imagePath: c };
333
- }
334
- function V(n) {
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 Rt {
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 f(this.projectRoot, ".pubuilder", "settings.json");
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(O(this.settingsPath(), "utf8"));
352
+ return JSON.parse(A(this.settingsPath(), "utf8"));
353
353
  } catch {
354
354
  return { skills: {} };
355
355
  }
356
356
  }
357
357
  saveSettings(t) {
358
- j(f(this.projectRoot, ".pubuilder"), { recursive: !0 }), R(this.settingsPath(), JSON.stringify(t, null, 2));
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 r of D(t, { withFileTypes: !0 })) {
364
- if (!r.isDirectory()) continue;
365
- const s = f(t, r.name, "SKILL.md");
366
- if (!v(s)) continue;
367
- const a = V(O(s, "utf8"));
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}:${r.name}`,
370
- name: a.name ?? r.name,
369
+ id: `${e}:${s.name}`,
370
+ name: a.name ?? s.name,
371
371
  description: a.description ?? "",
372
372
  source: e,
373
- path: s
373
+ path: i
374
374
  });
375
375
  }
376
376
  return o;
377
377
  }
378
378
  scanAll() {
379
379
  return [
380
- ...this.scanDir(f(this.projectRoot, ".claude", "skills"), "project"),
381
- ...this.scanDir(f(this.homeDir, ".claude", "skills"), "global"),
382
- ...this.scanDir(f(this.projectRoot, ".pubuilder", "skills"), "uploaded")
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((r) => r.id === t)) throw new m("NOT_FOUND", `스킬 없음: ${t}`, 404);
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 = V(e), r = (o.name ?? t.replace(/\.md$/i, "")).trim();
400
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(r))
401
- throw new m("INVALID_NAME", `스킬 이름이 유효하지 않아요: ${r}`, 400);
402
- const s = f(this.projectRoot, ".pubuilder", "skills", r);
403
- return j(s, { recursive: !0 }), R(f(s, "SKILL.md"), e), { id: `uploaded:${r}`, name: r, description: o.description ?? "", source: "uploaded", enabled: !0 };
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 m("FORBIDDEN", "업로드된 스킬만 삭제할 수 있어요", 403);
408
- if (!this.scanAll().some((s) => s.id === t)) throw new m("NOT_FOUND", `스킬 없음: ${t}`, 404);
409
- const e = t.slice(9), o = f(this.projectRoot, ".pubuilder", "skills", e);
410
- W(o, { recursive: !0 });
411
- const r = this.loadSettings();
412
- delete r.skills[t], this.saveSettings(r);
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: O(e.path, "utf8") }));
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 Nt(n) {
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((r) => `## 스킬: ${r.name}
425
+ ${n.skills.map((i) => `## 스킬: ${i.name}
426
426
 
427
- ${r.content}`).join(`
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 xt(n) {
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 r = o.input?.file_path ?? o.input?.command ?? "";
477
- e.push({ type: "tool", text: `${o.name}: ${r}`.trim() });
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, kt = {
508
+ const z = 600 * 1e3, Gt = {
484
509
  run({ prompt: n, cwd: t, onEvent: e, signal: o }) {
485
- return new Promise((r) => {
486
- const s = X("claude", [
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 i = (c) => {
503
- a || (a = !0, clearTimeout(p), c && e(c), r());
504
- }, h = () => {
505
- s.kill("SIGKILL"), i({ type: "error", text: "에이전트가 10분간 응답이 없어 중단했어요" });
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(h, z);
508
- const g = () => {
509
- a || (clearTimeout(p), p = setTimeout(h, z));
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
- s.kill("SIGKILL"), i({ type: "error", text: "사용자가 중단했어요" });
513
- }), s.on("error", (c) => {
514
- i({
537
+ i.kill("SIGKILL"), r({ type: "error", text: "사용자가 중단했어요" });
538
+ }), i.on("error", (l) => {
539
+ r({
515
540
  type: "error",
516
- text: c.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${c.message}`
541
+ text: l.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${l.message}`
517
542
  });
518
543
  });
519
- const w = [];
520
- s.stderr.on("data", (c) => w.push(String(c)));
521
- let d = !1;
522
- ct({ input: s.stdout }).on("line", (c) => {
523
- g();
524
- const l = xt(c);
525
- for (const u of l)
526
- e(u), (u.type === "done" || u.type === "error") && (d = !0);
527
- }), s.on("close", (c) => {
528
- if (c !== 0 && !d) {
529
- const l = w.join("").trim();
530
- i({ type: "error", text: l || `claude가 코드 ${c}로 종료됐어요. claude login 상태를 확인해주세요` });
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
- i();
557
+ r();
533
558
  });
534
559
  });
535
560
  }
536
- }, _t = 20;
537
- class jt {
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.active && !this.active.finished) throw new m("BUSY", "이미 퍼블리싱이 실행 중이에요", 409);
546
- const e = `job-${++this.seq}`, o = new AbortController(), r = { id: e, events: [], listeners: /* @__PURE__ */ new Set(), controller: o, done: Promise.resolve(), finished: !1 }, s = (a) => {
547
- r.events.push(a);
548
- for (const i of r.listeners) i(a);
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 r.done = this.adapter.run({ ...t, onEvent: s, signal: o.signal }).catch((a) => s({ type: "error", text: a instanceof Error ? a.message : String(a) })).finally(() => {
551
- r.finished = !0, this.evictFinishedJobs();
552
- }), this.jobs.set(e, r), this.active = r, e;
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 > _t; ) {
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 m("NOT_FOUND", `job 없음: ${t}`, 404);
570
- for (const r of o.events) e(r);
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 m("NOT_FOUND", `job 없음: ${t}`, 404);
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 m("NOT_FOUND", `job 없음: ${t}`, 404);
681
+ if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
581
682
  return e.done;
582
683
  }
583
684
  }
584
- const Dt = 4816, Ft = 1024 * 1024, Lt = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
585
- function _(n) {
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 r = 0;
589
- n.on("data", (s) => {
590
- if (r += s.length, r > Ft) {
591
- e(new m("PAYLOAD_TOO_LARGE", "요청 본문이 너무 커요 (1MB 초과)", 413)), n.destroy();
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(s);
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 m("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
704
+ e(new g("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
604
705
  }
605
706
  }), n.on("error", e);
606
707
  });
607
708
  }
608
- function T(n, t, e) {
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 Mt(n) {
613
- const t = n.projectRoot, e = n.homeDir ?? it(), o = n.adapter ?? kt, r = n.figmaFetch ?? fetch, s = new Rt({ projectRoot: t, homeDir: e }), a = new jt(o);
614
- async function i(c) {
615
- const l = await _(c), { selection: u, outerHTML: S, figmaUrl: $ } = l ?? {}, y = It(String($ ?? ""));
616
- if (typeof u != "object" || u === null || typeof u.pagePath != "string" || typeof u.selector != "string" || typeof S != "string")
617
- throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
618
- const I = K({ projectRoot: t, homeDir: e, env: process.env });
619
- if (!I) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
620
- const { nodeJson: x, imagePath: k } = await Ot(y, I, f(t, ".pubuilder/cache"), r), b = Nt({
621
- selection: u,
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($), nodeJson: x, imagePath: k },
624
- skills: s.enabledSkillContents()
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: b, cwd: t }) } };
728
+ return { status: 202, body: { jobId: a.start({ prompt: O, cwd: t }) } };
627
729
  }
628
- function h(c, l, u) {
629
- if (!a.has(u)) throw new m("NOT_FOUND", `job 없음: ${u}`, 404);
630
- l.writeHead(200, {
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 $ = (y) => {
638
- l.write(`data: ${JSON.stringify(y)}
739
+ const y = (w) => {
740
+ u.write(`data: ${JSON.stringify(w)}
639
741
 
640
- `), (y.type === "done" || y.type === "error") && (S(), l.end());
742
+ `), (w.type === "done" || w.type === "error") && (S(), u.end());
641
743
  };
642
- S = a.subscribe(u, $), c.on("close", S);
744
+ S = a.subscribe(f, y), l.on("close", S);
643
745
  }
644
- const p = st((c, l) => {
746
+ const p = ut((l, u) => {
645
747
  (async () => {
646
748
  try {
647
- const u = c.headers.origin;
648
- if (u && !Lt.test(u)) {
649
- T(l, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
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
- if (u && (l.setHeader("Access-Control-Allow-Origin", u), l.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE"), l.setHeader("Access-Control-Allow-Headers", "content-type")), c.method === "OPTIONS") {
653
- l.writeHead(204), l.end();
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
- const $ = new URL(c.url ?? "/", "http://localhost").pathname, y = c.method ?? "GET";
657
- if (y === "GET" && $ === "/api/health") {
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 (y === "GET" && $ === "/api/skills") {
662
- T(l, 200, s.list());
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
- if (y === "POST" && $ === "/api/skills") {
666
- const b = await _(c), N = s.upload(b.filename, b.content);
667
- T(l, 201, N);
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
- const I = $.match(/^\/api\/skills\/([^/]+)$/);
671
- if (I && y === "PATCH") {
672
- const b = decodeURIComponent(I[1]), N = await _(c);
673
- s.setEnabled(b, N.enabled), l.writeHead(204), l.end();
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 (I && y === "DELETE") {
677
- const b = decodeURIComponent(I[1]);
678
- s.remove(b), l.writeHead(204), l.end();
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 (y === "GET" && $ === "/api/settings/figma-token") {
682
- const b = K({ projectRoot: t, homeDir: e, env: process.env });
683
- T(l, 200, { configured: b != null, masked: b ? At(b) : null });
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 (y === "PUT" && $ === "/api/settings/figma-token") {
687
- const b = await _(c);
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 (y === "DELETE" && $ === "/api/settings/figma-token") {
692
- Et(e), l.writeHead(204), l.end();
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 (y === "POST" && $ === "/api/publish") {
696
- const { status: b, body: N } = await i(c);
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
- const x = $.match(/^\/api\/publish\/([^/]+)\/events$/);
701
- if (x && y === "GET") {
702
- h(c, l, x[1]);
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 k = $.match(/^\/api\/publish\/([^/]+)$/);
706
- if (k && y === "DELETE") {
707
- a.cancel(k[1]), l.writeHead(204), l.end();
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
- T(l, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
711
- } catch (u) {
712
- if (u instanceof m) {
713
- T(l, u.status, { code: u.code, message: u.message });
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
- const S = u instanceof Error ? u.message : String(u);
717
- T(l, 500, { code: "INTERNAL", message: S });
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
- }), g = n.port ?? Dt;
721
- await new Promise((c) => p.listen(g, "127.0.0.1", c));
722
- const w = p.address();
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 w == "object" && w ? w.port : g,
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 tt(n, t) {
731
- const e = await Mt({ projectRoot: n, port: t.port ?? 4816 });
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 et(n) {
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 Gt(n, t) {
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 r = await tt(n, { port: et(t.slice(0, e === -1 ? void 0 : e)) });
747
- return new Promise((s) => {
748
- const a = X(o[0], o.slice(1), { cwd: n, stdio: "inherit", env: process.env }), i = () => a.kill("SIGINT"), h = () => a.kill("SIGTERM");
749
- process.on("SIGINT", i), process.on("SIGTERM", h);
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", i), process.off("SIGTERM", h), r.close();
864
+ process.off("SIGINT", r), process.off("SIGTERM", c), s.close();
752
865
  };
753
- a.on("error", (g) => {
754
- console.error(`[pubuilder] 명령 실행 실패: ${g.message}`), p(), s(1);
755
- }), a.on("close", (g, w) => {
756
- p(), s(w ? 128 + (at.signals[w] ?? 0) : g ?? 0);
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 Ut = [bt, Pt];
761
- function Ct(n) {
762
- const t = f(n, "package.json");
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(O(t, "utf8"));
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 Jt(n) {
772
- if (!Ct(n)) return null;
773
- const t = Ut.filter((r) => r.detect(n));
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 r of t)
777
- for (const s of r.scan(n)) {
778
- const a = e.get(s.routePath);
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 충돌: ${s.routePath} (${a} 우선, ${r.name} 무시)`);
893
+ console.warn(`[pubuilder] path 충돌: ${i.routePath} (${a} 우선, ${s.name} 무시)`);
781
894
  continue;
782
895
  }
783
- e.set(s.routePath, r.name), o.push(s);
896
+ e.set(i.routePath, s.name), o.push(i);
784
897
  }
785
898
  return o;
786
899
  }
787
- async function Ht(n) {
788
- const e = await rt(import.meta.url).import(n, { default: !0 });
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 Bt(n, t) {
794
- const e = Jt(n), o = f(n, "ia.config.ts");
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 && (R(o, vt), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
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 i = await Ht(o), { config: h, added: p, removed: g } = ft(i, e);
799
- M(h), t.dryRun || R(o, G(h, { newPaths: new Set(p) }));
800
- for (const w of p) console.log(`[pubuilder] + 추가됨: ${w}`);
801
- for (const w of g) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${w}`);
802
- return p.length === 0 && g.length === 0 && console.log("[pubuilder] 변경 없음"), t.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: p, removed: g };
803
- }
804
- const s = { pages: dt(e) };
805
- M(s);
806
- const a = e.map((i) => i.routePath);
807
- return t.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (R(o, G(s, { newPaths: new Set(a) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${a.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: a };
808
- }
809
- async function Kt(n) {
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 Bt(process.cwd(), { dryRun: e.includes("--dry-run") })).kind === "unsupported" ? 1 : 0 : t === "serve" ? (await tt(process.cwd(), { port: et(e) }), await new Promise(() => {
813
- })) : t === "dev" ? await Gt(process.cwd(), e) : (console.error("사용법: pubuilder scan [--dry-run] | pubuilder serve [--port N] | pubuilder dev [--port N] -- <명령>"), 1);
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 Vt() {
931
+ function qt() {
819
932
  const n = process.argv[1];
820
933
  if (!n) return !1;
821
934
  try {
822
- return nt(n) === ot(import.meta.url);
935
+ return rt(n) === ct(import.meta.url);
823
936
  } catch {
824
937
  return !1;
825
938
  }
826
939
  }
827
- Vt() && Kt(process.argv.slice(2)).then(
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
- Kt as main,
833
- Bt as runScan
945
+ Qt as main,
946
+ Yt as runScan
834
947
  };