pubuilder 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ 이 프로젝트의 주요 변경 사항을 기록합니다.
4
+ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
5
+ 버전은 [Semantic Versioning](https://semver.org/lang/ko/)을 따릅니다.
6
+ (0.x 단계이므로 breaking change는 minor 버전으로 올립니다.)
7
+
8
+ ## [0.6.0] - 2026-07-15
9
+
10
+ ### Added
11
+
12
+ - **퍼블리싱 완료 후 AI 후속 답변(이어가기)** — 퍼블리싱이 끝난 뒤 AI에게 후속 지시를
13
+ 보내 중단·확인 대기 중인 작업을 현재 작업 트리에서 이어서 완료하도록 함
14
+ (완료 상태의 답변 textarea, ⌘/Ctrl+Enter 전송).
15
+
16
+ ### Changed
17
+
18
+ - **스킬함 진입점을 퍼블리싱 패널로 이동** — 미니맵/뷰어 헤더의 🧰 스킬함 버튼을
19
+ 제거하고 퍼블리싱 패널 상단으로 옮김.
20
+ - 스킬함 드로어 UI 개편 — 안내문, 섹션 헤더(Figma 연결 / AI 작업 스킬),
21
+ 상태 배지, 스킬 소스 설명 추가.
22
+
23
+ ## [0.5.0] - 2026-07-15
24
+
25
+ ### ⚠️ Breaking Changes
26
+
27
+ - `defineIA` 헬퍼의 import 경로가 `pubuilder`에서 **`pubuilder/config`**로 바뀌었습니다.
28
+ `"use client"` React 번들 없이 config만 로드할 수 있도록 별도 서브패스 export로 분리했습니다.
29
+
30
+ ```diff
31
+ - import { defineIA } from 'pubuilder'
32
+ + import { defineIA } from 'pubuilder/config'
33
+ ```
34
+
35
+ `pubuilder scan`이 생성하는 `ia.config.ts` 템플릿도 새 경로를 사용합니다.
36
+
37
+ ### Fixed
38
+
39
+ - 에이전트 spawn 시 `--setting-sources`를 격리해 스킬 화이트리스트가 실효성을 갖도록 수정.
40
+ - 컴패니언 서버: 종료 잡 상한 유지, publish 요청 바디 검증, 캐시 파일명 정규화.
41
+
42
+ ### Changed
43
+
44
+ - 상대경로 import에 `.js` 확장자를 명시(NodeNext ESM 해석).
45
+ - 데모(playground)를 음악 플랫폼(wavbor)에서 AI 에이전트 플랫폼(NIMBUS)으로 교체.
46
+
47
+ ## [0.4.0] - 2026-07-14
48
+
49
+ ### Added
50
+
51
+ - **AI 퍼블리싱 기능** — 미니맵에서 Figma 시안을 코드로 퍼블리싱하는 워크플로우.
52
+ - 컴패니언 HTTP 서버: 스킬 레지스트리(스캔/토글/업로드/삭제), Figma 토큰 로드·저장,
53
+ Figma 노드 JSON·PNG fetch, 퍼블리싱 잡 매니저, SSE 로그 스트리밍.
54
+ - 퍼블리싱 패널 UI: Figma URL 입력·실행·SSE 로그·중단.
55
+ - 스킬함 드로어: 스킬 목록/토글/업로드/삭제, 토큰 설정.
56
+ - CLI `serve`/`dev` 서브커맨드 — 컴패니언 서버 기동 및 dev 래퍼.
57
+
58
+ ### Changed
59
+
60
+ - 패키지명을 `pubmap`에서 **`pubuilder`**로 전면 개명(npm이 `pubmap`을 거부).
61
+
62
+ ## [0.3.0] - 2026-07-13
63
+
64
+ ### Added
65
+
66
+ - 최초 npm 공개 배포.
67
+ - `pubuilder scan` CLI — Next.js 라우트를 스캔해 `ia.config.ts` 생성/병합.
68
+ - `PageMap` 미니맵 컴포넌트 — 화면설계서(IA) 기반 페이지 트리를 iframe 썸네일로 렌더링.
69
+
70
+ [0.6.0]: https://www.npmjs.com/package/pubuilder/v/0.6.0
71
+ [0.5.0]: https://www.npmjs.com/package/pubuilder/v/0.5.0
72
+ [0.4.0]: https://www.npmjs.com/package/pubuilder/v/0.4.0
73
+ [0.3.0]: https://www.npmjs.com/package/pubuilder/v/0.3.0
package/README.md CHANGED
@@ -42,7 +42,7 @@ npx pubuilder scan --dry-run # preview without writing
42
42
 
43
43
  ```ts
44
44
  // ia.config.ts
45
- import { defineIA } from 'pubuilder'
45
+ import { defineIA } from 'pubuilder/config'
46
46
 
47
47
  export default defineIA({
48
48
  pages: [
@@ -57,7 +57,7 @@ export default defineIA({
57
57
  // screens that differ by query string become child nodes
58
58
  variants: [
59
59
  { title: 'All', query: { category: 'all' } },
60
- { title: 'Art', query: { category: 'art', tab: 1 } },
60
+ { title: 'Coding', query: { category: 'coding', tab: 1 } },
61
61
  ],
62
62
  },
63
63
  { path: 'https://example.com/brand', title: 'Brand', external: true },
package/dist/api.d.ts CHANGED
@@ -36,6 +36,7 @@ export declare class PubuilderApi {
36
36
  selection: BlockSelection;
37
37
  outerHTML: string;
38
38
  figmaUrl: string;
39
+ instruction?: string;
39
40
  }): Promise<{
40
41
  jobId: string;
41
42
  }>;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync as O, existsSync as v, readdirSync as D, mkdirSync as j, writeFileSync as R, rmSync as W, realpathSync as nt } from "node:fs";
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
3
  import { join as f } from "node:path";
4
4
  import { fileURLToPath as ot } from "node:url";
5
5
  import { createJiti as rt } from "jiti";
@@ -15,7 +15,7 @@ function ut(n, t) {
15
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
16
  for (const [p, g] of Object.entries(t.query))
17
17
  if (i.delete(p), Array.isArray(g))
18
- for (const w of g) L(i, p, w);
18
+ for (const $ of g) L(i, p, $);
19
19
  else
20
20
  L(i, p, g);
21
21
  const h = i.toString();
@@ -57,46 +57,46 @@ function M(n) {
57
57
  if (o(n.pages, null, 0), t.length === 0) throw new Error("[pubuilder] pages가 비어 있습니다");
58
58
  return { nodes: t, viewport: n.viewport ?? lt };
59
59
  }
60
- const P = " ";
60
+ const v = " ";
61
61
  function pt(n) {
62
62
  return n.split("/").map((t) => /^\[{1,2}\.{0,3}.*\]{1,2}$/.test(t) && t.startsWith("[") ? "1" : t).join("/");
63
63
  }
64
- function A(n) {
64
+ function O(n) {
65
65
  return JSON.stringify(n);
66
66
  }
67
67
  function Z(n, t, e) {
68
- const o = P.repeat(t), r = [];
68
+ const o = v.repeat(t), r = [];
69
69
  n.path.includes("[") && r.push(`${o}// TODO: 샘플 경로로 교체 (예: ${pt(n.path)})`);
70
70
  const s = e.has(n.path) ? " // TODO: title 확인" : "";
71
71
  if (!n.children?.length && !n.variants?.length && !n.external)
72
- return r.push(`${o}{ path: ${A(n.path)}, title: ${A(n.title)} },${s}`), r;
73
- if (r.push(`${o}{`), r.push(`${o}${P}path: ${A(n.path)},`), r.push(`${o}${P}title: ${A(n.title)},${s}`), n.external && r.push(`${o}${P}external: true,`), n.variants?.length) {
74
- r.push(`${o}${P}variants: [`);
72
+ return r.push(`${o}{ path: ${O(n.path)}, title: ${O(n.title)} },${s}`), r;
73
+ if (r.push(`${o}{`), r.push(`${o}${v}path: ${O(n.path)},`), r.push(`${o}${v}title: ${O(n.title)},${s}`), n.external && r.push(`${o}${v}external: true,`), n.variants?.length) {
74
+ r.push(`${o}${v}variants: [`);
75
75
  for (const i of n.variants)
76
76
  r.push(
77
- `${o}${P}${P}{ title: ${A(i.title)}, query: ${A(i.query)} },`
77
+ `${o}${v}${v}{ title: ${O(i.title)}, query: ${O(i.query)} },`
78
78
  );
79
- r.push(`${o}${P}],`);
79
+ r.push(`${o}${v}],`);
80
80
  }
81
81
  if (n.children?.length) {
82
- r.push(`${o}${P}children: [`);
82
+ r.push(`${o}${v}children: [`);
83
83
  for (const i of n.children) r.push(...Z(i, t + 2, e));
84
- r.push(`${o}${P}],`);
84
+ r.push(`${o}${v}],`);
85
85
  }
86
86
  return r.push(`${o}},`), r;
87
87
  }
88
88
  function G(n, t = {}) {
89
89
  const e = t.newPaths ?? /* @__PURE__ */ new Set(), o = [
90
- 'import { defineIA } from "pubuilder"',
90
+ 'import { defineIA } from "pubuilder/config"',
91
91
  "",
92
92
  "export default defineIA({"
93
93
  ];
94
- n.viewport && o.push(`${P}viewport: ${A(n.viewport)},`), o.push(`${P}pages: [`);
94
+ n.viewport && o.push(`${v}viewport: ${O(n.viewport)},`), o.push(`${v}pages: [`);
95
95
  for (const r of n.pages) o.push(...Z(r, 2, e));
96
- return o.push(`${P}],`), o.push("})"), o.push(""), o.join(`
96
+ return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
97
97
  `);
98
98
  }
99
- function E(n) {
99
+ function T(n) {
100
100
  return n === "/" ? [] : n.replace(/^\//, "").split("/");
101
101
  }
102
102
  function ht(n, t) {
@@ -104,11 +104,11 @@ function ht(n, t) {
104
104
  }
105
105
  function dt(n) {
106
106
  const t = [...n].sort((r, s) => {
107
- const a = E(r.routePath).length - E(s.routePath).length;
107
+ const a = T(r.routePath).length - T(s.routePath).length;
108
108
  return a !== 0 ? a : r.routePath.localeCompare(s.routePath);
109
109
  }), e = [], o = [];
110
110
  for (const r of t) {
111
- const s = E(r.routePath), a = {
111
+ const s = T(r.routePath), a = {
112
112
  path: r.routePath,
113
113
  title: r.title ?? s.at(-1) ?? "/"
114
114
  };
@@ -120,7 +120,7 @@ function dt(n) {
120
120
  return e;
121
121
  }
122
122
  function U(n, t) {
123
- const e = E(n), o = E(t);
123
+ const e = T(n), o = T(t);
124
124
  for (let r = 0; r < o.length; r++) {
125
125
  const s = o[r];
126
126
  if (/^\[\[\.\.\..+\]\]$/.test(s))
@@ -135,7 +135,7 @@ function U(n, t) {
135
135
  }
136
136
  function Y(n, t = []) {
137
137
  for (const e of n)
138
- t.push({ page: e, segments: E(e.path) }), e.children && Y(e.children, t);
138
+ t.push({ page: e, segments: T(e.path) }), e.children && Y(e.children, t);
139
139
  return t;
140
140
  }
141
141
  function C(n) {
@@ -146,16 +146,16 @@ function ft(n, t) {
146
146
  (c) => c.page.path === d.routePath || U(c.page.path, d.routePath)
147
147
  ), a = t.filter((d) => !s(d)), i = (d) => t.some(
148
148
  (c) => c.routePath === d.page.path || U(d.page.path, c.routePath)
149
- ), h = r.filter((d) => !i(d)).map((d) => d.page.path), p = [], g = [...o], w = [...a].sort(
150
- (d, c) => E(d.routePath).length - E(c.routePath).length
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
151
151
  );
152
- for (const d of w) {
153
- const c = E(d.routePath), l = { path: d.routePath, title: d.title ?? c.at(-1) ?? "/" };
152
+ for (const d of $) {
153
+ const c = T(d.routePath), l = { path: d.routePath, title: d.title ?? c.at(-1) ?? "/" };
154
154
  let u;
155
155
  for (const S of g) {
156
156
  if (C(S.page)) continue;
157
- const $ = S.segments;
158
- $.length >= c.length || $.every((y, I) => y === c[I]) && (!u || $.length > u.segments.length) && (u = S);
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);
159
159
  }
160
160
  u ? (u.page.children ??= []).push(l) : e.pages.push(l), g.push({ page: l, segments: c }), p.push(d.routePath);
161
161
  }
@@ -164,7 +164,7 @@ function ft(n, t) {
164
164
  const mt = /^page\.(tsx|jsx|ts|js)$/, gt = /^\(.+\)$/, wt = /^\(\.{1,3}\)/;
165
165
  function J(n) {
166
166
  for (const t of ["app", "src/app"])
167
- if (v(f(n, t))) return f(n, t);
167
+ if (P(f(n, t))) return f(n, t);
168
168
  return null;
169
169
  }
170
170
  function $t(n) {
@@ -201,7 +201,7 @@ const bt = {
201
201
  a !== null && o.push({
202
202
  routePath: a,
203
203
  isDynamic: /\[.+\]/.test(a),
204
- title: yt(O(r, "utf8"))
204
+ title: yt(R(r, "utf8"))
205
205
  });
206
206
  }
207
207
  return o;
@@ -209,7 +209,7 @@ const bt = {
209
209
  }, H = /\.(tsx|jsx|ts|js)$/, St = /* @__PURE__ */ new Set(["_app", "_document", "_error", "404", "500"]);
210
210
  function B(n) {
211
211
  for (const t of ["pages", "src/pages"])
212
- if (v(f(n, t))) return f(n, t);
212
+ if (P(f(n, t))) return f(n, t);
213
213
  return null;
214
214
  }
215
215
  function q(n, t, e) {
@@ -226,7 +226,7 @@ function q(n, t, e) {
226
226
  e.push({ routePath: a, isDynamic: /\[.+\]/.test(a) });
227
227
  }
228
228
  }
229
- const Pt = {
229
+ const vt = {
230
230
  name: "next-pages",
231
231
  detect(n) {
232
232
  return B(n) !== null;
@@ -237,7 +237,7 @@ const Pt = {
237
237
  const e = [];
238
238
  return q(t, [], e), e;
239
239
  }
240
- }, vt = `import { defineIA } from "pubuilder"
240
+ }, Pt = `import { defineIA } from "pubuilder/config"
241
241
 
242
242
  // pubuilder scan은 아직 이 프로젝트의 라우팅 방식을 지원하지 않아요 (v1은 Next.js 전용).
243
243
  // 아래 예시를 참고해 앱의 화면 구조를 직접 작성해주세요.
@@ -283,16 +283,16 @@ function F(n) {
283
283
  }
284
284
  function K(n) {
285
285
  const t = F(n.homeDir);
286
- if (v(t))
286
+ if (P(t))
287
287
  try {
288
- const o = JSON.parse(O(t, "utf8"));
288
+ const o = JSON.parse(R(t, "utf8"));
289
289
  if (o.figmaToken) return o.figmaToken;
290
290
  } catch {
291
291
  }
292
292
  if (n.env.FIGMA_TOKEN) return n.env.FIGMA_TOKEN;
293
293
  const e = f(n.projectRoot, ".env");
294
- if (v(e)) {
295
- const o = O(e, "utf8").split(`
294
+ if (P(e)) {
295
+ const o = R(e, "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, "");
@@ -302,7 +302,7 @@ function K(n) {
302
302
  return null;
303
303
  }
304
304
  function Tt(n, t) {
305
- j(f(n, ".pubuilder"), { recursive: !0 }), R(F(n), JSON.stringify({ figmaToken: t }, null, 2), { mode: 384 });
305
+ j(f(n, ".pubuilder"), { recursive: !0 }), N(F(n), JSON.stringify({ figmaToken: t }, null, 2), { mode: 384 });
306
306
  }
307
307
  function Et(n) {
308
308
  W(F(n), { force: !0 });
@@ -326,10 +326,10 @@ async function Ot(n, t, e, o = fetch) {
326
326
  if (!h.ok) throw new m("FIGMA_API_ERROR", `이미지 export 실패 (${h.status})`, 502);
327
327
  const g = (await h.json()).images[n.nodeId];
328
328
  if (!g) throw new m("FIGMA_API_ERROR", "이미지 export URL이 비어 있어요", 502);
329
- const w = await (await o(g)).arrayBuffer();
329
+ const $ = await (await o(g)).arrayBuffer();
330
330
  j(e, { recursive: !0 });
331
331
  const d = n.nodeId.replace(/[^A-Za-z0-9_-]/g, "-"), c = f(e, `${n.fileKey}-${d}.png`);
332
- return R(c, Buffer.from(w)), { nodeJson: i, imagePath: c };
332
+ return N(c, Buffer.from($)), { nodeJson: i, imagePath: c };
333
333
  }
334
334
  function V(n) {
335
335
  const t = n.match(/^---\n([\s\S]*?)\n---/);
@@ -347,24 +347,24 @@ class Rt {
347
347
  return f(this.projectRoot, ".pubuilder", "settings.json");
348
348
  }
349
349
  loadSettings() {
350
- if (!v(this.settingsPath())) return { skills: {} };
350
+ if (!P(this.settingsPath())) return { skills: {} };
351
351
  try {
352
- return JSON.parse(O(this.settingsPath(), "utf8"));
352
+ return JSON.parse(R(this.settingsPath(), "utf8"));
353
353
  } catch {
354
354
  return { skills: {} };
355
355
  }
356
356
  }
357
357
  saveSettings(t) {
358
- j(f(this.projectRoot, ".pubuilder"), { recursive: !0 }), R(this.settingsPath(), JSON.stringify(t, null, 2));
358
+ j(f(this.projectRoot, ".pubuilder"), { recursive: !0 }), N(this.settingsPath(), JSON.stringify(t, null, 2));
359
359
  }
360
360
  scanDir(t, e) {
361
- if (!v(t)) return [];
361
+ if (!P(t)) return [];
362
362
  const o = [];
363
363
  for (const r of D(t, { withFileTypes: !0 })) {
364
364
  if (!r.isDirectory()) continue;
365
365
  const s = f(t, r.name, "SKILL.md");
366
- if (!v(s)) continue;
367
- const a = V(O(s, "utf8"));
366
+ if (!P(s)) continue;
367
+ const a = V(R(s, "utf8"));
368
368
  o.push({
369
369
  id: `${e}:${r.name}`,
370
370
  name: a.name ?? r.name,
@@ -400,7 +400,7 @@ class Rt {
400
400
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(r))
401
401
  throw new m("INVALID_NAME", `스킬 이름이 유효하지 않아요: ${r}`, 400);
402
402
  const s = f(this.projectRoot, ".pubuilder", "skills", r);
403
- return j(s, { recursive: !0 }), R(f(s, "SKILL.md"), e), { id: `uploaded:${r}`, name: r, description: o.description ?? "", source: "uploaded", enabled: !0 };
403
+ return j(s, { recursive: !0 }), N(f(s, "SKILL.md"), e), { id: `uploaded:${r}`, name: r, description: o.description ?? "", source: "uploaded", enabled: !0 };
404
404
  }
405
405
  /** 삭제는 uploaded만 — 유저의 .claude/skills를 지우는 사고 방지 (스펙) */
406
406
  remove(t) {
@@ -413,7 +413,7 @@ class Rt {
413
413
  }
414
414
  enabledSkillContents() {
415
415
  const t = this.loadSettings();
416
- return this.scanAll().filter((e) => t.skills[e.id] ?? e.source !== "global").map((e) => ({ name: e.name, content: O(e.path, "utf8") }));
416
+ return this.scanAll().filter((e) => t.skills[e.id] ?? e.source !== "global").map((e) => ({ name: e.name, content: R(e.path, "utf8") }));
417
417
  }
418
418
  }
419
419
  function Nt(n) {
@@ -422,16 +422,22 @@ function Nt(n) {
422
422
 
423
423
  다른 스킬은 사용하지 말고, 아래에 명시된 스킬 지침만 따르세요.
424
424
 
425
- ${n.skills.map((r) => `## 스킬: ${r.name}
425
+ ${n.skills.map((s) => `## 스킬: ${s.name}
426
426
 
427
- ${r.content}`).join(`
427
+ ${s.content}`).join(`
428
428
 
429
429
  ---
430
430
 
431
431
  `)}
432
432
  ` : `
433
433
  다른 스킬은 스스로 판단해 사용하지 마세요. 아래에 명시된 스킬 지침만 따르세요 (이번 요청에는 없음).
434
- `;
434
+ `, r = n.instruction ? `
435
+ # 사용자의 후속 답변
436
+
437
+ ${n.instruction}
438
+
439
+ 이전 실행에서 이미 변경된 현재 작업 트리를 확인하고, 위 답변에 따라 중단된 작업을 이어서 완료하세요. 같은 선택지를 다시 질문하지 마세요.
440
+ ` : "";
435
441
  return `이 프로젝트의 특정 UI 블록을 Figma 시안대로 수정하는 퍼블리싱 작업입니다.
436
442
 
437
443
  # 대상 블록
@@ -451,6 +457,7 @@ ${n.outerHTML}
451
457
  ${JSON.stringify(e.nodeJson, null, 2)}
452
458
  \`\`\`
453
459
  ${o}
460
+ ${r}
454
461
  # 작업 순서
455
462
  1. 위 라우트와 outerHTML 스냅샷을 단서로, 이 블록을 렌더링하는 소스 컴포넌트 파일을 찾으세요.
456
463
  2. Figma 렌더 이미지를 Read 도구로 열어 시안을 육안으로 확인하세요. 노드 JSON의 수치(색상/간격/타이포)를 우선 근거로 쓰되, JSON에만 있고 이미지에 보이지 않는 요소는 구현하지 마세요.
@@ -516,8 +523,8 @@ const z = 600 * 1e3, kt = {
516
523
  text: c.code === "ENOENT" ? "claude CLI를 찾지 못했어요. https://claude.com/claude-code 에서 설치 후 claude login 해주세요" : `claude 실행 실패: ${c.message}`
517
524
  });
518
525
  });
519
- const w = [];
520
- s.stderr.on("data", (c) => w.push(String(c)));
526
+ const $ = [];
527
+ s.stderr.on("data", (c) => $.push(String(c)));
521
528
  let d = !1;
522
529
  ct({ input: s.stdout }).on("line", (c) => {
523
530
  g();
@@ -526,7 +533,7 @@ const z = 600 * 1e3, kt = {
526
533
  e(u), (u.type === "done" || u.type === "error") && (d = !0);
527
534
  }), s.on("close", (c) => {
528
535
  if (c !== 0 && !d) {
529
- const l = w.join("").trim();
536
+ const l = $.join("").trim();
530
537
  i({ type: "error", text: l || `claude가 코드 ${c}로 종료됐어요. claude login 상태를 확인해주세요` });
531
538
  } else
532
539
  i();
@@ -605,25 +612,26 @@ function _(n) {
605
612
  }), n.on("error", e);
606
613
  });
607
614
  }
608
- function T(n, t, e) {
615
+ function I(n, t, e) {
609
616
  const o = JSON.stringify(e);
610
617
  n.writeHead(t, { "content-type": "application/json" }), n.end(o);
611
618
  }
612
619
  async function Mt(n) {
613
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);
614
621
  async function i(c) {
615
- const l = await _(c), { selection: u, outerHTML: S, figmaUrl: $ } = l ?? {}, y = It(String($ ?? ""));
622
+ const l = await _(c), { selection: u, outerHTML: S, figmaUrl: y, instruction: w } = l ?? {}, E = It(String(y ?? ""));
616
623
  if (typeof u != "object" || u === null || typeof u.pagePath != "string" || typeof u.selector != "string" || typeof S != "string")
617
624
  throw new m("INVALID_JSON", "selection/outerHTML 형식이 올바르지 않아요", 400);
618
- const I = K({ projectRoot: t, homeDir: e, env: process.env });
619
- if (!I) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
620
- const { nodeJson: x, imagePath: k } = await Ot(y, I, f(t, ".pubuilder/cache"), r), b = Nt({
625
+ const x = K({ projectRoot: t, homeDir: e, env: process.env });
626
+ if (!x) throw new m("NO_FIGMA_TOKEN", "Figma 토큰이 설정되지 않았어요. 스킬함 설정에서 등록해주세요", 401);
627
+ const { nodeJson: k, imagePath: b } = await Ot(E, x, f(t, ".pubuilder/cache"), r), A = Nt({
621
628
  selection: u,
622
629
  outerHTML: S ?? "",
623
- figma: { url: String($), nodeJson: x, imagePath: k },
624
- skills: s.enabledSkillContents()
630
+ figma: { url: String(y), nodeJson: k, imagePath: b },
631
+ skills: s.enabledSkillContents(),
632
+ instruction: typeof w == "string" ? w.trim() : void 0
625
633
  });
626
- return { status: 202, body: { jobId: a.start({ prompt: b, cwd: t }) } };
634
+ return { status: 202, body: { jobId: a.start({ prompt: A, cwd: t }) } };
627
635
  }
628
636
  function h(c, l, u) {
629
637
  if (!a.has(u)) throw new m("NOT_FOUND", `job 없음: ${u}`, 404);
@@ -634,94 +642,94 @@ async function Mt(n) {
634
642
  });
635
643
  let S = () => {
636
644
  };
637
- const $ = (y) => {
638
- l.write(`data: ${JSON.stringify(y)}
645
+ const y = (w) => {
646
+ l.write(`data: ${JSON.stringify(w)}
639
647
 
640
- `), (y.type === "done" || y.type === "error") && (S(), l.end());
648
+ `), (w.type === "done" || w.type === "error") && (S(), l.end());
641
649
  };
642
- S = a.subscribe(u, $), c.on("close", S);
650
+ S = a.subscribe(u, y), c.on("close", S);
643
651
  }
644
652
  const p = st((c, l) => {
645
653
  (async () => {
646
654
  try {
647
655
  const u = c.headers.origin;
648
656
  if (u && !Lt.test(u)) {
649
- T(l, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
657
+ I(l, 403, { code: "FORBIDDEN", message: "허용되지 않은 origin이에요" });
650
658
  return;
651
659
  }
652
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") {
653
661
  l.writeHead(204), l.end();
654
662
  return;
655
663
  }
656
- const $ = new URL(c.url ?? "/", "http://localhost").pathname, y = c.method ?? "GET";
657
- if (y === "GET" && $ === "/api/health") {
658
- T(l, 200, { ok: !0 });
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 });
659
667
  return;
660
668
  }
661
- if (y === "GET" && $ === "/api/skills") {
662
- T(l, 200, s.list());
669
+ if (w === "GET" && y === "/api/skills") {
670
+ I(l, 200, s.list());
663
671
  return;
664
672
  }
665
- if (y === "POST" && $ === "/api/skills") {
666
- const b = await _(c), N = s.upload(b.filename, b.content);
667
- T(l, 201, N);
673
+ if (w === "POST" && y === "/api/skills") {
674
+ const b = await _(c), A = s.upload(b.filename, b.content);
675
+ I(l, 201, A);
668
676
  return;
669
677
  }
670
- const I = $.match(/^\/api\/skills\/([^/]+)$/);
671
- if (I && y === "PATCH") {
672
- const b = decodeURIComponent(I[1]), N = await _(c);
673
- s.setEnabled(b, N.enabled), l.writeHead(204), l.end();
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();
674
682
  return;
675
683
  }
676
- if (I && y === "DELETE") {
677
- const b = decodeURIComponent(I[1]);
684
+ if (E && w === "DELETE") {
685
+ const b = decodeURIComponent(E[1]);
678
686
  s.remove(b), l.writeHead(204), l.end();
679
687
  return;
680
688
  }
681
- if (y === "GET" && $ === "/api/settings/figma-token") {
689
+ if (w === "GET" && y === "/api/settings/figma-token") {
682
690
  const b = K({ projectRoot: t, homeDir: e, env: process.env });
683
- T(l, 200, { configured: b != null, masked: b ? At(b) : null });
691
+ I(l, 200, { configured: b != null, masked: b ? At(b) : null });
684
692
  return;
685
693
  }
686
- if (y === "PUT" && $ === "/api/settings/figma-token") {
694
+ if (w === "PUT" && y === "/api/settings/figma-token") {
687
695
  const b = await _(c);
688
696
  Tt(e, b.token), l.writeHead(204), l.end();
689
697
  return;
690
698
  }
691
- if (y === "DELETE" && $ === "/api/settings/figma-token") {
699
+ if (w === "DELETE" && y === "/api/settings/figma-token") {
692
700
  Et(e), l.writeHead(204), l.end();
693
701
  return;
694
702
  }
695
- if (y === "POST" && $ === "/api/publish") {
696
- const { status: b, body: N } = await i(c);
697
- T(l, b, N);
703
+ if (w === "POST" && y === "/api/publish") {
704
+ const { status: b, body: A } = await i(c);
705
+ I(l, b, A);
698
706
  return;
699
707
  }
700
- const x = $.match(/^\/api\/publish\/([^/]+)\/events$/);
701
- if (x && y === "GET") {
708
+ const x = y.match(/^\/api\/publish\/([^/]+)\/events$/);
709
+ if (x && w === "GET") {
702
710
  h(c, l, x[1]);
703
711
  return;
704
712
  }
705
- const k = $.match(/^\/api\/publish\/([^/]+)$/);
706
- if (k && y === "DELETE") {
713
+ const k = y.match(/^\/api\/publish\/([^/]+)$/);
714
+ if (k && w === "DELETE") {
707
715
  a.cancel(k[1]), l.writeHead(204), l.end();
708
716
  return;
709
717
  }
710
- T(l, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
718
+ I(l, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
711
719
  } catch (u) {
712
720
  if (u instanceof m) {
713
- T(l, u.status, { code: u.code, message: u.message });
721
+ I(l, u.status, { code: u.code, message: u.message });
714
722
  return;
715
723
  }
716
724
  const S = u instanceof Error ? u.message : String(u);
717
- T(l, 500, { code: "INTERNAL", message: S });
725
+ I(l, 500, { code: "INTERNAL", message: S });
718
726
  }
719
727
  })();
720
728
  }), g = n.port ?? Dt;
721
729
  await new Promise((c) => p.listen(g, "127.0.0.1", c));
722
- const w = p.address();
730
+ const $ = p.address();
723
731
  return {
724
- port: typeof w == "object" && w ? w.port : g,
732
+ port: typeof $ == "object" && $ ? $.port : g,
725
733
  close() {
726
734
  p.close();
727
735
  }
@@ -752,17 +760,17 @@ async function Gt(n, t) {
752
760
  };
753
761
  a.on("error", (g) => {
754
762
  console.error(`[pubuilder] 명령 실행 실패: ${g.message}`), p(), s(1);
755
- }), a.on("close", (g, w) => {
756
- p(), s(w ? 128 + (at.signals[w] ?? 0) : g ?? 0);
763
+ }), a.on("close", (g, $) => {
764
+ p(), s($ ? 128 + (at.signals[$] ?? 0) : g ?? 0);
757
765
  });
758
766
  });
759
767
  }
760
- const Ut = [bt, Pt];
768
+ const Ut = [bt, vt];
761
769
  function Ct(n) {
762
770
  const t = f(n, "package.json");
763
- if (!v(t)) return !1;
771
+ if (!P(t)) return !1;
764
772
  try {
765
- const e = JSON.parse(O(t, "utf8"));
773
+ const e = JSON.parse(R(t, "utf8"));
766
774
  return !!(e.dependencies?.next ?? e.devDependencies?.next);
767
775
  } catch {
768
776
  return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
@@ -793,18 +801,18 @@ async function Ht(n) {
793
801
  async function Bt(n, t) {
794
802
  const e = Jt(n), o = f(n, "ia.config.ts");
795
803
  if (e === null)
796
- return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !v(o) && !t.dryRun && (R(o, vt), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${o}`)), { kind: "unsupported" };
797
- if (v(o)) {
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)) {
798
806
  const i = await Ht(o), { config: h, added: p, removed: g } = ft(i, e);
799
- M(h), t.dryRun || R(o, G(h, { newPaths: new Set(p) }));
800
- for (const w of p) console.log(`[pubuilder] + 추가됨: ${w}`);
801
- for (const w of g) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${w}`);
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엔 유지): ${$}`);
802
810
  return p.length === 0 && g.length === 0 && console.log("[pubuilder] 변경 없음"), t.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: p, removed: g };
803
811
  }
804
812
  const s = { pages: dt(e) };
805
813
  M(s);
806
814
  const a = e.map((i) => i.routePath);
807
- return t.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (R(o, G(s, { newPaths: new Set(a) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${a.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: a };
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 };
808
816
  }
809
817
  async function Kt(n) {
810
818
  const [t, ...e] = n;
@@ -1,4 +1,4 @@
1
- import type { IAConfig } from '../types';
1
+ import type { IAConfig } from '../types.js';
2
2
  export interface PageMapProps {
3
3
  config: IAConfig;
4
4
  /**
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IAConfig, IANode } from './types';
1
+ import type { IAConfig, IANode } from './types.js';
2
2
  /** ia.config.ts 작성용 헬퍼 — 타입 추론만 제공 */
3
3
  export declare function defineIA(config: IAConfig): IAConfig;
4
4
  export interface NormalizedIA {