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