pubuilder 0.7.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 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
- e != null && n.append(t, String(e));
13
- }
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);
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
- 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
- depth: a
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
- if (e.has(p)) throw new Error(`[pubuilder] 중복 path: ${p}`);
44
- e.add(p), t.push({
45
- id: p,
46
- path: p,
47
- title: c.title,
20
+ M(i, f, g);
21
+ const a = i.toString();
22
+ return `${c}${a ? `?${a}` : ""}${o}`;
23
+ }
24
+ function C(t) {
25
+ const e = [], n = /* @__PURE__ */ new Set(), o = (r, s, c) => {
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 (n.has(i.path)) throw new Error(`[pubuilder] 중복 path: ${i.path}`);
30
+ if (n.add(i.path), e.push({
31
+ id: i.path,
32
+ path: i.path,
33
+ title: i.title,
34
+ external: i.external ?? /^https?:\/\//.test(i.path),
35
+ parentId: s,
36
+ depth: c
37
+ }), i.variants?.length) {
38
+ if (i.external ?? /^https?:\/\//.test(i.path))
39
+ throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${i.path}`);
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
- parentId: r.path,
50
- depth: a + 1
49
+ parentId: i.path,
50
+ depth: c + 1
51
51
  });
52
52
  }
53
53
  }
54
- r.children?.length && o(r.children, r.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 ?? dt };
59
- }
60
- const P = " ";
61
- function mt(n) {
62
- return n.split("/").map((t) => /^\[{1,2}\.{0,3}.*\]{1,2}$/.test(t) && t.startsWith("[") ? "1" : t).join("/");
63
- }
64
- function N(n) {
65
- return JSON.stringify(n);
66
- }
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
- if (!n.children?.length && !n.variants?.length && !n.external)
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)} },`
57
+ if (o(t.pages, null, 0), e.length === 0) throw new Error("[pubuilder] pages가 비어 있습니다");
58
+ return { nodes: e, viewport: t.viewport ?? wt };
59
+ }
60
+ const v = " ";
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
+ r.push(`${o}${v}variants: [`);
75
+ for (const i of t.variants)
76
+ r.push(
77
+ `${o}${v}${v}{ title: ${N(i.title)}, query: ${N(i.query)} },`
78
78
  );
79
- s.push(`${o}${P}],`);
79
+ r.push(`${o}${v}],`);
80
80
  }
81
- if (n.children?.length) {
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}],`);
81
+ if (t.children?.length) {
82
+ r.push(`${o}${v}children: [`);
83
+ for (const i of t.children) r.push(...tt(i, e + 2, n));
84
+ r.push(`${o}${v}],`);
85
85
  }
86
- return s.push(`${o}},`), s;
86
+ return r.push(`${o}},`), r;
87
87
  }
88
- function U(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(`${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(`
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
+ return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
97
97
  `);
98
98
  }
