pubuilder 0.5.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 +73 -0
- package/dist/api.d.ts +1 -0
- package/dist/cli.js +113 -105
- package/dist/index.js +726 -555
- package/dist/index.js.map +1 -1
- package/dist/server/prompt.d.ts +2 -0
- package/package.json +3 -2
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/dist/api.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync as
|
|
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
|
|
18
|
+
for (const $ of g) L(i, p, $);
|
|
19
19
|
else
|
|
20
20
|
L(i, p, g);
|
|
21
21
|
const h = i.toString();
|
|
@@ -57,31 +57,31 @@ 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
|
|
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
|
|
64
|
+
function O(n) {
|
|
65
65
|
return JSON.stringify(n);
|
|
66
66
|
}
|
|
67
67
|
function Z(n, t, e) {
|
|
68
|
-
const o =
|
|
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: ${
|
|
73
|
-
if (r.push(`${o}{`), r.push(`${o}${
|
|
74
|
-
r.push(`${o}${
|
|
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}${
|
|
77
|
+
`${o}${v}${v}{ title: ${O(i.title)}, query: ${O(i.query)} },`
|
|
78
78
|
);
|
|
79
|
-
r.push(`${o}${
|
|
79
|
+
r.push(`${o}${v}],`);
|
|
80
80
|
}
|
|
81
81
|
if (n.children?.length) {
|
|
82
|
-
r.push(`${o}${
|
|
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}${
|
|
84
|
+
r.push(`${o}${v}],`);
|
|
85
85
|
}
|
|
86
86
|
return r.push(`${o}},`), r;
|
|
87
87
|
}
|
|
@@ -91,12 +91,12 @@ function G(n, t = {}) {
|
|
|
91
91
|
"",
|
|
92
92
|
"export default defineIA({"
|
|
93
93
|
];
|
|
94
|
-
n.viewport && o.push(`${
|
|
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(`${
|
|
96
|
+
return o.push(`${v}],`), o.push("})"), o.push(""), o.join(`
|
|
97
97
|
`);
|
|
98
98
|
}
|
|
99
|
-
function
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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],
|
|
150
|
-
(d, c) =>
|
|
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
|
|
153
|
-
const c =
|
|
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
|
|
158
|
-
|
|
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 (
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
-
},
|
|
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 (
|
|
286
|
+
if (P(t))
|
|
287
287
|
try {
|
|
288
|
-
const o = JSON.parse(
|
|
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 (
|
|
295
|
-
const o =
|
|
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 }),
|
|
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
|
|
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
|
|
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 (!
|
|
350
|
+
if (!P(this.settingsPath())) return { skills: {} };
|
|
351
351
|
try {
|
|
352
|
-
return JSON.parse(
|
|
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 }),
|
|
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 (!
|
|
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 (!
|
|
367
|
-
const a = V(
|
|
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 }),
|
|
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:
|
|
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((
|
|
425
|
+
${n.skills.map((s) => `## 스킬: ${s.name}
|
|
426
426
|
|
|
427
|
-
${
|
|
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
|
|
520
|
-
s.stderr.on("data", (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 =
|
|
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
|
|
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:
|
|
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
|
|
619
|
-
if (!
|
|
620
|
-
const { nodeJson:
|
|
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(
|
|
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:
|
|
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
|
|
638
|
-
l.write(`data: ${JSON.stringify(
|
|
645
|
+
const y = (w) => {
|
|
646
|
+
l.write(`data: ${JSON.stringify(w)}
|
|
639
647
|
|
|
640
|
-
`), (
|
|
648
|
+
`), (w.type === "done" || w.type === "error") && (S(), l.end());
|
|
641
649
|
};
|
|
642
|
-
S = a.subscribe(u,
|
|
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
|
-
|
|
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
|
|
657
|
-
if (
|
|
658
|
-
|
|
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 (
|
|
662
|
-
|
|
669
|
+
if (w === "GET" && y === "/api/skills") {
|
|
670
|
+
I(l, 200, s.list());
|
|
663
671
|
return;
|
|
664
672
|
}
|
|
665
|
-
if (
|
|
666
|
-
const b = await _(c),
|
|
667
|
-
|
|
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
|
|
671
|
-
if (
|
|
672
|
-
const b = decodeURIComponent(
|
|
673
|
-
s.setEnabled(b,
|
|
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 (
|
|
677
|
-
const b = decodeURIComponent(
|
|
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 (
|
|
689
|
+
if (w === "GET" && y === "/api/settings/figma-token") {
|
|
682
690
|
const b = K({ projectRoot: t, homeDir: e, env: process.env });
|
|
683
|
-
|
|
691
|
+
I(l, 200, { configured: b != null, masked: b ? At(b) : null });
|
|
684
692
|
return;
|
|
685
693
|
}
|
|
686
|
-
if (
|
|
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 (
|
|
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 (
|
|
696
|
-
const { status: b, body:
|
|
697
|
-
|
|
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 =
|
|
701
|
-
if (x &&
|
|
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 =
|
|
706
|
-
if (k &&
|
|
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
|
-
|
|
718
|
+
I(l, 404, { code: "NOT_FOUND", message: "해당 경로를 찾을 수 없어요" });
|
|
711
719
|
} catch (u) {
|
|
712
720
|
if (u instanceof m) {
|
|
713
|
-
|
|
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
|
-
|
|
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
|
|
730
|
+
const $ = p.address();
|
|
723
731
|
return {
|
|
724
|
-
port: typeof
|
|
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,
|
|
756
|
-
p(), s(
|
|
763
|
+
}), a.on("close", (g, $) => {
|
|
764
|
+
p(), s($ ? 128 + (at.signals[$] ?? 0) : g ?? 0);
|
|
757
765
|
});
|
|
758
766
|
});
|
|
759
767
|
}
|
|
760
|
-
const Ut = [bt,
|
|
768
|
+
const Ut = [bt, vt];
|
|
761
769
|
function Ct(n) {
|
|
762
770
|
const t = f(n, "package.json");
|
|
763
|
-
if (!
|
|
771
|
+
if (!P(t)) return !1;
|
|
764
772
|
try {
|
|
765
|
-
const e = JSON.parse(
|
|
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) 전용입니다."), !
|
|
797
|
-
if (
|
|
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 ||
|
|
800
|
-
for (const
|
|
801
|
-
for (const
|
|
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}개 라우트. 파일은 수정하지 않았어요.`) : (
|
|
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;
|