pubuilder 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,243 +1,243 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync as R, existsSync as P, readdirSync as D, mkdirSync as j, writeFileSync as N, 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 $ of g) L(i, p, $);
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
- const v = " ";
61
- function pt(n) {
60
+ const P = " ";
61
+ function mt(n) {
62
62
  return n.split("/").map((t) => /^\[{1,2}\.{0,3}.*\]{1,2}$/.test(t) && t.startsWith("[") ? "1" : t).join("/");
63
63
  }
64
- function O(n) {
64
+ function N(n) {
65
65
  return JSON.stringify(n);
66
66
  }
67
- function Z(n, t, e) {
68
- const o = v.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: ${O(n.path)}, title: ${O(n.title)} },${s}`), r;
73
- if (r.push(`${o}{`), r.push(`${o}${v}path: ${O(n.path)},`), r.push(`${o}${v}title: ${O(n.title)},${s}`), n.external && r.push(`${o}${v}external: true,`), n.variants?.length) {
74
- r.push(`${o}${v}variants: [`);
75
- for (const i of n.variants)
76
- r.push(
77
- `${o}${v}${v}{ title: ${O(i.title)}, query: ${O(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}${v}],`);
79
+ s.push(`${o}${P}],`);
80
80
  }
81
81
  if (n.children?.length) {
82
- r.push(`${o}${v}children: [`);
83
- for (const i of n.children) r.push(...Z(i, t + 2, e));
84
- r.push(`${o}${v}],`);
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(`${v}viewport: ${O(n.viewport)},`), o.push(`${v}pages: [`);
95
- for (const r of n.pages) o.push(...Z(r, 2, e));
96
- return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
94
+ n.viewport && o.push(`${P}viewport: ${N(n.viewport)},`), o.push(`${P}pages: [`);
95
+ for (const s of n.pages) o.push(...Q(s, 2, e));
96
+ return o.push(`${P}],`), o.push("})"), o.push(""), o.join(`
97
97
  `);
98
98
  }
99
- function T(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 = T(r.routePath).length - T(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 = T(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 = T(n), o = T(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: T(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], $ = [...a].sort(
150
- (d, c) => T(d.routePath).length - T(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 $) {
153
- const c = T(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;
152
+ for (const h of b) {
153
+ const l = R(h.routePath), u = { path: h.routePath, title: h.title ?? l.at(-1) ?? "/" };
154
+ let f;
155
+ for (const S of m) {
156
+ if (J(S.page)) continue;
157
157
  const y = S.segments;
158
- y.length >= c.length || y.every((w, E) => w === c[E]) && (!u || y.length > u.segments.length) && (u = S);
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 (P(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(R(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 (P(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 vt = {
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
- }, Pt = `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);
286
- if (P(t))
284
+ function V(n) {
285
+ const t = L(n.homeDir);
286
+ if (v(t))
287
287
  try {
288
- const o = JSON.parse(R(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");
294
- if (P(e)) {
295
- const o = R(e, "utf8").split(`
296
- `).find((r) => r.trim().startsWith("FIGMA_TOKEN="));
293
+ const e = d(n.projectRoot, ".env");
294
+ if (v(e)) {
295
+ const o = A(e, "utf8").split(`
296
+ `).find((s) => s.trim().startsWith("FIGMA_TOKEN="));
297
297
  if (o) {
298
- const 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 }), N(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 $ = 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 N(c, Buffer.from($)), { 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
- if (!P(this.settingsPath())) return { skills: {} };
350
+ if (!v(this.settingsPath())) return { skills: {} };
351
351
  try {
352
- return JSON.parse(R(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 }), N(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
- if (!P(t)) return [];
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 (!P(s)) continue;
367
- const a = V(R(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,47 @@ 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 }), N(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: R(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((s) => `## 스킬: ${s.name}
425
+ ${n.skills.map((i) => `## 스킬: ${i.name}
426
426
 
427
- ${s.content}`).join(`
427
+ ${i.content}`).join(`
428
428
 
429
429
  ---
430
430
 
431
431
  `)}
432
432
  ` : `
433
433
  다른 스킬은 스스로 판단해 사용하지 마세요. 아래에 명시된 스킬 지침만 따르세요 (이번 요청에는 없음).
434
- `, r = n.instruction ? `
434
+ `, s = n.instruction ? `
435
435
  # 사용자의 후속 답변
436
436
 
437
437
  ${n.instruction}
@@ -457,7 +457,7 @@ ${n.outerHTML}
457
457
  ${JSON.stringify(e.nodeJson, null, 2)}
458
458
  \`\`\`
459
459
  ${o}
460
- ${r}
460
+ ${s}
461
461
  # 작업 순서
462
462
  1. 위 라우트와 outerHTML 스냅샷을 단서로, 이 블록을 렌더링하는 소스 컴포넌트 파일을 찾으세요.
463
463
  2. Figma 렌더 이미지를 Read 도구로 열어 시안을 육안으로 확인하세요. 노드 JSON의 수치(색상/간격/타이포)를 우선 근거로 쓰되, JSON에만 있고 이미지에 보이지 않는 요소는 구현하지 마세요.
@@ -465,7 +465,25 @@ ${r}
465
465
  4. 수정 후 타입체크(pnpm type-check)가 있으면 실행해 통과를 확인하세요.
466
466
  5. 마지막 메시지에 수정한 파일 경로를 나열하세요.`;
467
467
  }
468
- function 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) {
469
487
  let t;
470
488
  try {
471
489
  t = JSON.parse(n);
@@ -480,17 +498,17 @@ function xt(n) {
480
498
  const e = [];
481
499
  for (const o of t.message?.content ?? [])
482
500
  if (o.type === "text" && o.text && e.push({ type: "log", text: o.text }), o.type === "tool_use") {
483
- const r = o.input?.file_path ?? o.input?.command ?? "";
484
- 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 } : {} });
485
503
  }
486
504
  return e;
487
505
  }
488
506
  return [];
489
507
  }
490
- const z = 600 * 1e3, kt = {
508
+ const z = 600 * 1e3, Gt = {
491
509
  run({ prompt: n, cwd: t, onEvent: e, signal: o }) {
492
- return new Promise((r) => {
493
- const s = X("claude", [
510
+ return new Promise((s) => {
511
+ const i = Y("claude", [
494
512
  "-p",
495
513
  n,
496
514
  "--output-format",
@@ -506,62 +524,94 @@ const z = 600 * 1e3, kt = {
506
524
  ""
507
525
  ], { cwd: t, env: process.env });
508
526
  let a = !1;
509
- const i = (c) => {
510
- a || (a = !0, clearTimeout(p), c && e(c), r());
511
- }, h = () => {
512
- 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분간 응답이 없어 중단했어요" });
513
531
  };
514
- let p = setTimeout(h, z);
515
- const g = () => {
516
- a || (clearTimeout(p), p = setTimeout(h, z));
532
+ let p = setTimeout(c, z);
533
+ const m = () => {
534
+ a || (clearTimeout(p), p = setTimeout(c, z));
517
535
  };
518
536
  o?.addEventListener("abort", () => {
519
- s.kill("SIGKILL"), i({ type: "error", text: "사용자가 중단했어요" });
520
- }), s.on("error", (c) => {
521
- i({
537
+ i.kill("SIGKILL"), r({ type: "error", text: "사용자가 중단했어요" });
538
+ }), i.on("error", (l) => {
539
+ r({
522
540
  type: "error",
523
- 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}`
524
542
  });
525
543
  });
526
- const $ = [];
527
- s.stderr.on("data", (c) => $.push(String(c)));
528
- let d = !1;
529
- ct({ input: s.stdout }).on("line", (c) => {
530
- g();
531
- const l = xt(c);
532
- for (const u of l)
533
- e(u), (u.type === "done" || u.type === "error") && (d = !0);
534
- }), s.on("close", (c) => {
535
- if (c !== 0 && !d) {
536
- const l = $.join("").trim();
537
- 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 상태를 확인해주세요` });
538
556
  } else
539
- i();
557
+ r();
540
558
  });
541
559
  });
542
560
  }
543
- }, _t = 20;
544
- class jt {
545
- constructor(t) {
546
- this.adapter = t;
561
+ }, Ut = 20, X = 3;
562
+ class Ct {
563
+ constructor(t, e = {}) {
564
+ this.adapter = t, this.capture = e.captureBaseline ?? Dt, this.readBaseline = e.readBaseline ?? nt, this.restoreFile = e.restoreFile ?? Lt, this.readCurrent = e.readCurrent ?? ((o, s) => {
565
+ try {
566
+ return A(d(o, s), "utf8");
567
+ } catch {
568
+ return "";
569
+ }
570
+ }), this.writeFile = e.writeFile ?? ((o, s, i) => T(d(o, s), i));
547
571
  }
548
572
  jobs = /* @__PURE__ */ new Map();
549
- active = null;
550
573
  seq = 0;
574
+ capture;
575
+ readBaseline;
576
+ restoreFile;
577
+ readCurrent;
578
+ writeFile;
579
+ /** suspend 시 파일별 baseline ref를 보관 → resolve의 revert에서 사용 */
580
+ pendingBaseline = /* @__PURE__ */ new Map();
581
+ activeCount() {
582
+ let t = 0;
583
+ for (const e of this.jobs.values()) e.finished || t++;
584
+ return t;
585
+ }
551
586
  start(t) {
552
- if (this.active && !this.active.finished) throw new m("BUSY", "이미 퍼블리싱이 실행 중이에요", 409);
553
- 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) => {
554
- r.events.push(a);
555
- 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);
556
606
  };
557
- 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(() => {
558
- r.finished = !0, this.evictFinishedJobs();
559
- }), 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;
560
610
  }
561
611
  /** finished job이 상한을 넘으면 가장 오래된 것부터 제거 (active job은 finished=false라 대상에서 제외) */
562
612
  evictFinishedJobs() {
563
613
  const t = [...this.jobs.entries()].filter(([, e]) => e.finished).map(([e]) => e);
564
- for (; t.length > _t; ) {
614
+ for (; t.length > Ut; ) {
565
615
  const e = t.shift();
566
616
  if (e === void 0) break;
567
617
  this.jobs.delete(e);
@@ -571,34 +621,78 @@ class jt {
571
621
  has(t) {
572
622
  return this.jobs.has(t);
573
623
  }
624
+ /** job이 편집한 파일 목록 (상대경로, 정렬) */
625
+ touchedFiles(t) {
626
+ const e = this.jobs.get(t);
627
+ return e ? [...e.touchedFiles].sort() : [];
628
+ }
629
+ /** job 시작 시점 baseline ref */
630
+ baselineRef(t) {
631
+ return this.jobs.get(t)?.baselineRef ?? null;
632
+ }
633
+ /**
634
+ * 진행 중(running)인 job을 전부 abort하고, 그들이 편집한 파일을 분류한다.
635
+ * - 자동 롤백: running job만 건드린 파일 → baseline으로 즉시 복원
636
+ * - 충돌: 완료(finished)된 job도 건드린 파일 → 사용자 해결 대상으로 반환
637
+ * 파일별 baseline은 그 파일을 건드린 running job 중 가장 이른(seq 최소) job의 것.
638
+ */
639
+ suspendRunning(t) {
640
+ const e = [...this.jobs.values()].filter((r) => !r.finished).sort((r, c) => r.seq - c.seq);
641
+ for (const r of e) r.controller.abort();
642
+ const o = /* @__PURE__ */ new Map();
643
+ for (const r of e)
644
+ for (const c of r.touchedFiles)
645
+ o.has(c) || o.set(c, r.baselineRef);
646
+ const s = /* @__PURE__ */ new Set();
647
+ for (const r of this.jobs.values())
648
+ if (r.finished)
649
+ for (const c of r.touchedFiles) s.add(c);
650
+ const i = [], a = [];
651
+ this.pendingBaseline.clear();
652
+ for (const [r, c] of o)
653
+ c !== null && (this.pendingBaseline.set(r, c), s.has(r) ? a.push({ file: r, baseline: this.readBaseline(t, c, r) ?? "", current: this.readCurrent(t, r) }) : (this.restoreFile(t, c, r), i.push(r)));
654
+ return { autoReverted: i, conflicts: a };
655
+ }
656
+ /**
657
+ * 충돌 파일에 대한 사용자 결정을 적용한다.
658
+ * revert → baseline 복원, edit → content로 덮어쓰기, keep → 그대로 둔다.
659
+ */
660
+ applyResolutions(t, e) {
661
+ for (const o of e)
662
+ if (o.action === "revert") {
663
+ const s = this.pendingBaseline.get(o.file);
664
+ s && this.restoreFile(t, s, o.file);
665
+ } else o.action === "edit" && this.writeFile(t, o.file, o.content ?? "");
666
+ this.pendingBaseline.clear();
667
+ }
574
668
  subscribe(t, e) {
575
669
  const o = this.jobs.get(t);
576
- if (!o) throw new m("NOT_FOUND", `job 없음: ${t}`, 404);
577
- 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);
578
672
  return o.listeners.add(e), () => o.listeners.delete(e);
579
673
  }
580
674
  cancel(t) {
581
675
  const e = this.jobs.get(t);
582
- if (!e) throw new m("NOT_FOUND", `job 없음: ${t}`, 404);
676
+ if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
583
677
  e.controller.abort();
584
678
  }
585
679
  waitFor(t) {
586
680
  const e = this.jobs.get(t);
587
- if (!e) throw new m("NOT_FOUND", `job 없음: ${t}`, 404);
681
+ if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
588
682
  return e.done;
589
683
  }
590
684
  }
591
- const Dt = 4816, Ft = 1024 * 1024, Lt = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
592
- function _(n) {
685
+ const Jt = 4816, Bt = 1024 * 1024, Ht = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
686
+ function j(n) {
593
687
  return new Promise((t, e) => {
594
688
  const o = [];
595
- let r = 0;
596
- n.on("data", (s) => {
597
- if (r += s.length, r > Ft) {
598
- 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();
599
693
  return;
600
694
  }
601
- o.push(s);
695
+ o.push(i);
602
696
  }), n.on("end", () => {
603
697
  if (o.length === 0) {
604
698
  t(void 0);
@@ -607,7 +701,7 @@ function _(n) {
607
701
  try {
608
702
  t(JSON.parse(Buffer.concat(o).toString("utf8")));
609
703
  } catch {
610
- e(new m("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
704
+ e(new g("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
611
705
  }
612
706
  }), n.on("error", e);
613
707
  });
@@ -616,26 +710,26 @@ function I(n, t, e) {
616
710
  const o = JSON.stringify(e);
617
711
  n.writeHead(t, { "content-type": "application/json" }), n.end(o);
618
712
  }
619
- async function Mt(n) {
620
- 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);
621
- async function i(c) {
622
- const l = await _(c), { selection: u, outerHTML: S, figmaUrl: y, instruction: w } = l ?? {}, E = It(String(y ?? ""));
623
- if (typeof u != "object" || u === null || typeof u.pagePath != "string" || typeof u.selector != "string" || typeof S != "string")
624
- throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
625
- const x = K({ projectRoot: t, homeDir: e, env: process.env });
626
- if (!x) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
627
- const { nodeJson: k, imagePath: b } = await Ot(E, x, f(t, ".pubuilder/cache"), r), A = Nt({
628
- 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,
629
723
  outerHTML: S ?? "",
630
- figma: { url: String(y), nodeJson: k, imagePath: b },
631
- skills: s.enabledSkillContents(),
724
+ figma: { url: String(y), nodeJson: x, imagePath: $ },
725
+ skills: i.enabledSkillContents(),
632
726
  instruction: typeof w == "string" ? w.trim() : void 0
633
727
  });
634
- return { status: 202, body: { jobId: a.start({ prompt: A, cwd: t }) } };
728
+ return { status: 202, body: { jobId: a.start({ prompt: O, cwd: t }) } };
635
729
  }
636
- function h(c, l, u) {
637
- if (!a.has(u)) throw new m("NOT_FOUND", `job 없음: ${u}`, 404);
638
- 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, {
639
733
  "content-type": "text/event-stream",
640
734
  "cache-control": "no-cache",
641
735
  connection: "keep-alive"
@@ -643,103 +737,114 @@ async function Mt(n) {
643
737
  let S = () => {
644
738
  };
645
739
  const y = (w) => {
646
- l.write(`data: ${JSON.stringify(w)}
740
+ u.write(`data: ${JSON.stringify(w)}
647
741
 
648
- `), (w.type === "done" || w.type === "error") && (S(), l.end());
742
+ `), (w.type === "done" || w.type === "error") && (S(), u.end());
649
743
  };
650
- S = a.subscribe(u, y), c.on("close", S);
744
+ S = a.subscribe(f, y), l.on("close", S);
651
745
  }
652
- const p = st((c, l) => {
746
+ const p = ut((l, u) => {
653
747
  (async () => {
654
748
  try {
655
- const u = c.headers.origin;
656
- if (u && !Lt.test(u)) {
657
- I(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이에요" });
658
752
  return;
659
753
  }
660
- 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") {
661
- l.writeHead(204), l.end();
754
+ if (f && (u.setHeader("Access-Control-Allow-Origin", f), u.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE"), u.setHeader("Access-Control-Allow-Headers", "content-type")), l.method === "OPTIONS") {
755
+ u.writeHead(204), u.end();
662
756
  return;
663
757
  }
664
- const y = new URL(c.url ?? "/", "http://localhost").pathname, w = c.method ?? "GET";
758
+ const y = new URL(l.url ?? "/", "http://localhost").pathname, w = l.method ?? "GET";
665
759
  if (w === "GET" && y === "/api/health") {
666
- I(l, 200, { ok: !0 });
760
+ I(u, 200, { ok: !0 });
667
761
  return;
668
762
  }
669
763
  if (w === "GET" && y === "/api/skills") {
670
- I(l, 200, s.list());
764
+ I(u, 200, i.list());
671
765
  return;
672
766
  }
673
767
  if (w === "POST" && y === "/api/skills") {
674
- const b = await _(c), A = s.upload(b.filename, b.content);
675
- I(l, 201, A);
768
+ const $ = await j(l), O = i.upload($.filename, $.content);
769
+ I(u, 201, O);
676
770
  return;
677
771
  }
678
772
  const E = y.match(/^\/api\/skills\/([^/]+)$/);
679
773
  if (E && w === "PATCH") {
680
- const b = decodeURIComponent(E[1]), A = await _(c);
681
- s.setEnabled(b, A.enabled), l.writeHead(204), l.end();
774
+ const $ = decodeURIComponent(E[1]), O = await j(l);
775
+ i.setEnabled($, O.enabled), u.writeHead(204), u.end();
682
776
  return;
683
777
  }
684
778
  if (E && w === "DELETE") {
685
- const b = decodeURIComponent(E[1]);
686
- s.remove(b), l.writeHead(204), l.end();
779
+ const $ = decodeURIComponent(E[1]);
780
+ i.remove($), u.writeHead(204), u.end();
687
781
  return;
688
782
  }
689
783
  if (w === "GET" && y === "/api/settings/figma-token") {
690
- const b = K({ projectRoot: t, homeDir: e, env: process.env });
691
- I(l, 200, { configured: b != null, masked: b ? At(b) : null });
784
+ const $ = V({ projectRoot: t, homeDir: e, env: process.env });
785
+ I(u, 200, { configured: $ != null, masked: $ ? jt($) : null });
692
786
  return;
693
787
  }
694
788
  if (w === "PUT" && y === "/api/settings/figma-token") {
695
- const b = await _(c);
696
- Tt(e, b.token), l.writeHead(204), l.end();
789
+ const $ = await j(l);
790
+ Nt(e, $.token), u.writeHead(204), u.end();
697
791
  return;
698
792
  }
699
793
  if (w === "DELETE" && y === "/api/settings/figma-token") {
700
- Et(e), l.writeHead(204), l.end();
794
+ Ft(e), u.writeHead(204), u.end();
701
795
  return;
702
796
  }
703
797
  if (w === "POST" && y === "/api/publish") {
704
- const { status: b, body: A } = await i(c);
705
- I(l, b, A);
798
+ const { status: $, body: O } = await r(l);
799
+ I(u, $, O);
800
+ return;
801
+ }
802
+ if (w === "POST" && y === "/api/publish/suspend") {
803
+ I(u, 200, a.suspendRunning(t));
804
+ return;
805
+ }
806
+ if (w === "POST" && y === "/api/publish/suspend/resolve") {
807
+ const $ = await j(l);
808
+ if (!Array.isArray($?.resolutions))
809
+ throw new g("INVALID_JSON", "resolutions 배열이 필요해요", 400);
810
+ a.applyResolutions(t, $.resolutions), u.writeHead(204), u.end();
706
811
  return;
707
812
  }
708
- const x = y.match(/^\/api\/publish\/([^/]+)\/events$/);
709
- if (x && w === "GET") {
710
- h(c, l, x[1]);
813
+ const F = y.match(/^\/api\/publish\/([^/]+)\/events$/);
814
+ if (F && w === "GET") {
815
+ c(l, u, F[1]);
711
816
  return;
712
817
  }
713
- const k = y.match(/^\/api\/publish\/([^/]+)$/);
714
- if (k && w === "DELETE") {
715
- a.cancel(k[1]), l.writeHead(204), l.end();
818
+ const x = y.match(/^\/api\/publish\/([^/]+)$/);
819
+ if (x && w === "DELETE") {
820
+ a.cancel(x[1]), u.writeHead(204), u.end();
716
821
  return;
717
822
  }
718
- I(l, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
719
- } catch (u) {
720
- if (u instanceof m) {
721
- I(l, u.status, { code: u.code, message: u.message });
823
+ I(u, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
824
+ } catch (f) {
825
+ if (f instanceof g) {
826
+ I(u, f.status, { code: f.code, message: f.message });
722
827
  return;
723
828
  }
724
- const S = u instanceof Error ? u.message : String(u);
725
- I(l, 500, { code: "INTERNAL", message: S });
829
+ const S = f instanceof Error ? f.message : String(f);
830
+ I(u, 500, { code: "INTERNAL", message: S });
726
831
  }
727
832
  })();
728
- }), g = n.port ?? Dt;
729
- await new Promise((c) => p.listen(g, "127.0.0.1", c));
730
- const $ = 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();
731
836
  return {
732
- port: typeof $ == "object" && $ ? $.port : g,
837
+ port: typeof b == "object" && b ? b.port : m,
733
838
  close() {
734
839
  p.close();
735
840
  }
736
841
  };
737
842
  }
738
- async function tt(n, t) {
739
- 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 });
740
845
  return console.log(`[pubuilder] 컴패니언 서버 실행 중: http://localhost:${e.port}`), console.log("[pubuilder] 미니맵의 퍼블리싱 패널이 이 서버에 연결됩니다. Ctrl+C로 종료."), e;
741
846
  }
742
- function et(n) {
847
+ function st(n) {
743
848
  const t = n.indexOf("--port");
744
849
  if (t === -1) return;
745
850
  const e = Number(n[t + 1]);
@@ -747,96 +852,96 @@ function et(n) {
747
852
  throw new Error("--port 값이 유효하지 않아요 (0-65535)");
748
853
  return e;
749
854
  }
750
- async function Gt(n, t) {
855
+ async function Vt(n, t) {
751
856
  const e = t.indexOf("--"), o = e === -1 ? [] : t.slice(e + 1);
752
857
  if (o.length === 0)
753
858
  return console.error("사용법: pubuilder dev [--port N] -- <dev 명령> (예: pubuilder dev -- next dev)"), 1;
754
- const r = await tt(n, { port: et(t.slice(0, e === -1 ? void 0 : e)) });
755
- return new Promise((s) => {
756
- const a = X(o[0], o.slice(1), { cwd: n, stdio: "inherit", env: process.env }), i = () => a.kill("SIGINT"), h = () => a.kill("SIGTERM");
757
- 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);
758
863
  const p = () => {
759
- process.off("SIGINT", i), process.off("SIGTERM", h), r.close();
864
+ process.off("SIGINT", r), process.off("SIGTERM", c), s.close();
760
865
  };
761
- a.on("error", (g) => {
762
- console.error(`[pubuilder] 명령 실행 실패: ${g.message}`), p(), s(1);
763
- }), a.on("close", (g, $) => {
764
- p(), s($ ? 128 + (at.signals[$] ?? 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);
765
870
  });
766
871
  });
767
872
  }
768
- const Ut = [bt, vt];
769
- function Ct(n) {
770
- const t = f(n, "package.json");
771
- if (!P(t)) return !1;
873
+ const Wt = [Tt, Et];
874
+ function zt(n) {
875
+ const t = d(n, "package.json");
876
+ if (!v(t)) return !1;
772
877
  try {
773
- const e = JSON.parse(R(t, "utf8"));
878
+ const e = JSON.parse(A(t, "utf8"));
774
879
  return !!(e.dependencies?.next ?? e.devDependencies?.next);
775
880
  } catch {
776
881
  return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
777
882
  }
778
883
  }
779
- function Jt(n) {
780
- if (!Ct(n)) return null;
781
- 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));
782
887
  if (t.length === 0) return null;
783
888
  const e = /* @__PURE__ */ new Map(), o = [];
784
- for (const r of t)
785
- for (const s of r.scan(n)) {
786
- 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);
787
892
  if (a) {
788
- console.warn(`[pubuilder] path 충돌: ${s.routePath} (${a} 우선, ${r.name} 무시)`);
893
+ console.warn(`[pubuilder] path 충돌: ${i.routePath} (${a} 우선, ${s.name} 무시)`);
789
894
  continue;
790
895
  }
791
- e.set(s.routePath, r.name), o.push(s);
896
+ e.set(i.routePath, s.name), o.push(i);
792
897
  }
793
898
  return o;
794
899
  }
795
- async function Ht(n) {
796
- 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 });
797
902
  if (!e || !Array.isArray(e.pages))
798
903
  throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
799
904
  return e;
800
905
  }
801
- async function Bt(n, t) {
802
- 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");
803
908
  if (e === null)
804
- return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !P(o) && !t.dryRun && (N(o, Pt), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
805
- if (P(o)) {
806
- const i = await Ht(o), { config: h, added: p, removed: g } = ft(i, e);
807
- M(h), t.dryRun || N(o, G(h, { newPaths: new Set(p) }));
808
- for (const $ of p) console.log(`[pubuilder] + 추가됨: ${$}`);
809
- for (const $ of g) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${$}`);
810
- return p.length === 0 && g.length === 0 && console.log("[pubuilder] 변경 없음"), t.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: p, removed: g };
811
- }
812
- const s = { pages: dt(e) };
813
- M(s);
814
- const a = e.map((i) => i.routePath);
815
- return t.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (N(o, G(s, { newPaths: new Set(a) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${a.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: a };
816
- }
817
- async function Kt(n) {
909
+ return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !v(o) && !t.dryRun && (T(o, At), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
910
+ if (v(o)) {
911
+ const r = await Zt(o), { config: c, added: p, removed: m } = bt(r, e);
912
+ G(c), t.dryRun || T(o, U(c, { newPaths: new Set(p) }));
913
+ for (const b of p) console.log(`[pubuilder] + 추가됨: ${b}`);
914
+ for (const b of m) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${b}`);
915
+ return p.length === 0 && m.length === 0 && console.log("[pubuilder] 변경 없음"), t.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: p, removed: m };
916
+ }
917
+ const i = { pages: yt(e) };
918
+ G(i);
919
+ const a = e.map((r) => r.routePath);
920
+ return t.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (T(o, U(i, { newPaths: new Set(a) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${a.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: a };
921
+ }
922
+ async function Qt(n) {
818
923
  const [t, ...e] = n;
819
924
  try {
820
- return t === "scan" ? (await Bt(process.cwd(), { dryRun: e.includes("--dry-run") })).kind === "unsupported" ? 1 : 0 : t === "serve" ? (await tt(process.cwd(), { port: et(e) }), await new Promise(() => {
821
- })) : 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);
822
927
  } catch (o) {
823
928
  return console.error(`[pubuilder] 실패: ${o instanceof Error ? o.message : String(o)}`), 1;
824
929
  }
825
930
  }
826
- function Vt() {
931
+ function qt() {
827
932
  const n = process.argv[1];
828
933
  if (!n) return !1;
829
934
  try {
830
- return nt(n) === ot(import.meta.url);
935
+ return rt(n) === ct(import.meta.url);
831
936
  } catch {
832
937
  return !1;
833
938
  }
834
939
  }
835
- Vt() && Kt(process.argv.slice(2)).then(
940
+ qt() && Qt(process.argv.slice(2)).then(
836
941
  (n) => process.exit(n),
837
942
  () => process.exit(1)
838
943
  );
839
944
  export {
840
- Kt as main,
841
- Bt as runScan
945
+ Qt as main,
946
+ Yt as runScan
842
947
  };