99
- function R(n) {
100
- return n === "/" ? [] : n.replace(/^\//, "").split("/");
101
- }
102
- function wt(n, t) {
103
- return n.length >= t.length ? !1 : n.every((e, o) => e === t[o]);
104
- }
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
- }), e = [], o = [];
110
- for (const s of t) {
111
- const i = R(s.routePath), a = {
112
- path: s.routePath,
113
- title: s.title ?? i.at(-1) ?? "/"
99
+ function P(t) {
100
+ return t === "/" ? [] : t.replace(/^\//, "").split("/");
101
+ }
102
+ function St(t, e) {
103
+ return t.length >= e.length ? !1 : t.every((n, o) => n === e[o]);
104
+ }
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
+ path: r.routePath,
113
+ title: r.title ?? s.at(-1) ?? "/"
114
114
  };
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
- }
120
- return e;
121
- }
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])
115
+ let i;
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
+ }
120
+ return n;
121
+ }
122
+ function U(t, e) {
123
+ const n = P(t), o = P(e);
124
+ for (let r = 0; r < o.length; r++) {
125
+ const s = o[r];
126
+ if (/^\[\[\.\.\..+\]\]$/.test(s))
127
+ return r <= n.length;
128
+ if (/^\[\.\.\..+\]$/.test(s))
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 q(n, t = []) {
137
- for (const e of n)
138
- t.push({ page: e, segments: R(e.path) }), e.children && q(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 J(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 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
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 h of b) {
153
- const l = R(h.routePath), u = { path: h.routePath, title: h.title ?? l.at(-1) ?? "/" };
154
- let f;
155
- for (const S of m) {
156
- if (J(S.page)) continue;
157
- const y = S.segments;
158
- y.length >= l.length || y.every((w, E) => w === l[E]) && (!f || y.length > f.segments.length) && (f = S);
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
+ for (const S of g) {
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
- f ? (f.page.children ??= []).push(u) : e.pages.push(u), m.push({ page: u, segments: l }), p.push(h.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: c };
162
+ return { config: n, added: f, removed: a };
163
163
  }
164
- const $t = /^page\.(tsx|jsx|ts|js)$/, St = /^\(.+\)$/, Pt = /^\(\.{1,3}\)/;
165
- function B(n) {
166
- for (const t of ["app", "src/app"])
167
- if (v(d(n, t))) return d(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 vt(n) {
171
- const t = [];
172
- for (const e of n) {
173
- if (e.startsWith("@") || e.startsWith("_") || Pt.test(e)) return null;
174
- St.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 It(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 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 });
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 Tt = {
188
+ const Ot = {
189
189
  name: "next-app",
190
- detect(n) {
191
- return B(n) !== null;
190
+ detect(t) {
191
+ return H(t) !== null;
192
192
  },
193
- scan(n) {
194
- const t = B(n);
195
- if (!t) return [];
196
- const e = [];
197
- tt(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: s, segments: i } of e) {
200
- const a = vt(i);
201
- a !== null && o.push({
202
- routePath: a,
203
- isDynamic: /\[.+\]/.test(a),
204
- title: It(A(s, "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)$/, Rt = /* @__PURE__ */ new Set(["_app", "_document", "_error", "404", "500"]);
210
- function K(n) {
211
- for (const t of ["pages", "src/pages"])
212
- if (v(d(n, t))) return d(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 et(n, t, e) {
216
- for (const o of k(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
- et(d(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 s = o.name.replace(H, "");
224
- if (Rt.has(s)) continue;
225
- const a = `/${(s === "index" ? t : [...t, s]).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 Et = {
229
+ const xt = {
230
230
  name: "next-pages",
231
- detect(n) {
232
- return K(n) !== null;
231
+ detect(t) {
232
+ return K(t) !== null;
233
233
  },
234
- scan(n) {
235
- const t = K(n);
236
- if (!t) return [];
237
- const e = [];
238
- return et(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
- }, At = `import { defineIA } from "pubuilder/config"
240
+ }, Dt = `import { defineIA } from "pubuilder/config"
241
241
 
242
242
  // pubuilder scan은 아직 이 프로젝트의 라우팅 방식을 지원하지 않아요 (v1은 Next.js 전용).
243
243
  // 아래 예시를 참고해 앱의 화면 구조를 직접 작성해주세요.
@@ -257,172 +257,236 @@ export default defineIA({
257
257
  ],
258
258
  })
259
259
  `;
260
- class g extends Error {
261
- constructor(t, e, o = 400) {
262
- super(`${t}: ${e}`), this.code = t, this.status = o;
260
+ class m extends Error {
261
+ constructor(e, n, o = 400) {
262
+ super(`${e}: ${n}`), this.code = e, this.status = o;
263
263
  }
264
264
  }
265
- function Ot(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
- throw new g("INVALID_FIGMA_URL", "URL 형식이 아니에요");
270
+ throw new m("INVALID_FIGMA_URL", "URL 형식이 아니에요");
271
271
  }
272
- if (!/(^|\.)figma\.com$/.test(t.hostname))
273
- throw new g("INVALID_FIGMA_URL", "figma.com 링크가 아니에요");
274
- const e = t.pathname.match(/^\/(?:design|file)\/([A-Za-z0-9]+)(?:\/|$)/);
275
- if (!e) throw new g("INVALID_FIGMA_URL", "fileKey를 찾지 못했어요");
276
- const o = t.searchParams.get("node-id");
272
+ if (!/(^|\.)figma\.com$/.test(e.hostname))
273
+ throw new m("INVALID_FIGMA_URL", "figma.com 링크가 아니에요");
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
- throw new g("INVALID_FIGMA_URL", "node-id가 없어요. Figma에서 프레임 우클릭 → Copy link로 복사해주세요");
279
- return { fileKey: e[1], nodeId: o.replace(/-/g, ":") };
278
+ throw new m("INVALID_FIGMA_URL", "node-id가 없어요. Figma에서 프레임 우클릭 → Copy link로 복사해주세요");
279
+ return { fileKey: n[1], nodeId: o.replace(/-/g, ":") };
280
280
  }
281
- function L(n) {
282
- return d(n, ".pubuilder", "credentials.json");
281
+ function L(t) {
282
+ return h(t, ".pubuilder", "credentials.json");
283
283
  }
284
- function V(n) {
285
- const t = L(n.homeDir);
286
- if (v(t))
284
+ function V(t) {
285
+ const e = L(t.homeDir);
286
+ if (I(e))
287
287
  try {
288
- const o = JSON.parse(A(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 = d(n.projectRoot, ".env");
294
- if (v(e)) {
295
- const o = A(e, "utf8").split(`
296
- `).find((s) => s.trim().startsWith("FIGMA_TOKEN="));
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
+ `).find((r) => r.trim().startsWith("FIGMA_TOKEN="));
297
297
  if (o) {
298
- const s = o.slice(o.indexOf("=") + 1).trim().replace(/^["']|["']$/g, "");
299
- if (s) return s;
298
+ const r = o.slice(o.indexOf("=") + 1).trim().replace(/^["']|["']$/g, "");
299
+ if (r) return r;
300
300
  }
301
301
  }
302
302
  return null;
303
303
  }
304
- function Nt(n, t) {
305
- _(d(n, ".pubuilder"), { recursive: !0 }), T(L(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 Ft(n) {
308
- D(L(n), { force: !0 });
307
+ function kt(t) {
308
+ _(L(t), { force: !0 });
309
309
  }
310
- function jt(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 xt(n, t, e, o = fetch) {
314
- const s = { "X-Figma-Token": t }, i = await o(
315
- `https://api.figma.com/v1/files/${n.fileKey}/nodes?ids=${encodeURIComponent(n.nodeId)}`,
316
- { headers: s }
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
+ { headers: r }
317
317
  );
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
- `https://api.figma.com/v1/images/${n.fileKey}?ids=${encodeURIComponent(n.nodeId)}&format=png&scale=2`,
324
- { headers: s }
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[t.nodeId]?.document;
321
+ if (!i) throw new m("FIGMA_API_ERROR", "해당 노드를 찾지 못했어요. URL의 node-id를 확인해주세요", 404);
322
+ const a = await o(
323
+ `https://api.figma.com/v1/images/${t.fileKey}?ids=${encodeURIComponent(t.nodeId)}&format=png&scale=2`,
324
+ { headers: r }
325
325
  );
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
- 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 _t {
326
+ if (!a.ok) throw new m("FIGMA_API_ERROR", `이미지 export 실패 (${a.status})`, 502);
327
+ const g = (await a.json()).images[t.nodeId];
328
+ if (!g) throw new m("FIGMA_API_ERROR", "이미지 export URL이 비어 있어요", 502);
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 d(this.projectRoot, ".pubuilder", "settings.json");
349
+ return h(this.projectRoot, ".pubuilder", "settings.json");
348
350
  }
349
351
  loadSettings() {
350
- if (!v(this.settingsPath())) return { skills: {} };
352
+ if (!I(this.settingsPath())) return { skills: {} };
351
353
  try {
352
- return JSON.parse(A(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
- _(d(this.projectRoot, ".pubuilder"), { recursive: !0 }), T(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 (!v(t)) return [];
362
+ scanDir(e, n) {
363
+ if (!I(e)) return [];
362
364
  const o = [];
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"));
365
+ for (const r of k(e, { withFileTypes: !0 })) {
366
+ if (!r.isDirectory()) continue;
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}:${s.name}`,
370
- name: a.name ?? s.name,
371
- description: a.description ?? "",
372
- source: e,
373
- path: i
371
+ id: `${n}:${r.name}`,
372
+ name: c.name ?? r.name,
373
+ description: c.description ?? "",
374
+ source: n,
375
+ path: s
374
376
  });
375
377
  }
376
378
  return o;
377
379
  }
378
380
  scanAll() {
379
381
  return [
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")
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((s) => s.id === t)) throw new g("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 = 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 };
401
+ upload(e, n) {
402
+ const o = W(n), r = (o.name ?? e.replace(/\.md$/i, "")).trim();
403
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(r))
404
+ throw new m("INVALID_NAME", `스킬 이름이 유효하지 않아요: ${r}`, 400);
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 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);
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 });
414
+ const r = this.loadSettings();
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: A(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 kt(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((i) => `## 스킬: ${i.name}
489
+ ${t.skills.map((i) => `## 스킬: ${i.name}
426
490
 
427
491
  ${i.content}`).join(`
428
492
 
@@ -431,86 +495,88 @@ ${i.content}`).join(`
431
495
  `)}
432
496
  ` : `
433
497
  다른 스킬은 스스로 판단해 사용하지 마세요. 아래에 명시된 스킬 지침만 따르세요 (이번 요청에는 없음).
434
- `, s = 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→프로젝트 토큰 매핑 힌트):
524
+ ${r}
460
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 Dt(n) {
534
+ function Bt(t) {
469
535
  try {
470
- return Z("git", ["stash", "create"], { cwd: n, encoding: "utf8", stdio: "pipe" }).trim() || "HEAD";
536
+ return Q("git", ["stash", "create"], { cwd: t, encoding: "utf8", stdio: "pipe" }).trim() || "HEAD";
471
537
  } catch {
472
538
  return null;
473
539
  }
474
540
  }
475
- function nt(n, t, e) {
541
+ function st(t, e, n) {
476
542
  try {
477
- return Z("git", ["show", `${t}:${e}`], { cwd: n, encoding: "utf8", stdio: "pipe" });
543
+ return Q("git", ["show", `${e}:${n}`], { cwd: t, encoding: "utf8", stdio: "pipe" });
478
544
  } catch {
479
545
  return null;
480
546
  }
481
547
  }
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);
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);
485
551
  }
486
- function Mt(n) {
487
- let t;
552
+ function Jt(t) {
553
+ let e;
488
554
  try {
489
- t = JSON.parse(n);
555
+ e = JSON.parse(t);
490
556
  } catch {
491
557
  return [];
492
558
  }
493
- if (t.type === "result")
559
+ if (e.type === "result")
494
560
  return [
495
- 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 ?? "실패") }
496
562
  ];
497
- if (t.type === "assistant") {
498
- const e = [];
499
- for (const o of t.message?.content ?? [])
500
- if (o.type === "text" && o.text && e.push({ type: "log", text: o.text }), o.type === "tool_use") {
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 } : {} });
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 } : {} });
503
569
  }
504
- return e;
570
+ return n;
505
571
  }
506
572
  return [];
507
573
  }
508
- const z = 600 * 1e3, Gt = {
509
- run({ prompt: n, cwd: t, onEvent: e, signal: o }) {
510
- return new Promise((s) => {
511
- const i = Y("claude", [
574
+ const X = 600 * 1e3, Kt = {
575
+ run({ prompt: t, cwd: e, onEvent: n, signal: o }) {
576
+ return new Promise((r) => {
577
+ const s = q("claude", [
512
578
  "-p",
513
- n,
579
+ t,
514
580
  "--output-format",
515
581
  "stream-json",
516
582
  "--verbose",
@@ -522,52 +588,52 @@ const z = 600 * 1e3, Gt = {
522
588
  // 설정을 로드하면 개발자 머신의 관대한 allow 규칙이 화이트리스트를 무력화한다 (e2e에서 확인)
523
589
  "--setting-sources",
524
590
  ""
525
- ], { cwd: t, env: process.env });
526
- let a = !1;
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분간 응답이 없어 중단했어요" });
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 = () => {
596
+ s.kill("SIGKILL"), i({ type: "error", text: "에이전트가 10분간 응답이 없어 중단했어요" });
531
597
  };
532
- let p = setTimeout(c, z);
533
- const m = () => {
534
- a || (clearTimeout(p), p = setTimeout(c, z));
598
+ let f = setTimeout(a, X);
599
+ const g = () => {
600
+ c || (clearTimeout(f), f = setTimeout(a, X));
535
601
  };
536
602
  o?.addEventListener("abort", () => {
537
- i.kill("SIGKILL"), r({ type: "error", text: "사용자가 중단했어요" });
538
- }), i.on("error", (l) => {
539
- r({
603
+ s.kill("SIGKILL"), i({ type: "error", text: "사용자가 중단했어요" });
604
+ }), s.on("error", (l) => {
605
+ i({
540
606
  type: "error",
541
607
  text: l.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${l.message}`
542
608
  });
543
609
  });
544
610
  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) {
611
+ s.stderr.on("data", (l) => b.push(String(l)));
612
+ let d = !1;
613
+ yt({ input: s.stdout }).on("line", (l) => {
614
+ g();
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) {
554
620
  const u = b.join("").trim();
555
- r({ type: "error", text: u || `claude가 코드 ${l}로 종료됐어요. claude login 상태를 확인해주세요` });
621
+ i({ type: "error", text: u || `claude가 코드 ${l}로 종료됐어요. claude login 상태를 확인해주세요` });
556
622
  } else
557
- r();
623
+ i();
558
624
  });
559
625
  });
560
626
  }
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) => {
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) => {
565
631
  try {
566
- return A(d(o, s), "utf8");
632
+ return E(h(o, r), "utf8");
567
633
  } catch {
568
634
  return "";
569
635
  }
570
- }), this.writeFile = e.writeFile ?? ((o, s, i) => T(d(o, s), i));
636
+ }), this.writeFile = n.writeFile ?? ((o, r, s) => A(h(o, r), s));
571
637
  }
572
638
  jobs = /* @__PURE__ */ new Map();
573
639
  seq = 0;
@@ -579,56 +645,56 @@ class Ct {
579
645
  /** suspend 시 파일별 baseline ref를 보관 → resolve의 revert에서 사용 */
580
646
  pendingBaseline = /* @__PURE__ */ new Map();
581
647
  activeCount() {
582
- let t = 0;
583
- for (const e of this.jobs.values()) e.finished || t++;
584
- return t;
585
- }
586
- start(t) {
587
- if (this.activeCount() >= X)
588
- throw new g("BUSY", `동시 퍼블리싱은 최대 ${X}개예요. 하나가 끝나면 다시 시도해주세요`, 409);
589
- const e = ++this.seq, o = `job-${e}`, s = new AbortController(), i = {
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 = {
590
656
  id: o,
591
- seq: e,
657
+ seq: n,
592
658
  events: [],
593
659
  listeners: /* @__PURE__ */ new Set(),
594
- controller: s,
660
+ controller: r,
595
661
  done: Promise.resolve(),
596
662
  finished: !1,
597
- baselineRef: this.capture(t.cwd),
663
+ baselineRef: this.capture(e.cwd),
598
664
  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);
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);
603
669
  }
604
- i.events.push(r);
605
- for (const c of i.listeners) c(r);
670
+ s.events.push(i);
671
+ for (const a of s.listeners) a(i);
606
672
  };
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;
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;
610
676
  }
611
677
  /** finished job이 상한을 넘으면 가장 오래된 것부터 제거 (active job은 finished=false라 대상에서 제외) */
612
678
  evictFinishedJobs() {
613
- const t = [...this.jobs.entries()].filter(([, e]) => e.finished).map(([e]) => e);
614
- for (; t.length > Ut; ) {
615
- const e = t.shift();
616
- if (e === void 0) break;
617
- 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);
618
684
  }
619
685
  }
620
686
  /** SSE 핸들러가 헤더 전송 전에 job 존재를 확인할 때 사용 (subscribe는 replay 부작용이 있어 분리) */
621
- has(t) {
622
- return this.jobs.has(t);
687
+ has(e) {
688
+ return this.jobs.has(e);
623
689
  }
624
690
  /** job이 편집한 파일 목록 (상대경로, 정렬) */
625
- touchedFiles(t) {
626
- const e = this.jobs.get(t);
627
- return e ? [...e.touchedFiles].sort() : [];
691
+ touchedFiles(e) {
692
+ const n = this.jobs.get(e);
693
+ return n ? [...n.touchedFiles].sort() : [];
628
694
  }
629
695
  /** job 시작 시점 baseline ref */
630
- baselineRef(t) {
631
- return this.jobs.get(t)?.baselineRef ?? null;
696
+ baselineRef(e) {
697
+ return this.jobs.get(e)?.baselineRef ?? null;
632
698
  }
633
699
  /**
634
700
  * 진행 중(running)인 job을 전부 abort하고, 그들이 편집한 파일을 분류한다.
@@ -636,99 +702,99 @@ class Ct {
636
702
  * - 충돌: 완료(finished)된 job도 건드린 파일 → 사용자 해결 대상으로 반환
637
703
  * 파일별 baseline은 그 파일을 건드린 running job 중 가장 이른(seq 최소) job의 것.
638
704
  */
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();
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();
642
708
  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 = [];
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 = [];
651
717
  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 };
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 };
655
721
  }
656
722
  /**
657
723
  * 충돌 파일에 대한 사용자 결정을 적용한다.
658
724
  * revert → baseline 복원, edit → content로 덮어쓰기, keep → 그대로 둔다.
659
725
  */
660
- applyResolutions(t, e) {
661
- for (const o of e)
726
+ applyResolutions(e, n) {
727
+ for (const o of n)
662
728
  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 ?? "");
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 ?? "");
666
732
  this.pendingBaseline.clear();
667
733
  }
668
- subscribe(t, e) {
669
- const o = this.jobs.get(t);
670
- if (!o) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
671
- for (const s of o.events) e(s);
672
- return o.listeners.add(e), () => o.listeners.delete(e);
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);
673
739
  }
674
- cancel(t) {
675
- const e = this.jobs.get(t);
676
- if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
677
- e.controller.abort();
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();
678
744
  }
679
- waitFor(t) {
680
- const e = this.jobs.get(t);
681
- if (!e) throw new g("NOT_FOUND", `job 없음: ${t}`, 404);
682
- return e.done;
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;
683
749
  }
684
750
  }
685
- const Jt = 4816, Bt = 1024 * 1024, Ht = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
686
- function j(n) {
687
- return new Promise((t, e) => {
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) => {
688
754
  const o = [];
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();
755
+ let r = 0;
756
+ t.on("data", (s) => {
757
+ if (r += s.length, r > Xt) {
758
+ n(new m("PAYLOAD_TOO_LARGE", "요청 본문이 너무 커요 (1MB 초과)", 413)), t.destroy();
693
759
  return;
694
760
  }
695
- o.push(i);
696
- }), n.on("end", () => {
761
+ o.push(s);
762
+ }), t.on("end", () => {
697
763
  if (o.length === 0) {
698
- t(void 0);
764
+ e(void 0);
699
765
  return;
700
766
  }
701
767
  try {
702
- t(JSON.parse(Buffer.concat(o).toString("utf8")));
768
+ e(JSON.parse(Buffer.concat(o).toString("utf8")));
703
769
  } catch {
704
- e(new g("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
770
+ n(new m("INVALID_JSON", "JSON 파싱에 실패했어요", 400));
705
771
  }
706
- }), n.on("error", e);
772
+ }), t.on("error", n);
707
773
  });
708
774
  }
709
- function I(n, t, e) {
710
- const o = JSON.stringify(e);
711
- n.writeHead(t, { "content-type": "application/json" }), n.end(o);
712
- }
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,
775
+ function R(t, e, n) {
776
+ const o = JSON.stringify(n);
777
+ t.writeHead(e, { "content-type": "application/json" }), t.end(o);
778
+ }
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")
784
+ throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
785
+ const x = V({ projectRoot: e, homeDir: n, env: process.env });
786
+ if (!x) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
787
+ const { nodeJson: F, imagePath: $ } = await Lt(T, x, h(e, ".pubuilder/cache"), r), O = Ut({
788
+ selection: p,
723
789
  outerHTML: S ?? "",
724
- figma: { url: String(y), nodeJson: x, imagePath: $ },
725
- skills: i.enabledSkillContents(),
726
- instruction: typeof w == "string" ? w.trim() : void 0
790
+ figma: { url: String(w), nodeJson: F, imagePath: $ },
791
+ skills: s.enabledSkillContents(),
792
+ instruction: typeof y == "string" ? y.trim() : void 0
727
793
  });
728
- return { status: 202, body: { jobId: a.start({ prompt: O, cwd: t }) } };
794
+ return { status: 202, body: { jobId: c.start({ prompt: O, cwd: e }) } };
729
795
  }
730
- function c(l, u, f) {
731
- if (!a.has(f)) throw new g("NOT_FOUND", `job 없음: ${f}`, 404);
796
+ function a(l, u, p) {
797
+ if (!c.has(p)) throw new m("NOT_FOUND", `job 없음: ${p}`, 404);
732
798
  u.writeHead(200, {
733
799
  "content-type": "text/event-stream",
734
800
  "cache-control": "no-cache",
@@ -736,212 +802,219 @@ async function Kt(n) {
736
802
  });
737
803
  let S = () => {
738
804
  };
739
- const y = (w) => {
740
- u.write(`data: ${JSON.stringify(w)}
805
+ const w = (y) => {
806
+ u.write(`data: ${JSON.stringify(y)}
741
807
 
742
- `), (w.type === "done" || w.type === "error") && (S(), u.end());
808
+ `), (y.type === "done" || y.type === "error") && (S(), u.end());
743
809
  };
744
- S = a.subscribe(f, y), l.on("close", S);
810
+ S = c.subscribe(p, w), l.on("close", S);
745
811
  }
746
- const p = ut((l, u) => {
812
+ const f = dt((l, u) => {
747
813
  (async () => {
748
814
  try {
749
- const f = l.headers.origin;
750
- if (f && !Ht.test(f)) {
751
- I(u, 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이에요" });
752
818
  return;
753
819
  }
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") {
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") {
755
821
  u.writeHead(204), u.end();
756
822
  return;
757
823
  }
758
- const y = new URL(l.url ?? "/", "http://localhost").pathname, w = l.method ?? "GET";
759
- if (w === "GET" && y === "/api/health") {
760
- I(u, 200, { ok: !0 });
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 });
761
827
  return;
762
828
  }
763
- if (w === "GET" && y === "/api/skills") {
764
- I(u, 200, i.list());
829
+ if (y === "GET" && w === "/api/skills") {
830
+ R(u, 200, s.list());
765
831
  return;
766
832
  }
767
- if (w === "POST" && y === "/api/skills") {
768
- const $ = await j(l), O = i.upload($.filename, $.content);
769
- I(u, 201, O);
833
+ if (y === "POST" && w === "/api/skills") {
834
+ const $ = await D(l), O = s.upload($.filename, $.content);
835
+ R(u, 201, O);
770
836
  return;
771
837
  }
772
- const E = y.match(/^\/api\/skills\/([^/]+)$/);
773
- if (E && w === "PATCH") {
774
- const $ = decodeURIComponent(E[1]), O = await j(l);
775
- i.setEnabled($, O.enabled), u.writeHead(204), u.end();
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();
776
842
  return;
777
843
  }
778
- if (E && w === "DELETE") {
779
- const $ = decodeURIComponent(E[1]);
780
- i.remove($), u.writeHead(204), u.end();
844
+ if (T && y === "DELETE") {
845
+ const $ = decodeURIComponent(T[1]);
846
+ s.remove($), u.writeHead(204), u.end();
781
847
  return;
782
848
  }
783
- if (w === "GET" && y === "/api/settings/figma-token") {
784
- const $ = V({ projectRoot: t, homeDir: e, env: process.env });
785
- I(u, 200, { configured: $ != null, masked: $ ? jt($) : null });
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 });
786
852
  return;
787
853
  }
788
- if (w === "PUT" && y === "/api/settings/figma-token") {
789
- const $ = await j(l);
790
- Nt(e, $.token), u.writeHead(204), u.end();
854
+ if (y === "PUT" && w === "/api/settings/figma-token") {
855
+ const $ = await D(l);
856
+ jt(n, $.token), u.writeHead(204), u.end();
791
857
  return;
792
858
  }
793
- if (w === "DELETE" && y === "/api/settings/figma-token") {
794
- Ft(e), u.writeHead(204), u.end();
859
+ if (y === "DELETE" && w === "/api/settings/figma-token") {
860
+ kt(n), u.writeHead(204), u.end();
795
861
  return;
796
862
  }
797
- if (w === "POST" && y === "/api/publish") {
798
- const { status: $, body: O } = await r(l);
799
- I(u, $, O);
863
+ if (y === "POST" && w === "/api/publish") {
864
+ const { status: $, body: O } = await i(l);
865
+ R(u, $, O);
800
866
  return;
801
867
  }
802
- if (w === "POST" && y === "/api/publish/suspend") {
803
- I(u, 200, a.suspendRunning(t));
868
+ if (y === "POST" && w === "/api/publish/suspend") {
869
+ R(u, 200, c.suspendRunning(e));
804
870
  return;
805
871
  }
806
- if (w === "POST" && y === "/api/publish/suspend/resolve") {
807
- const $ = await j(l);
872
+ if (y === "POST" && w === "/api/publish/suspend/resolve") {
873
+ const $ = await D(l);
808
874
  if (!Array.isArray($?.resolutions))
809
- throw new g("INVALID_JSON", "resolutions 배열이 필요해요", 400);
810
- a.applyResolutions(t, $.resolutions), u.writeHead(204), u.end();
875
+ throw new m("INVALID_JSON", "resolutions 배열이 필요해요", 400);
876
+ c.applyResolutions(e, $.resolutions), u.writeHead(204), u.end();
811
877
  return;
812
878
  }
813
- const F = y.match(/^\/api\/publish\/([^/]+)\/events$/);
814
- if (F && w === "GET") {
815
- c(l, u, F[1]);
879
+ const x = w.match(/^\/api\/publish\/([^/]+)\/events$/);
880
+ if (x && y === "GET") {
881
+ a(l, u, x[1]);
816
882
  return;
817
883
  }
818
- const x = y.match(/^\/api\/publish\/([^/]+)$/);
819
- if (x && w === "DELETE") {
820
- a.cancel(x[1]), u.writeHead(204), u.end();
884
+ const F = w.match(/^\/api\/publish\/([^/]+)$/);
885
+ if (F && y === "DELETE") {
886
+ c.cancel(F[1]), u.writeHead(204), u.end();
821
887
  return;
822
888
  }
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 });
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 });
827
893
  return;
828
894
  }
829
- const S = f instanceof Error ? f.message : String(f);
830
- I(u, 500, { code: "INTERNAL", message: S });
895
+ const S = p instanceof Error ? p.message : String(p);
896
+ R(u, 500, { code: "INTERNAL", message: S });
831
897
  }
832
898
  })();
833
- }), m = n.port ?? Jt;
834
- await new Promise((l) => p.listen(m, "127.0.0.1", l));
835
- const b = 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();
836
902
  return {
837
- port: typeof b == "object" && b ? b.port : m,
903
+ port: typeof b == "object" && b ? b.port : g,
838
904
  close() {
839
- p.close();
905
+ f.close();
840
906
  }
841
907
  };
842
908
  }
843
- async function ot(n, t) {
844
- const e = await Kt({ projectRoot: n, port: t.port ?? 4816 });
845
- 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
+ }
915
+ }
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;
846
919
  }
847
- function st(n) {
848
- const t = n.indexOf("--port");
849
- if (t === -1) return;
850
- const e = Number(n[t + 1]);
851
- if (!Number.isInteger(e) || e < 0 || e > 65535)
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)
852
925
  throw new Error("--port 값이 유효하지 않아요 (0-65535)");
853
- return e;
926
+ return n;
854
927
  }
855
- async function Vt(n, t) {
856
- 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);
857
930
  if (o.length === 0)
858
931
  return console.error("사용법: pubuilder dev [--port N] -- <dev 명령> (예: pubuilder dev -- next dev)"), 1;
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);
863
- const p = () => {
864
- process.off("SIGINT", r), process.off("SIGTERM", c), s.close();
932
+ const r = await at(t, { port: ct(e.slice(0, n === -1 ? void 0 : n)) });
933
+ return new Promise((s) => {
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();
865
938
  };
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);
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);
870
943
  });
871
944
  });
872
945
  }
873
- const Wt = [Tt, Et];
874
- function zt(n) {
875
- const t = d(n, "package.json");
876
- if (!v(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;
877
950
  try {
878
- const e = JSON.parse(A(t, "utf8"));
879
- return !!(e.dependencies?.next ?? e.devDependencies?.next);
951
+ const n = JSON.parse(E(e, "utf8"));
952
+ return !!(n.dependencies?.next ?? n.devDependencies?.next);
880
953
  } catch {
881
954
  return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
882
955
  }
883
956
  }
884
- function Xt(n) {
885
- if (!zt(n)) return null;
886
- const t = Wt.filter((s) => s.detect(n));
887
- if (t.length === 0) return null;
888
- const e = /* @__PURE__ */ new Map(), o = [];
889
- for (const s of t)
890
- for (const i of s.scan(n)) {
891
- const a = e.get(i.routePath);
892
- if (a) {
893
- console.warn(`[pubuilder] path 충돌: ${i.routePath} (${a} 우선, ${s.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} 무시)`);
894
967
  continue;
895
968
  }
896
- e.set(i.routePath, s.name), o.push(i);
969
+ n.set(s.routePath, r.name), o.push(s);
897
970
  }
898
971
  return o;
899
972
  }
900
- async function Zt(n) {
901
- const e = await lt(import.meta.url).import(n, { default: !0 });
902
- 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))
903
976
  throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
904
- return e;
905
- }
906
- async function Yt(n, t) {
907
- const e = Xt(n), o = d(n, "ia.config.ts");
908
- if (e === null)
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) {
923
- 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;
924
997
  try {
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);
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);
927
1000
  } catch (o) {
928
1001
  return console.error(`[pubuilder] 실패: ${o instanceof Error ? o.message : String(o)}`), 1;
929
1002
  }
930
1003
  }
931
- function qt() {
932
- const n = process.argv[1];
933
- if (!n) return !1;
1004
+ function se() {
1005
+ const t = process.argv[1];
1006
+ if (!t) return !1;
934
1007
  try {
935
- return rt(n) === ct(import.meta.url);
1008
+ return lt(t) === Y(import.meta.url);
936
1009
  } catch {
937
1010
  return !1;
938
1011
  }
939
1012
  }
940
- qt() && Qt(process.argv.slice(2)).then(
941
- (n) => process.exit(n),
1013
+ se() && ie(process.argv.slice(2)).then(
1014
+ (t) => process.exit(t),
942
1015
  () => process.exit(1)
943
1016
  );
944
1017
  export {
945
- Qt as main,
946
- Yt as runScan
1018
+ ie as main,
1019
+ re as runScan
947
1020
  };