@bstage-sdk/cli 0.1.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/index.js ADDED
@@ -0,0 +1,2800 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_TRANSLATION_TIER,
4
+ LOGIN_PATH,
5
+ TRANSLATION_CDN_BASE_KEY,
6
+ TRANSLATION_CDN_BUCKET_PREFIX,
7
+ TRANSLATION_CDN_ORIGIN,
8
+ TRANSLATION_LANGUAGES,
9
+ bstageDevPlugin,
10
+ createViteConfig,
11
+ describePhase,
12
+ resolvePhase,
13
+ resolveProjectTarget
14
+ } from "./chunk-S6HGR2U3.js";
15
+
16
+ // src/index.ts
17
+ import { readFileSync as readFileSync2 } from "fs";
18
+ import { dirname as dirname3, join as join7 } from "path";
19
+ import { fileURLToPath as fileURLToPath2 } from "url";
20
+ import { Command } from "commander";
21
+
22
+ // src/commands/build.ts
23
+ import { resolve, join, relative } from "path";
24
+ import { readFile, access, readdir, rename, mkdir, rm } from "fs/promises";
25
+ import { build as viteBuild, loadEnv } from "vite";
26
+ import pc from "picocolors";
27
+ import { getSlotsByTargetV2, isSlotIdV2, slotIdToDirName } from "@bstage-sdk/core";
28
+
29
+ // src/vite/metaPlugin.ts
30
+ function createMetaPlugin() {
31
+ let meta2 = null;
32
+ const plugin = {
33
+ name: "bstage-meta-extract",
34
+ enforce: "pre",
35
+ transform(code, id) {
36
+ if (meta2) return null;
37
+ if (!/\.[cm]?[tj]sx?$/.test(id)) return null;
38
+ if (!code.includes("createTemplate") && !code.includes("createWebComponent")) return null;
39
+ const extracted = extractMeta(code);
40
+ if (extracted) meta2 = extracted;
41
+ return null;
42
+ }
43
+ };
44
+ return { plugin, getMeta: () => meta2 };
45
+ }
46
+ function extractMeta(code) {
47
+ let framework;
48
+ if (code.includes("'@bstage-sdk/react'") || code.includes('"@bstage-sdk/react"')) {
49
+ framework = "react";
50
+ } else if (code.includes("'@bstage-sdk/vue'") || code.includes('"@bstage-sdk/vue"')) {
51
+ framework = "vue";
52
+ } else if (code.includes("'@bstage-sdk/svelte'") || code.includes('"@bstage-sdk/svelte"')) {
53
+ framework = "svelte";
54
+ }
55
+ const attempts = [
56
+ { fnName: "createTemplate", argIndex: 1 },
57
+ { fnName: "createWebComponent", argIndex: 2 }
58
+ ];
59
+ for (const { fnName, argIndex } of attempts) {
60
+ const idx = code.indexOf(fnName + "(");
61
+ if (idx === -1) continue;
62
+ const objStr = findNthObjectArg(code, idx + fnName.length, argIndex);
63
+ if (!objStr) continue;
64
+ const name = extractStringProp(objStr, "name");
65
+ if (!name) continue;
66
+ const slot = extractStringProp(objStr, "slot");
67
+ const type = extractStringProp(objStr, "type");
68
+ const explicitFramework = extractStringProp(objStr, "framework");
69
+ return {
70
+ name,
71
+ elementName: name,
72
+ ...slot && { slot },
73
+ ...type && { type },
74
+ framework: explicitFramework ?? framework
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+ function findNthObjectArg(code, parenPos, n) {
80
+ let i = parenPos;
81
+ while (i < code.length && code[i] !== "(") i++;
82
+ i++;
83
+ let argCount = 0;
84
+ let depth = 0;
85
+ while (i < code.length && argCount < n) {
86
+ const ch = code[i];
87
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
88
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
89
+ else if (ch === "," && depth === 0) argCount++;
90
+ if (ch === "'" || ch === '"' || ch === "`") {
91
+ i = skipString(code, i);
92
+ }
93
+ i++;
94
+ }
95
+ while (i < code.length && /\s/.test(code[i])) i++;
96
+ if (i >= code.length || code[i] !== "{") return null;
97
+ let braceDepth = 0;
98
+ const start = i;
99
+ while (i < code.length) {
100
+ const ch = code[i];
101
+ if (ch === "'" || ch === '"' || ch === "`") {
102
+ i = skipString(code, i);
103
+ } else if (ch === "{") {
104
+ braceDepth++;
105
+ } else if (ch === "}") {
106
+ braceDepth--;
107
+ if (braceDepth === 0) return code.slice(start, i + 1);
108
+ }
109
+ i++;
110
+ }
111
+ return null;
112
+ }
113
+ function skipString(code, i) {
114
+ const quote = code[i];
115
+ i++;
116
+ while (i < code.length && code[i] !== quote) {
117
+ if (code[i] === "\\") i++;
118
+ i++;
119
+ }
120
+ return i;
121
+ }
122
+ function extractStringProp(objStr, key) {
123
+ const regex = new RegExp(`(?:^|[,{\\s])\\s*${key}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`);
124
+ return objStr.match(regex)?.[1];
125
+ }
126
+
127
+ // src/vite/registerPlugin.ts
128
+ function createRegisterPlugin() {
129
+ return {
130
+ name: "bstage-auto-register",
131
+ enforce: "post",
132
+ config() {
133
+ return {
134
+ optimizeDeps: {
135
+ include: ["react-dom/client"]
136
+ }
137
+ };
138
+ },
139
+ transform(code, id) {
140
+ if (!id.endsWith("template.tsx") && !id.endsWith("template.ts")) return null;
141
+ if (!code.includes("createTemplate")) return null;
142
+ const injection = `
143
+ import { __bstage_registry__ as __reg__, registerTemplate as __registerTemplate__ } from '@bstage-sdk/react';
144
+ __reg__.forEach(__registerTemplate__);
145
+ `;
146
+ return {
147
+ code: code + injection,
148
+ map: null
149
+ };
150
+ }
151
+ };
152
+ }
153
+
154
+ // src/build/credentialCheck.ts
155
+ var CLIENT_MARKER = "X-BSTAGE-APP-ID";
156
+ var APP_ID_PREFIX = "bsa_";
157
+ var APP_SECRET_PREFIX = "bsp_";
158
+ var CREDENTIAL_ENV_VARS = [
159
+ "VITE_BSTAGE_APP_ID",
160
+ "VITE_BSTAGE_APP_SECRET",
161
+ "VITE_BSTAGE_TENANT_ID"
162
+ ];
163
+ function usesBstageClient(bundle) {
164
+ return bundle.includes(CLIENT_MARKER);
165
+ }
166
+ function isPlaceholderValue(value) {
167
+ const upper = value.toUpperCase();
168
+ return ["YOUR", "EXAMPLE", "PLACEHOLDER", "XXX"].some((hint) => upper.includes(hint));
169
+ }
170
+ function collectReferencedVars(sources) {
171
+ const referenced = /* @__PURE__ */ new Set();
172
+ for (const source of sources) {
173
+ for (const varName of CREDENTIAL_ENV_VARS) {
174
+ if (source.includes(`import.meta.env.${varName}`)) referenced.add(varName);
175
+ }
176
+ }
177
+ return referenced;
178
+ }
179
+ function checkCredentials(env2, referenced) {
180
+ const issues = [];
181
+ for (const varName of CREDENTIAL_ENV_VARS) {
182
+ if (!referenced.has(varName)) continue;
183
+ const value = env2[varName]?.trim();
184
+ if (!value) {
185
+ issues.push({
186
+ level: "error",
187
+ varName,
188
+ message: "\uAC12\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uBC88\uB4E4\uC5D0 undefined\uAC00 \uBC15\uD600 \uBC30\uD3EC \uD6C4 401\uC774 \uB0A9\uB2C8\uB2E4."
189
+ });
190
+ continue;
191
+ }
192
+ if (isPlaceholderValue(value)) {
193
+ issues.push({
194
+ level: "error",
195
+ varName,
196
+ message: "\uC790\uB9AC\uD45C\uC2DC\uC790\uAC00 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4 \u2014 \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC73C\uB85C \uBC14\uAFB8\uC138\uC694."
197
+ });
198
+ }
199
+ }
200
+ const appId = referenced.has("VITE_BSTAGE_APP_ID") ? env2.VITE_BSTAGE_APP_ID?.trim() : void 0;
201
+ const appSecret = referenced.has("VITE_BSTAGE_APP_SECRET") ? env2.VITE_BSTAGE_APP_SECRET?.trim() : void 0;
202
+ const appIdUsable = !!appId && !isPlaceholderValue(appId);
203
+ const appSecretUsable = !!appSecret && !isPlaceholderValue(appSecret);
204
+ const swapped = appIdUsable && appId.startsWith(APP_SECRET_PREFIX) || appSecretUsable && appSecret.startsWith(APP_ID_PREFIX);
205
+ if (swapped) {
206
+ issues.push({
207
+ level: "error",
208
+ varName: "VITE_BSTAGE_APP_ID",
209
+ message: `APP_ID\uC640 APP_SECRET\uC774 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4 \u2014 APP_ID\uB294 "${APP_ID_PREFIX}", APP_SECRET\uC740 "${APP_SECRET_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.`
210
+ });
211
+ return issues;
212
+ }
213
+ if (appIdUsable && !appId.startsWith(APP_ID_PREFIX)) {
214
+ issues.push({
215
+ level: "warn",
216
+ varName: "VITE_BSTAGE_APP_ID",
217
+ message: `"${APP_ID_PREFIX}"\uB85C \uC2DC\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uD30C\uD2B8\uB108 \uCF58\uC194 \uBC1C\uAE09 \uAC12\uC774 \uB9DE\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`
218
+ });
219
+ }
220
+ if (appSecretUsable && !appSecret.startsWith(APP_SECRET_PREFIX)) {
221
+ issues.push({
222
+ level: "warn",
223
+ varName: "VITE_BSTAGE_APP_SECRET",
224
+ message: `"${APP_SECRET_PREFIX}"\uB85C \uC2DC\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uD30C\uD2B8\uB108 \uCF58\uC194 \uBC1C\uAE09 \uAC12\uC774 \uB9DE\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`
225
+ });
226
+ }
227
+ return issues;
228
+ }
229
+
230
+ // src/commands/build.ts
231
+ var ELEMENT_NAME_RE = /^[a-z][a-z0-9-]*-[a-z0-9-]*$/;
232
+ async function exists(path) {
233
+ try {
234
+ await access(path);
235
+ return true;
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
241
+ var PAGES_ROOT = "src/pages";
242
+ var SLOTS_ROOT = "src/slots";
243
+ var LEGACY_ROOT = "src/templates";
244
+ var TEMPLATE_FILE = "template.tsx";
245
+ var DIST_DIR = "dist";
246
+ var STAGING_DIR = ".staging";
247
+ function entryLabel(entry) {
248
+ if (entry.kind === "page") return entry.segments.length > 0 ? `/${entry.segments.join("/")}` : "/";
249
+ return [SLOTS_ROOT, ...entry.segments].join("/");
250
+ }
251
+ async function discoverEntries(cwd) {
252
+ const roots = [
253
+ ["page", PAGES_ROOT],
254
+ ["slot", SLOTS_ROOT]
255
+ ];
256
+ const results = [];
257
+ async function walk(kind, dir, segments) {
258
+ let dirents;
259
+ try {
260
+ dirents = await readdir(dir, { withFileTypes: true });
261
+ } catch {
262
+ return;
263
+ }
264
+ for (const d of dirents) {
265
+ if (IGNORE_DIRS.has(d.name) || d.name.startsWith(".")) continue;
266
+ const fullPath = resolve(dir, d.name);
267
+ if (d.isDirectory()) {
268
+ await walk(kind, fullPath, [...segments, d.name]);
269
+ } else if (d.name === TEMPLATE_FILE) {
270
+ results.push({ kind, entry: relative(cwd, fullPath), segments });
271
+ }
272
+ }
273
+ }
274
+ for (const [kind, root] of roots) {
275
+ await walk(kind, resolve(cwd, root), []);
276
+ }
277
+ return results.sort((a, b) => a.entry.localeCompare(b.entry));
278
+ }
279
+ var BuildRuleError = class extends Error {
280
+ };
281
+ var DYNAMIC_SEGMENT_RE = /^\[.*\]$/;
282
+ function resolveOutputName(entry, slot) {
283
+ const label = entryLabel(entry);
284
+ if (entry.kind === "page") {
285
+ if (slot) {
286
+ throw new BuildRuleError(
287
+ `${label}: \uD398\uC774\uC9C0(${PAGES_ROOT}/)\uC5D0\uB294 slot \uC635\uC158\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
288
+ \uBC30\uD3EC \uACBD\uB85C\uB294 ${PAGES_ROOT}/ \uC544\uB798 \uD3F4\uB354 \uAD6C\uC870\uAC00 \uC815\uD569\uB2C8\uB2E4.
289
+ \uC704\uC82F\uC73C\uB85C \uB9CC\uB4E4 \uC0DD\uAC01\uC774\uC5C8\uB2E4\uBA74 \uC774 \uD3F4\uB354\uB97C ${SLOTS_ROOT}/ \uC544\uB798\uB85C \uC62E\uAE30\uC138\uC694.`
290
+ );
291
+ }
292
+ const dynamic = entry.segments.find((s) => DYNAMIC_SEGMENT_RE.test(s));
293
+ if (dynamic) {
294
+ throw new BuildRuleError(
295
+ `${label}: \uB3D9\uC801 \uACBD\uB85C("${dynamic}")\uB294 \uC544\uC9C1 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
296
+ \uACE0\uC815 \uACBD\uB85C \uD3F4\uB354\uB85C \uBC14\uAFD4\uC8FC\uC138\uC694.`
297
+ );
298
+ }
299
+ return entry.segments.join("/");
300
+ }
301
+ if (!slot) {
302
+ throw new BuildRuleError(
303
+ `${label}: \uC704\uC82F(${SLOTS_ROOT}/)\uC5D0\uB294 slot \uC635\uC158\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.
304
+ createTemplate(Component, { name: '...', slot: 'user.contents-home.curation:after' })
305
+ \uD3F4\uB354 \uC774\uB984\uC740 \uBC30\uCE58\uC5D0 \uC4F0\uC774\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uC5B4\uB290 \uC790\uB9AC\uC5D0 \uBD99\uC744\uC9C0\uB294 slot \uC635\uC158\uB9CC \uC815\uD569\uB2C8\uB2E4.`
306
+ );
307
+ }
308
+ if (!isSlotIdV2(slot)) {
309
+ throw new BuildRuleError(
310
+ `${label}: \uCE74\uD0C8\uB85C\uADF8\uC5D0 \uC5C6\uB294 \uC2AC\uB86F id "${slot}"\uC785\uB2C8\uB2E4.
311
+ \uC4F8 \uC218 \uC788\uB294 \uC790\uB9AC:
312
+ ${formatSlotSuggestions(slot)}`
313
+ );
314
+ }
315
+ return slotIdToDirName(slot);
316
+ }
317
+ function formatSlotSuggestions(slot) {
318
+ const all = [...getSlotsByTargetV2("user"), ...getSlotsByTargetV2("admin")].map((s) => s.id);
319
+ const prefix = slot.split(".").slice(0, 2).join(".");
320
+ const near = all.filter((id) => id.startsWith(`${prefix}.`));
321
+ const shown = near.length > 0 ? near : all;
322
+ const lines = shown.slice(0, 12).map((id) => ` - ${id}`);
323
+ if (shown.length > lines.length) lines.push(` ... \uC804\uCCB4 \uBAA9\uB85D\uC740 SLOT_CATALOG_V2 \uCC38\uC870`);
324
+ return lines.join("\n");
325
+ }
326
+ var SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
327
+ async function readProjectSources(cwd) {
328
+ const sources = [];
329
+ async function walk(dir) {
330
+ let dirents;
331
+ try {
332
+ dirents = await readdir(dir, { withFileTypes: true });
333
+ } catch {
334
+ return;
335
+ }
336
+ for (const d of dirents) {
337
+ if (IGNORE_DIRS.has(d.name)) continue;
338
+ const fullPath = resolve(dir, d.name);
339
+ if (d.isDirectory()) {
340
+ await walk(fullPath);
341
+ } else if (SOURCE_EXT_RE.test(d.name) && !d.name.endsWith(".d.ts")) {
342
+ try {
343
+ sources.push(await readFile(fullPath, "utf-8"));
344
+ } catch {
345
+ }
346
+ }
347
+ }
348
+ }
349
+ await walk(cwd);
350
+ return sources;
351
+ }
352
+ async function buildEntry(ctx, entryInfo) {
353
+ const { cwd } = ctx;
354
+ const { entry } = entryInfo;
355
+ const label = entryLabel(entryInfo);
356
+ console.log(`
357
+ Building template: ${label}`);
358
+ const { plugin: metaPlugin, getMeta } = createMetaPlugin();
359
+ const outDir = join(DIST_DIR, STAGING_DIR);
360
+ const viteConfig2 = createViteConfig({ entry, outDir });
361
+ await viteBuild({
362
+ ...viteConfig2,
363
+ configFile: false,
364
+ root: cwd,
365
+ logLevel: "info",
366
+ plugins: [...viteConfig2.plugins ?? [], metaPlugin, createRegisterPlugin()]
367
+ });
368
+ const bundlePath = join(cwd, outDir, "template.js");
369
+ let bundleSize = 0;
370
+ let usesClient = false;
371
+ try {
372
+ const bundle = await readFile(bundlePath);
373
+ bundleSize = bundle.length;
374
+ usesClient = usesBstageClient(bundle);
375
+ } catch {
376
+ console.warn("Warning: Could not read the built bundle.");
377
+ }
378
+ const bundleMeta = getMeta();
379
+ const name = bundleMeta?.name;
380
+ if (!name) {
381
+ console.error(`Error: Could not determine template metadata (name) for "${label}".`);
382
+ console.error("Ensure createTemplate() is called at module level with name.");
383
+ process.exit(1);
384
+ }
385
+ if (!ELEMENT_NAME_RE.test(name)) {
386
+ console.error(
387
+ `Error: \uC798\uBABB\uB41C name "${name}". Custom Element \uC2A4\uD399\uC0C1 \uC18C\uBB38\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uD558\uC774\uD508\uC744 1\uAC1C \uC774\uC0C1 \uD3EC\uD568\uD574\uC57C \uD558\uBA70, \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC608: "bmf-hello"`
388
+ );
389
+ process.exit(1);
390
+ }
391
+ const elementName = bundleMeta?.elementName ?? name;
392
+ let outputName;
393
+ try {
394
+ outputName = resolveOutputName(entryInfo, bundleMeta?.slot);
395
+ } catch (e) {
396
+ if (!(e instanceof BuildRuleError)) throw e;
397
+ console.error(`
398
+ ${pc.red("Error")} ${e.message}`);
399
+ process.exit(1);
400
+ }
401
+ const previousOwner = ctx.takenOutputs.get(outputName);
402
+ if (previousOwner !== void 0) {
403
+ console.error(
404
+ `
405
+ ${pc.red("Error")} ${label}: \uC0B0\uCD9C\uBB3C \uC704\uCE58 "${join(DIST_DIR, outputName)}"\uAC00 ${previousOwner}\uC640 \uACB9\uCE69\uB2C8\uB2E4.
406
+ \uB450 \uD15C\uD50C\uB9BF\uC774 \uAC19\uC740 \uC790\uB9AC\uB85C \uB098\uAC08 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
407
+ );
408
+ process.exit(1);
409
+ }
410
+ ctx.takenOutputs.set(outputName, label);
411
+ const elementNameOwner = ctx.takenElementNames.get(elementName);
412
+ if (elementNameOwner !== void 0) {
413
+ console.error(
414
+ `
415
+ ${pc.red("Error")} ${label}: name "${elementName}"\uC744 ${elementNameOwner}\uAC00 \uC774\uBBF8 \uC501\uB2C8\uB2E4.
416
+ Custom Element \uD0DC\uADF8\uBA85\uC740 \uD15C\uD50C\uB9BF\uB9C8\uB2E4 \uB2EC\uB77C\uC57C \uD569\uB2C8\uB2E4.`
417
+ );
418
+ process.exit(1);
419
+ }
420
+ ctx.takenElementNames.set(elementName, label);
421
+ const finalDir = outputName ? join(cwd, DIST_DIR, outputName) : join(cwd, DIST_DIR);
422
+ await mkdir(finalDir, { recursive: true });
423
+ await rename(join(cwd, outDir, "template.js"), join(finalDir, "template.js"));
424
+ console.log(` ${relative(cwd, finalDir)}/template.js (${formatSize(bundleSize)})`);
425
+ return { usesClient };
426
+ }
427
+ function printCredentialReport(env2, referenced) {
428
+ const issues = checkCredentials(env2, referenced);
429
+ if (issues.length === 0) return;
430
+ const hasError = issues.some((i) => i.level === "error");
431
+ console.log("");
432
+ console.log(
433
+ hasError ? pc.red(pc.bold("\u2717 \uC778\uC99D \uAC12 \uC810\uAC80 \u2014 \uC774\uB300\uB85C \uBC30\uD3EC\uD558\uBA74 API \uD638\uCD9C\uC774 \uC2E4\uD328\uD569\uB2C8\uB2E4.")) : pc.yellow(pc.bold("\u26A0 \uC778\uC99D \uAC12 \uC810\uAC80"))
434
+ );
435
+ for (const i of issues) {
436
+ const mark = i.level === "error" ? pc.red("\u2717") : pc.yellow("\u26A0");
437
+ console.log(` ${mark} ${i.varName} ${i.message}`);
438
+ }
439
+ console.log(
440
+ pc.dim(
441
+ '\n .env \uAC12\uC740 \uBE4C\uB4DC \uC2DC\uC810\uC5D0 \uBC88\uB4E4\uB85C \uC778\uB77C\uC778\uB429\uB2C8\uB2E4 \u2014 \uAC12\uC744 \uACE0\uCE5C \uB4A4 \uB2E4\uC2DC \uBE4C\uB4DC\uD574\uC57C \uBC18\uC601\uB429\uB2C8\uB2E4.\n \uBC30\uD3EC\uD560 \uD658\uACBD\uC5D0\uC11C \uBC1C\uAE09\uD55C \uD0A4\uC778\uC9C0\uB3C4 \uD655\uC778\uD558\uC138\uC694(\uD0A4 \uD3EC\uB9F7\uB9CC\uC73C\uB85C\uB294 SDK\uAC00 \uAD6C\uBD84\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4).\n \uBC30\uD3EC \uC804 \uC810\uAC80 \uBAA9\uB85D: SDK \uBB38\uC11C\uC758 GETTING_STARTED.md \u2014 "9. \uBC30\uD3EC"'
442
+ )
443
+ );
444
+ console.log("");
445
+ }
446
+ async function buildCommand() {
447
+ const cwd = process.cwd();
448
+ const entries = await discoverEntries(cwd);
449
+ if (entries.length === 0) {
450
+ console.error(
451
+ `Error: ${PAGES_ROOT}/ \xB7 ${SLOTS_ROOT}/ \uC544\uB798\uC5D0\uC11C ${TEMPLATE_FILE}\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.`
452
+ );
453
+ if (await exists(join(cwd, LEGACY_ROOT))) {
454
+ console.error(
455
+ `
456
+ ${LEGACY_ROOT}/ \uC774 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4. \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C \uACBD\uB85C \uADDC\uCE59\uC774 \uBC14\uB00C\uBA74\uC11C
457
+ \uD398\uC774\uC9C0\uB294 ${PAGES_ROOT}/, \uC704\uC82F\uC740 ${SLOTS_ROOT}/ \uC544\uB798\uB85C \uC62E\uACA8\uC57C \uD569\uB2C8\uB2E4.
458
+ \uC9C4\uB2E8: npx @bstage-sdk/cli@latest doctor
459
+ \uBCC0\uD658: \uC5D0\uC774\uC804\uD2B8\uC5D0\uAC8C "bstage \uB9C8\uC774\uADF8\uB808\uC774\uC158" \uC694\uCCAD (bstage-migrate \uC2A4\uD0AC)`
460
+ );
461
+ }
462
+ process.exit(1);
463
+ }
464
+ await rm(join(cwd, DIST_DIR), { recursive: true, force: true });
465
+ const ctx = {
466
+ cwd,
467
+ takenOutputs: /* @__PURE__ */ new Map(),
468
+ takenElementNames: /* @__PURE__ */ new Map()
469
+ };
470
+ console.log(
471
+ `Found ${entries.length} template${entries.length > 1 ? "s" : ""}: ${entries.map(entryLabel).join(", ")}`
472
+ );
473
+ const results = [];
474
+ for (const entry of entries) {
475
+ results.push(await buildEntry(ctx, entry));
476
+ }
477
+ await rm(join(cwd, DIST_DIR, STAGING_DIR), { recursive: true, force: true });
478
+ console.log(`
479
+ Build complete.`);
480
+ if (results.some((r) => r.usesClient)) {
481
+ const referenced = collectReferencedVars(await readProjectSources(cwd));
482
+ printCredentialReport(loadEnv("production", cwd, "VITE_"), referenced);
483
+ }
484
+ }
485
+ function formatSize(bytes) {
486
+ if (bytes < 1024) return `${bytes} B`;
487
+ const kb = bytes / 1024;
488
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
489
+ const mb = kb / 1024;
490
+ return `${mb.toFixed(2)} MB`;
491
+ }
492
+
493
+ // src/commands/dev.ts
494
+ import { createServer as createViteServer, searchForWorkspaceRoot } from "vite";
495
+ import react from "@vitejs/plugin-react";
496
+ async function devCommand(options) {
497
+ const port = parseInt(options.port, 10);
498
+ const root = process.cwd();
499
+ let resolved;
500
+ try {
501
+ resolved = resolvePhase(options.phase, root);
502
+ } catch (err) {
503
+ console.error(`[bstage] ${err.message}`);
504
+ process.exit(1);
505
+ }
506
+ const phase = resolved.phase;
507
+ console.log(describePhase(resolved));
508
+ const plugins = [react(), bstageDevPlugin({ phase }), createRegisterPlugin()];
509
+ let vite;
510
+ try {
511
+ vite = await createViteServer({
512
+ root,
513
+ configFile: false,
514
+ plugins,
515
+ server: {
516
+ port,
517
+ strictPort: true,
518
+ // 프로젝트 루트 밖 파일은 내보내지 않는다. 워크스페이스 루트를 함께 허용하는 것은
519
+ // 모노레포·pnpm 스토어에서 의존 패키지 실제 경로가 루트 위에 있기 때문이다.
520
+ //
521
+ // 허용 경로를 넓혀야 한다면(로컬 링크 개발 등) 스캐폴드의 `npm run dev`를 쓴다.
522
+ // 그쪽은 소비자의 `vite.config.ts`를 타므로 `server.fs.allow`를 직접 넣을 수 있다.
523
+ fs: { strict: true, allow: [root, searchForWorkspaceRoot(root)] }
524
+ }
525
+ });
526
+ } catch (err) {
527
+ console.error(`[bstage] ${err.message}`);
528
+ process.exit(1);
529
+ }
530
+ await vite.listen();
531
+ vite.printUrls();
532
+ const resolvedPort = vite.config.server.port ?? port;
533
+ console.log(` Login: http://localhost:${resolvedPort}${LOGIN_PATH}
534
+ `);
535
+ }
536
+
537
+ // src/commands/init.ts
538
+ import * as p from "@clack/prompts";
539
+ import pc2 from "picocolors";
540
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
541
+ import { execSync } from "child_process";
542
+ import { resolve as resolve2, join as join2 } from "path";
543
+
544
+ // src/templates/agentsMdRegion.ts
545
+ var AGENTS_MANAGED_VERSION = 10;
546
+ var START_MARKER = `<!-- BSTAGE:MANAGED:START v=${AGENTS_MANAGED_VERSION} \u2014 SDK\uAC00 \uAD00\uB9AC\uD558\uB294 \uC601\uC5ED\uC785\uB2C8\uB2E4. \`bstage skills install\` / \`bstage migrate\`\uAC00 \uC7AC\uC0DD\uC131\uD558\uBBC0\uB85C \uC9C1\uC811 \uD3B8\uC9D1\uD558\uC9C0 \uB9C8\uC138\uC694(\uAC31\uC2E0 \uC2DC \uB36E\uC5B4\uC4F0\uC785\uB2C8\uB2E4). \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uADDC\uCE59\uC740 \uC544\uB798 \uC790\uC720 \uC601\uC5ED\uC5D0 \uC801\uC73C\uC138\uC694. -->`;
547
+ var END_MARKER = `<!-- BSTAGE:MANAGED:END -->`;
548
+ var START_RE = /<!--\s*BSTAGE:MANAGED:START(?:\s+v=(\d+))?[\s\S]*?-->/;
549
+ var END_RE = /<!--\s*BSTAGE:MANAGED:END[\s\S]*?-->/;
550
+ var FREE_REGION_NOTE = `<!-- \uC544\uB798\uB294 \uD504\uB85C\uC81D\uD2B8 \uC790\uC720 \uC601\uC5ED\uC785\uB2C8\uB2E4. SDK \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC774 \uBCF4\uC874\uD569\uB2C8\uB2E4. \uC774 \uD504\uB85C\uC81D\uD2B8\uB9CC\uC758 \uADDC\uCE59\xB7\uCEE8\uD14D\uC2A4\uD2B8\uB97C \uC5EC\uAE30\uC5D0 \uC801\uC73C\uC138\uC694. -->`;
551
+ function wrapManaged(body) {
552
+ return `${START_MARKER}
553
+ ${body}
554
+ ${END_MARKER}`;
555
+ }
556
+ function freeRegionScaffold() {
557
+ return `${FREE_REGION_NOTE}
558
+
559
+ ## \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59
560
+
561
+ <!-- \uC774 \uD504\uB85C\uC81D\uD2B8\uB9CC\uC758 \uADDC\uCE59\xB7\uCEE8\uD14D\uC2A4\uD2B8\uB97C \uC5EC\uAE30\uC5D0 \uC791\uC131\uD558\uC138\uC694. -->
562
+ `;
563
+ }
564
+ function readManagedVersion(content) {
565
+ const m = START_RE.exec(content);
566
+ if (!m) return null;
567
+ return m[1] ? parseInt(m[1], 10) : 0;
568
+ }
569
+ function agentsMdStatus(content) {
570
+ const v = readManagedVersion(content);
571
+ if (v === null) return "legacy";
572
+ return v < AGENTS_MANAGED_VERSION ? "stale" : "ok";
573
+ }
574
+ function swapManagedBlock(existing, newWrappedBlock) {
575
+ const s = START_RE.exec(existing);
576
+ const e = END_RE.exec(existing);
577
+ if (!s || !e) return null;
578
+ const startIdx = s.index;
579
+ const endIdx = e.index + e[0].length;
580
+ if (endIdx <= startIdx) return null;
581
+ return existing.slice(0, startIdx) + newWrappedBlock + existing.slice(endIdx);
582
+ }
583
+ function parseAgentsIdentity(content) {
584
+ const space = /\*\*Space\*\*:\s*([A-Za-z0-9-]+)/.exec(content)?.[1];
585
+ const projectName = /\*\*레포\*\*:\s*`([^`]+)`/.exec(content)?.[1];
586
+ return { space, projectName };
587
+ }
588
+
589
+ // src/templates/designGuide.ts
590
+ function meta(target) {
591
+ if (target === "admin") {
592
+ return {
593
+ pkg: "@bstage-sdk/design/admin",
594
+ prefix: "admin",
595
+ colorExample: "color.text.primary",
596
+ cssVarExample: "text/primary",
597
+ textStyleExample: "16/title/med",
598
+ shadowExample: "default-large",
599
+ themeNote: "\uC5B4\uB4DC\uBBFC\uC740 **\uB77C\uC774\uD2B8 \uC804\uC6A9**(\uB2E4\uD06C\uBAA8\uB4DC \uC5C6\uC74C)."
600
+ };
601
+ }
602
+ return {
603
+ pkg: "@bstage-sdk/design/user",
604
+ prefix: "user",
605
+ colorExample: "color.text.secondary",
606
+ cssVarExample: "text/secondary",
607
+ textStyleExample: "16/title/semibold",
608
+ shadowExample: "default-large",
609
+ themeNote: "\uC0C9 \uD1A0\uD070\uC740 \uB77C\uC774\uD2B8/\uB2E4\uD06C \uC790\uB3D9 \uB300\uC751 \u2014 \uD50C\uB7AB\uD3FC \uD14C\uB9C8\uB97C \uB530\uB77C \uAC12\uC774 \uBC14\uB010\uB2E4."
610
+ };
611
+ }
612
+ function designRulesBrief(target) {
613
+ const m = meta(target);
614
+ return `## \uB514\uC790\uC778 (bstage \uB514\uC790\uC778 \uD1A0\uD070)
615
+
616
+ UI\uC758 \uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790\uB294 **\uBC18\uB4DC\uC2DC \`${m.pkg}\`\uC758 \uD1A0\uD070\uB9CC** \uC0AC\uC6A9\uD55C\uB2E4. hex/rgb \uD558\uB4DC\uCF54\uB529 \uAE08\uC9C0.
617
+
618
+ - **\uC0C9**: \`${m.colorExample}\`(\uC911\uCCA9 \uD2B8\uB9AC) \uB610\uB294 \`cssVar('${m.cssVarExample}')\` \u2192 \`var(--${m.prefix}-...)\`. \uCE74\uD14C\uACE0\uB9AC: bg / text / border / icon / surface / overlay / service
619
+ - **\uD0C0\uC774\uD3EC**: \`textStyle('${m.textStyleExample}')\` \u2192 \uC778\uB77C\uC778 \uC2A4\uD0C0\uC77C\uC5D0 spread
620
+ - **\uADF8\uB9BC\uC790**: \`shadow['${m.shadowExample}']\`
621
+ - **\uC778\uD130\uB799\uC158**(hover/press): overlay \uD1A0\uD070 \u2014 \`color.overlay['hover-a']\` / \`color.overlay['press-a']\`
622
+ - **\uAC04\uACA9\xB7\uB77C\uC6B4\uB4DC**: \uC804\uC6A9 \uD1A0\uD070 \uC5C6\uC74C \u2014 \uC77C\uAD00\uB41C \uC2A4\uCF00\uC77C(4\uC758 \uBC30\uC218 \uAD8C\uC7A5)\xB7\uC77C\uAD00\uB41C radius. \uC784\uC758\uAC12 \uB0A8\uBC1C \uAE08\uC9C0
623
+ - **\uD14C\uB9C8**: ${m.themeNote}
624
+ - \uAC12\uC740 \uD50C\uB7AB\uD3FC\uC774 \uB7F0\uD0C0\uC784\uC5D0 \`:root\`\uB85C \uC8FC\uC785\uD558\uACE0 \uB85C\uCEEC\uC740 \`bstage dev\`\uAC00 fallback \uC8FC\uC785 \u2192 \uAC1C\uBC1C \uC911\uC5D0\uB3C4 \uC2E4\uC81C \uD1A4\uC73C\uB85C \uBCF4\uC778\uB2E4. \uC815\uD655\uD55C \uD1A0\uD070 \uD0A4\uB294 \`node_modules/${m.pkg.split("/").slice(0, 2).join("/")}/dist/${m.prefix}/index.d.ts\` \uCC38\uC870
625
+
626
+ \uC790\uC138\uD55C \uC0AC\uC6A9\uBC95\uC740 SDK \uBB38\uC11C \`DESIGN_TOKENS.md\`(\`bstage docs\`)\uC640 \`bstage-template\` \uC2A4\uD0AC \uCC38\uACE0.`;
627
+ }
628
+ function designGuideSection(target) {
629
+ const m = meta(target);
630
+ const pkgRoot = m.pkg.split("/").slice(0, 2).join("/");
631
+ return `UI \uC2A4\uD0C0\uC77C\uC740 \uC784\uC758 \uAC12 \uB300\uC2E0 **bstage \uB514\uC790\uC778 \uD1A0\uD070**\uB9CC \uC4F4\uB2E4. \uAC12\uC740 \uD50C\uB7AB\uD3FC\uC774 \uB7F0\uD0C0\uC784\uC5D0 \`:root\`\uB85C \uC8FC\uC785\uD558\uACE0, \uB85C\uCEEC \`bstage dev\`\uB294 fallback\uC744 \uC8FC\uC785\uD558\uBBC0\uB85C \uAC1C\uBC1C \uC911\uC5D0\uB3C4 \uC2E4\uC81C \uD1A4\uC73C\uB85C \uBCF4\uC778\uB2E4. **hex/rgb \uD558\uB4DC\uCF54\uB529 \uAE08\uC9C0.**
632
+
633
+ \`\`\`tsx
634
+ import { color, shadow, cssVar, textStyle } from '${m.pkg}'
635
+
636
+ <div
637
+ style={{
638
+ background: color.bg.base,
639
+ color: ${m.colorExample}, // = 'var(--${m.prefix}-mode-text-...)'
640
+ boxShadow: shadow['${m.shadowExample}'],
641
+ ...textStyle('${m.textStyleExample}'), // fontSize/lineHeight/fontWeight/letterSpacing
642
+ }}
643
+ />
644
+ \`\`\`
645
+
646
+ - **\uC0C9**: \`color.<category>.<name>\` \uC911\uCCA9 \uD2B8\uB9AC, \uB610\uB294 \uD0C0\uC785 \uC548\uC804 \`cssVar('${m.cssVarExample}')\`. \uCE74\uD14C\uACE0\uB9AC: bg / text / border / icon / surface / overlay / service
647
+ - **\uD0C0\uC774\uD3EC**: \`textStyle('{size}/{role}/{weight}')\` \u2192 \uC778\uB77C\uC778 \uC2A4\uD0C0\uC77C \uAC1D\uCCB4(spread). \uC815\uD655\uD55C \uD0A4\uB294 \uD0C0\uC785 \uD655\uC778
648
+ - **\uADF8\uB9BC\uC790**: \`shadow['<name>']\`
649
+ - **\uC778\uD130\uB799\uC158**: hover/press\uB294 overlay \uD1A0\uD070\uC744 \uC704\uC5D0 \uC5B9\uB294\uB2E4 \u2014 \`color.overlay['hover-a']\`, \`color.overlay['press-a']\`
650
+ - **\uAC04\uACA9\xB7\uB77C\uC6B4\uB4DC**: \uC804\uC6A9 \uD1A0\uD070 \uC5C6\uC74C \u2014 \uC77C\uAD00\uB41C \uC2A4\uCF00\uC77C(4\uC758 \uBC30\uC218)\xB7\uC77C\uAD00\uB41C radius\uB97C \uC4F0\uACE0 \uC784\uC758\uAC12\uC744 \uB0A8\uBC1C\uD558\uC9C0 \uC54A\uB294\uB2E4
651
+ - **\uD14C\uB9C8**: ${m.themeNote}
652
+ - **\uC815\uD655\uD55C \uD1A0\uD070 \uBAA9\uB85D(SSOT)**: \`node_modules/${pkgRoot}/dist/${m.prefix}/index.d.ts\`. \uC0AC\uC6A9 \uAC00\uC774\uB4DC\uB294 SDK \uBB38\uC11C \`DESIGN_TOKENS.md\`(\`bstage docs\`). \uAE30\uC5B5\uC5D0 \uC758\uC874\uD574 \uD1A0\uD070\uBA85\uC744 \uCD94\uCE21\uD558\uC9C0 \uC54A\uB294\uB2E4`;
653
+ }
654
+
655
+ // src/templates/agentsMd.ts
656
+ function agentsManagedBody({
657
+ space,
658
+ projectName,
659
+ target = "user"
660
+ }) {
661
+ const structureBlock = `src/
662
+ main.tsx \u2014 App \uB9C8\uC6B4\uD2B8\uB9CC (\uC218\uC815 \uBD88\uD544\uC694)
663
+ App.tsx \u2014 import.meta.glob \uAE30\uBC18 \uAC1C\uBC1C\uC6A9 \uBAA9\uB85D\xB7\uB77C\uC6B0\uD305 (\uC218\uC815 \uBD88\uD544\uC694)
664
+ shared/
665
+ client.ts \u2014 BstageClient \uC778\uC2A4\uD134\uC2A4 (API \uD0A4\uB294 .env\uC5D0\uC11C \uC8FC\uC785). \uC720\uC800\uB2E8 API\uB9CC \uC9C0\uC6D0
666
+ pages/
667
+ {\uACBD\uB85C}/
668
+ template.tsx \u2014 \uD398\uC774\uC9C0 (\uD3F4\uB354 \uAD6C\uC870\uAC00 \uACE7 \uBC30\uD3EC \uACBD\uB85C)
669
+ slots/
670
+ {\uC544\uBB34 \uC774\uB984}/
671
+ template.tsx \u2014 \uC704\uC82F (createTemplate\uC758 slot \uC635\uC158\uC774 \uC790\uB9AC\uB97C \uC815\uD568)
672
+ .env \u2014 VITE_BSTAGE_PHASE + API \uD0A4 (\uCEE4\uBC0B \uC548 \uB428, .gitignore)
673
+ .env.example \u2014 \uC704 \uD56D\uBAA9 placeholder (\uCEE4\uBC0B\uB428 \u2014 \uBCF5\uC0AC\uD574\uC11C .env \uC0DD\uC131)
674
+ vite.config.ts \u2014 Vite + bstageDevPlugin (phase\uB294 .env\uC5D0\uC11C \uC77D\uC74C)`;
675
+ const envSection = `\`.env\`(\uCEE4\uBC0B \uC548 \uB428)\uB294 phase\uC640 API \uD0A4\uB97C, \`.env.example\`(\uCEE4\uBC0B\uB428)\uC740 \uADF8 placeholder\uB97C \uB2F4\uB294\uB2E4. \`client.ts\`\uB294 \`import.meta.env.VITE_BSTAGE_APP_*\`\uB97C, \`vite.config.ts\`\uB294 \`.env\`\uC758 phase\uB97C \uCC38\uC870\uD55C\uB2E4.
676
+
677
+ \`\`\`
678
+ VITE_BSTAGE_PHASE=dev # dev | qa | real | sandbox
679
+ \`\`\``;
680
+ const commandsSection = `- \`npm run dev\` \u2014 \uAC1C\uBC1C \uC11C\uBC84 \uC2E4\uD589 (\uC778\uC99D \uD504\uB85D\uC2DC \uD3EC\uD568)
681
+ - \uB85C\uADF8\uC778 \uD398\uC774\uC9C0: http://localhost:5173/__bstage__/login`;
682
+ const apiForbidBullet = `- \uACBD\uB85C\xB7\uC751\uB2F5 \uBAA8\uC591\uC744 \uCD94\uCE21\uD574 \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 SDK\uB294 API \uACBD\uB85C \uBAA9\uB85D\uC744 \uB4E4\uACE0 \uC788\uC9C0 \uC54A\uB2E4(\uC790\uB3D9\uC644\uC131 \uC5C6\uC74C). \uCD9C\uCC98\uB294 \uAC8C\uC774\uD2B8\uC6E8\uC774 API Reference Doc\uC774\uACE0, \uC751\uB2F5 \uD0C0\uC785\uC740 \`client.get<T>(...)\`\uCC98\uB7FC \uC81C\uB124\uB9AD\uC73C\uB85C \uBA85\uC2DC\uD55C\uB2E4.
683
+ - \uC5B4\uB4DC\uBBFC\uC6A9 API\uB97C \uCD94\uCE21\uD574 \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 BstageClient\uB294 **\uC720\uC800\uB2E8 API\uB9CC \uC9C0\uC6D0**\uD55C\uB2E4. \uC5B4\uB4DC\uBBFC API\uAC00 \uD544\uC694\uD558\uBA74 \uACBD\uB85C\uB97C \uC784\uC758\uB85C \uB9CC\uB4E4\uC9C0 \uB9D0\uACE0 **\uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBA3C\uC800 \uD655\uC778**\uD55C\uB2E4.`;
684
+ return `# AGENTS.md
685
+
686
+ ## \uD504\uB85C\uC81D\uD2B8 \uAC1C\uC694
687
+
688
+ b.stage \uC11C\uB4DC\uD30C\uD2F0 \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8. React \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC791\uC131\uD558\uBA74 SDK\uAC00 Web Component\uB85C \uBE4C\uB4DC\uD558\uACE0, \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC774 b.stage \uD50C\uB7AB\uD3FC\uC5D0\uC11C \uB85C\uB4DC\uB418\uC5B4 \uC2E4\uD589\uB41C\uB2E4.
689
+
690
+ - **Space**: ${space}
691
+ - **\uB808\uD3EC**: \`${projectName}\`
692
+
693
+ ## SDK \uBB38\uC11C
694
+
695
+ SDK\uC758 API, hooks, \uC2AC\uB86F \uC2DC\uC2A4\uD15C, \uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778 \uB4F1 **SDK\uC640 \uAD00\uB828\uB41C \uBAA8\uB4E0 \uB0B4\uC6A9**\uC740 \uC124\uCE58\uB41C \uD328\uD0A4\uC9C0\uC758 \uBB38\uC11C\uB97C \uCC38\uC870\uD55C\uB2E4. \uC774 \uBB38\uC11C\uB4E4\uC774 \uD56D\uC0C1 \uCD5C\uC2E0\uC774\uBA70 \uC678\uBD80 \uAC80\uC0C9 \uACB0\uACFC\uBCF4\uB2E4 \uC2E0\uB8B0\uD560 \uC218 \uC788\uB2E4.
696
+
697
+ \`\`\`
698
+ node_modules/@bstage-sdk/core/docs/
699
+ \u251C\u2500\u2500 GETTING_STARTED.md \u2014 \uBE60\uB978 \uC2DC\uC791 \uAC00\uC774\uB4DC
700
+ \u251C\u2500\u2500 API_REFERENCE.md \u2014 createTemplate, hooks, BstageClient, PlatformBridge API
701
+ \u251C\u2500\u2500 I18N.md \u2014 \uB2E4\uAD6D\uC5B4 \uD6C5 + bstage i18n pull (\uBC88\uC5ED \uCF54\uB4DC\uC820) \uC6CC\uD06C\uD50C\uB85C
702
+ \u251C\u2500\u2500 DESIGN_TOKENS.md \u2014 \uB514\uC790\uC778 \uD1A0\uD070 (\uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790) \uC0AC\uC6A9 \uAC00\uC774\uB4DC
703
+ \u251C\u2500\u2500 SLOT_PROTOCOL.md \u2014 \uC2AC\uB86F \uC2DC\uC2A4\uD15C v1 \uD504\uB85C\uD1A0\uCF5C (3-part \uD0A4\xB7context \uB2E8\uBC29\uD5A5)
704
+ \u251C\u2500\u2500 SLOT_CATALOG.md \u2014 \uC2AC\uB86F \uBAA9\uB85D, \uC774\uBCA4\uD2B8 \uC778\uD130\uD398\uC774\uC2A4, resourceId \uC758\uBBF8
705
+ \u251C\u2500\u2500 SLOT_SYSTEM.md \u2014 \uC2AC\uB86F \uC2DC\uC2A4\uD15C \uC124\uACC4, \uB7F0\uD0C0\uC784 \uD750\uB984
706
+ \u251C\u2500\u2500 BUILD_SYSTEM.md \u2014 \uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778, \uC0B0\uCD9C\uBB3C \uACBD\uB85C \uADDC\uCE59
707
+ \u251C\u2500\u2500 DEV_SERVER.md \u2014 \uB85C\uCEEC \uAC1C\uBC1C \uC11C\uBC84, \uC778\uC99D \uD504\uB85D\uC2DC
708
+ \u251C\u2500\u2500 MIGRATION.md \u2014 \uBC84\uC804 \uC0AC\uC774 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uAC00\uC774\uB4DC (bstage doctor \uC5F0\uACC4)
709
+ \u2514\u2500\u2500 SDK_ARCHITECTURE.md \u2014 \uD328\uD0A4\uC9C0 \uAD6C\uC870, \uC124\uACC4 \uACB0\uC815
710
+ \`\`\`
711
+
712
+ \uBB38\uC11C \uBAA9\uB85D\uC740 \`npx bstage docs\`\uB85C\uB3C4 \uBCFC \uC218 \uC788\uB2E4. \uD0C0\uC785 \uC815\uC758(.d.ts)\uC640 \uB7F0\uD0C0\uC784 \uC18C\uC2A4(.js)\uB294 \`node_modules/@bstage-sdk/react/dist/\`, \`node_modules/@bstage-sdk/core/dist/\`\uC5D0\uC11C \uC9C1\uC811 \uD655\uC778\uD560 \uC218 \uC788\uB2E4.
713
+
714
+ ## \uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870
715
+
716
+ \`\`\`
717
+ ${structureBlock}
718
+ \`\`\`
719
+
720
+ ## \uD15C\uD50C\uB9BF \uAC1C\uBC1C \uADDC\uCE59
721
+
722
+ ### \uC0C8 \uD15C\uD50C\uB9BF \uCD94\uAC00
723
+
724
+ \uD398\uC774\uC9C0\uB0D0 \uC704\uC82F\uC774\uB0D0\uC5D0 \uB530\uB77C \uB450\uB294 \uACF3\uC774 \uB2E4\uB974\uB2E4. \uD30C\uC77C\uBA85\uC740 \uBC18\uB4DC\uC2DC \`template.tsx\`\uC5EC\uC57C \uD558\uBA70, \`src/pages/\`\xB7\`src/slots/\` \uBC16\uC5D0 \uB450\uBA74 \uBE4C\uB4DC\uAC00 \uC778\uC2DD\uD558\uC9C0 \uC54A\uB294\uB2E4.
725
+
726
+ - **\uD398\uC774\uC9C0** \u2014 \`src/pages/{\uACBD\uB85C}/template.tsx\`. **\uD3F4\uB354 \uAD6C\uC870\uAC00 \uACE7 \uBC30\uD3EC \uACBD\uB85C**\uB2E4(\`src/pages/settings/custom/\` \u2192 \`/settings/custom\`). \uB3D9\uC801 \uACBD\uB85C(\`[id]\`)\uB294 \uC544\uC9C1 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294\uB2E4.
727
+ - **\uC704\uC82F** \u2014 \`src/slots/{\uC544\uBB34 \uC774\uB984}/template.tsx\` + \`createTemplate\`\uC5D0 \`slot\` \uC635\uC158. \uD3F4\uB354 \uC774\uB984\uC740 \uBC30\uCE58\uC5D0 \uC4F0\uC774\uC9C0 \uC54A\uACE0, \uC5B4\uB290 \uC790\uB9AC\uC5D0 \uBD99\uC744\uC9C0\uB294 \`slot\`\uB9CC \uC815\uD55C\uB2E4.
728
+
729
+ \`name\`\uC740 Custom Element \uD0DC\uADF8\uB85C \uADF8\uB300\uB85C \uC0AC\uC6A9\uB41C\uB2E4. Custom Element \uC2A4\uD399\uC0C1 **\uC18C\uBB38\uC790\uB85C \uC2DC\uC791 + \uD558\uC774\uD508 1\uAC1C \uC774\uC0C1 + \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC** \uD5C8\uC6A9. \uC704\uBC18 \uC2DC \uB7F0\uD0C0\uC784\xB7\uBE4C\uB4DC\uAC00 \uAC70\uBD80\uD55C\uB2E4.
730
+
731
+ \`\`\`
732
+ src/pages/welcome/template.tsx \u2192 /welcome
733
+ src/pages/settings/custom/template.tsx \u2192 /settings/custom
734
+ src/slots/curation-banner/template.tsx \u2192 slot \uC635\uC158\uC774 \uC790\uB9AC\uB97C \uC815\uD568
735
+ \`\`\`
736
+
737
+ ### template.tsx \uD544\uC218 \uAD6C\uC870
738
+
739
+ \`\`\`tsx
740
+ import { createTemplate } from '@bstage-sdk/react'
741
+
742
+ // \uCEF4\uD3EC\uB10C\uD2B8\uB294 \uBC18\uB4DC\uC2DC export default
743
+ export default function MyWidget() {
744
+ return <div>...</div>
745
+ }
746
+
747
+ // createTemplate \uD638\uCD9C \uD544\uC218 \u2014 \uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778\uC774 \uC774 \uD638\uCD9C\uC744 \uD30C\uC2F1\uD558\uC5EC \uBA54\uD0C0\uB370\uC774\uD130 \uCD94\uCD9C
748
+ createTemplate(MyWidget, {
749
+ name: '${space}-my-widget', // \uD3F4\uB354\uBA85\uACFC \uB3D9\uC77C\uD574\uC57C \uD568. \uD558\uC774\uD508 \uD544\uC218.
750
+ })
751
+ \`\`\`
752
+
753
+ - \`name\`\uC740 \uD544\uC218 \u2014 \uD15C\uD50C\uB9BF \uD3F4\uB354\uBA85\uACFC \uC77C\uCE58, \uD558\uC774\uD508 \uD3EC\uD568
754
+ - \uCEF4\uD3EC\uB10C\uD2B8 \uD568\uC218\uBA85\uC740 \uC790\uC720\uC774\uB098 \`export default\` \uD544\uC218
755
+ - \`type\` \uB4F1 \uC120\uD0DD \uD544\uB4DC \uCD94\uAC00 \uAC00\uB2A5
756
+
757
+ ${designRulesBrief(target)}
758
+
759
+ ## \uD658\uACBD \uC124\uC815
760
+
761
+ ${envSection}
762
+
763
+ ## \uBA85\uB839\uC5B4
764
+
765
+ ${commandsSection}
766
+
767
+ ## SDK \uC5C5\uB370\uC774\uD2B8 / \uB9C8\uC774\uADF8\uB808\uC774\uC158
768
+
769
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 \uC0DD\uC131 \uC2DC\uC810\uC758 SDK \uBC84\uC804\uC744 \`package.json\`\uC5D0 \uACE0\uC815\uD55C\uB2E4. SDK\uAC00 \uC5C5\uB370\uC774\uD2B8\uB3FC\uB3C4 \uC790\uB3D9\uC73C\uB85C \uB530\uB77C\uC624\uC9C0 \uC54A\uC73C\uBBC0\uB85C, \uCD5C\uC2E0\uC73C\uB85C \uC62C\uB9AC\uB824\uBA74:
770
+
771
+ - \`npx @bstage-sdk/cli@latest doctor\` \u2014 \uBC84\uC804 \uB4DC\uB9AC\uD504\uD2B8\xB7\uB204\uB77D \uD30C\uC77C\xB7\uC801\uC6A9 \uAC00\uB2A5\uD55C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD56D\uBAA9\uC744 \uC9C4\uB2E8\uD55C\uB2E4(\uD30C\uC77C \uC218\uC815 \uC5C6\uC74C, \`--json\` \uC9C0\uC6D0).
772
+ - \uC2E4\uC81C \uC801\uC6A9\uC740 \`bstage-migrate\` \uC2A4\uD0AC\uC5D0\uAC8C \uB9E1\uAE34\uB2E4 \u2014 Claude Code\uC5D0\uC11C "bstage \uCD5C\uC2E0 \uBC84\uC804\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD574\uC918"\uB77C\uACE0 \uC694\uCCAD\uD558\uBA74, doctor \uACB0\uACFC\uC640 SDK\uC758 \`MIGRATION.md\`\uB97C \uC77D\uACE0 \uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uCDB0 \uBCC0\uD658\uD55C\uB2E4(\uC0AC\uC6A9\uC790 \uCF54\uB4DC\uB294 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uACE0 reconcile).
773
+
774
+ \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC808\uCC28\uC758 \uB2E8\uC77C \uC18C\uC2A4\uB294 SDK\uC758 \`node_modules/@bstage-sdk/core/docs/MIGRATION.md\`\uB2E4.
775
+
776
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \`.claude/skills/\`\uC5D0\uB294 \uC5D0\uC774\uC804\uD2B8\uC6A9 \uC2A4\uD0AC(\`bstage-template\` \uC791\uC131 \uAC00\uC774\uB4DC, \`bstage-migrate\` \uB9C8\uC774\uADF8\uB808\uC774\uC158)\uC774 \uB4E4\uC5B4 \uC788\uB2E4. SDK \uBC84\uC804\uC5C5 \uD6C4 \`npx @bstage-sdk/cli@latest skills install\`\uB85C \uCD5C\uC2E0 \uC2A4\uD0AC\uC744 \uB3D9\uAE30\uD654\uD560 \uC218 \uC788\uC73C\uBA70, \uC774\uB54C **\uC774 AGENTS.md\uC758 SDK \uAD00\uB9AC \uC601\uC5ED(\uB9C8\uCEE4\uB85C \uAC10\uC2FC \uBD80\uBD84)\uB3C4 \uD568\uAED8 \uCD5C\uC2E0\uD654**\uB41C\uB2E4. \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uADDC\uCE59\uC740 \uAD00\uB9AC \uC601\uC5ED \uC544\uB798 **\uC790\uC720 \uC601\uC5ED**\uC5D0 \uC801\uC73C\uBA74 \uAC31\uC2E0 \uC2DC \uBCF4\uC874\uB41C\uB2E4.
777
+
778
+ ## \uBE4C\uB4DC & \uBC30\uD3EC
779
+
780
+ - \uBC30\uD3EC\uB294 \uAD00\uB9AC\uB3C4\uAD6C\uAC00 \uC774 \uB808\uD3EC\uC758 GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uD2B8\uB9AC\uAC70\uD558\uC5EC \uC218\uD589\uD55C\uB2E4.
781
+ - \uB85C\uCEEC\uC5D0\uC11C \uC0B0\uCD9C\uBB3C \uD655\uC778\uC774 \uD544\uC694\uD558\uBA74 \`npx bstage build\`\uB85C \`dist/\` \uB97C \uC0DD\uC131\uD560 \uC218 \uC788\uB2E4.
782
+
783
+ ## \uAE08\uC9C0 \uC0AC\uD56D
784
+
785
+ - \`src/main.tsx\`\xB7\`src/App.tsx\`\uB97C \uC218\uC815\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uAC1C\uBC1C\uC6A9 \uC9C4\uC785\uC810\xB7\uBAA9\uB85D \uD654\uBA74\uC73C\uB85C SDK\uAC00 \uAD00\uB9AC
786
+ - \`customElements.define()\`\uC744 \uC9C1\uC811 \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \`createTemplate()\`\uC774 \uC790\uB3D9 \uCC98\uB9AC
787
+ - Shadow DOM\uC744 \uC9C1\uC811 \uC870\uC791\uD558\uC9C0 \uC54A\uB294\uB2E4 (\`attachShadow\`, \`shadowRoot\` \uB4F1) \u2014 SDK\uAC00 \uAD00\uB9AC
788
+ - \`createTemplate()\` \uD638\uCD9C\uC758 \uC778\uC790 \uAD6C\uC870\uB97C \uC784\uC758\uB85C \uBCC0\uACBD\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778\uC774 \uD30C\uC2F1\uC5D0 \uC2E4\uD328\uD560 \uC218 \uC788\uB2E4
789
+ - \`@bstage-sdk/core\`\uB97C \uC9C1\uC811 import\uD558\uC5EC \`createWebComponent()\`\uB97C \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \`@bstage-sdk/react\`\uC758 API\uB9CC \uC0AC\uC6A9
790
+ - \uC804\uC5ED CSS \uD30C\uC77C(\`<link>\`, \uC678\uBD80 \uC2A4\uD0C0\uC77C\uC2DC\uD2B8)\uC744 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 Shadow DOM \uB0B4\uBD80\uC5D0 \uC801\uC6A9\uB418\uC9C0 \uC54A\uB294\uB2E4.
791
+ - **\`import './style.css'\`\uB85C \uC2A4\uD0C0\uC77C\uC744 \uB123\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uBC30\uD3EC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC0AC\uB77C\uC9C4\uB2E4.** \uB85C\uCEEC \uAC1C\uBC1C \uD654\uBA74\uC5D0\uC11C\uB294 \uC801\uC6A9\uB418\uC5B4 \uB208\uCE58\uCC44\uAE30 \uC5B4\uB835\uB2E4. CSS \uD30C\uC77C\uC744 \uC4F0\uB824\uBA74 \`import css from './style.css?inline'\`\uC73C\uB85C \uBB38\uC790\uC5F4\uC744 \uAC00\uC838\uC640 \`createTemplate(..., { styles: css })\`\uC5D0 \uB118\uAE30\uAC70\uB098 \uCEF4\uD3EC\uB10C\uD2B8 \uC548 \`<style>{css}</style>\`\uB85C \uB80C\uB354\uD55C\uB2E4. inline \`style={{}}\`\uB3C4 \uB41C\uB2E4
792
+ - UI \uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790\uB97C hex/rgb\uB85C \uD558\uB4DC\uCF54\uB529\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC704 '\uB514\uC790\uC778' \uC139\uC158\uC758 \uB514\uC790\uC778 \uD1A0\uD070\uC744 \uC0AC\uC6A9
793
+ - \uC571\uD0A4(APP-ID \`bsa_\u2026\`\xB7Secret \`bsp_\u2026\`, \uC5B4\uB4DC\uBBFC \uAC8C\uC774\uD2B8\uC6E8\uC774 \`bsm_\u2026\`)\uB97C \uC18C\uC2A4 \uCF54\uB4DC\uC5D0 \uB9AC\uD130\uB7F4\uB85C \uB123\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC778\uC99D \uAC12\uC740 \`.env\`(\uCEE4\uBC0B \uC548 \uB428)\uC5D0\uC11C \`import.meta.env.VITE_BSTAGE_*\`\uB85C\uB9CC \uC8FC\uC785\uD55C\uB2E4. pre-commit \uD6C5\uC774 \uCEE4\uBC0B \uC804 \uAC80\uCD9C\xB7\uCC28\uB2E8\uD558\uBA70 \`--no-verify\`\uB85C \uC6B0\uD68C\uD558\uC9C0 \uC54A\uB294\uB2E4
794
+ ${apiForbidBullet}
795
+ `;
796
+ }
797
+ function renderManagedBlock(opts) {
798
+ return wrapManaged(agentsManagedBody(opts));
799
+ }
800
+ function agentsMd(opts) {
801
+ return `${renderManagedBlock(opts)}
802
+
803
+ ${freeRegionScaffold()}`;
804
+ }
805
+
806
+ // src/templates/claudeMd.ts
807
+ function claudeMd() {
808
+ return `# CLAUDE.md
809
+
810
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uADDC\uCE59\uACFC \uCEE8\uD14D\uC2A4\uD2B8\uB294 [AGENTS.md](./AGENTS.md)\uB97C \uCC38\uACE0\uD558\uC138\uC694.
811
+ `;
812
+ }
813
+
814
+ // src/templates/env.ts
815
+ function env({ phase, appId, appSecret, tenantId }) {
816
+ return `VITE_BSTAGE_PHASE=${phase}
817
+
818
+ # \uC778\uC99D \uC815\uBCF4(\uC120\uD0DD) \u2014 BstageClient\uB85C API\uB97C \uD638\uCD9C\uD560 \uB54C\uB9CC \uD544\uC694\uD569\uB2C8\uB2E4.
819
+ # API \uC5C6\uC774 UI\uB9CC \uB80C\uB354\uD558\uB294 \uD15C\uD50C\uB9BF\uC740 \uAC12\uC744 \uCC44\uC6B0\uC9C0 \uC54A\uC544\uB3C4 \uB429\uB2C8\uB2E4.
820
+ # .env\uB294 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(.gitignore). \uAC12\uC740 b.stage \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09.
821
+ VITE_BSTAGE_APP_ID=${appId}
822
+ VITE_BSTAGE_APP_SECRET=${appSecret}
823
+ VITE_BSTAGE_TENANT_ID=${tenantId}
824
+
825
+ # CF Access \uC790\uACA9\uC99D\uBA85 \u2014 dev\xB7qa phase\uB85C \uAC1C\uBC1C\uD560 \uB54C\uB9CC \uD544\uC694\uD569\uB2C8\uB2E4.
826
+ # dev\xB7qa\uB294 \uC0AC\uB0B4 \uC804\uC6A9 \uD658\uACBD\uC774\uB77C Cloudflare Access \uAC8C\uC774\uD2B8 \uB4A4\uC5D0 \uC788\uACE0, \uAC12\uC774 \uC5C6\uC73C\uBA74
827
+ # \uB85C\uADF8\uC778\xB7API \uD504\uB85D\uC2DC\uAC00 302\uB85C \uB9C9\uD799\uB2C8\uB2E4. \uC0AC\uB0B4\uC5D0\uC11C \uC4F4\uB2E4\uBA74 CF Access \uC11C\uBE44\uC2A4 \uD1A0\uD070\uC744 \uBC1C\uAE09\uBC1B\uC544 \uCC44\uC6B0\uC138\uC694.
828
+ # sandbox\xB7real\uC740 \uAC8C\uC774\uD2B8\uAC00 \uC5C6\uC5B4 \uC124\uC815\uC774 \uD544\uC694 \uC5C6\uC2B5\uB2C8\uB2E4.
829
+ VITE_CF_ACCESS_CLIENT_ID=
830
+ VITE_CF_ACCESS_CLIENT_SECRET=
831
+
832
+ # \uC0AC\uB0B4 \uC804\uC6A9 phase\uC758 \uD638\uC2A4\uD2B8 \u2014 dev\xB7qa phase\uB85C \uAC1C\uBC1C\uD560 \uB54C\uB9CC \uD544\uC694\uD569\uB2C8\uB2E4.
833
+ # \uC774 \uD638\uC2A4\uD2B8\uB4E4\uC740 \uC0AC\uB0B4 \uD658\uACBD\uC774\uB77C SDK\uC5D0 \uB4E4\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uAC12\uC774 \uC5C6\uC73C\uBA74 dev \uC11C\uBC84\uAC00
834
+ # \uB728\uC9C0 \uC54A\uACE0 \uC5B4\uB5A4 \uAC12\uC774 \uBE44\uC5C8\uB294\uC9C0 \uC54C\uB824\uC90D\uB2C8\uB2E4. \uAC12\uC740 \uC0AC\uB0B4 \uBB38\uC11C\uB97C \uCC38\uACE0\uD558\uC138\uC694.
835
+ # sandbox\xB7real\uC740 \uD638\uC2A4\uD2B8\uAC00 SDK\uC5D0 \uC788\uC5B4 \uC124\uC815\uC774 \uD544\uC694 \uC5C6\uC2B5\uB2C8\uB2E4.
836
+ VITE_BSTAGE_GATEWAY_HOST=
837
+ VITE_BSTAGE_AUTH_HOST=
838
+ `;
839
+ }
840
+
841
+ // src/templates/packageJson.ts
842
+ function packageJson({
843
+ projectName,
844
+ target = "user",
845
+ sdkVersions
846
+ }) {
847
+ const dependencies = {
848
+ "@bstage-sdk/cli": sdkVersions.cli,
849
+ "@bstage-sdk/core": sdkVersions.core,
850
+ "@bstage-sdk/design": sdkVersions.design,
851
+ "@bstage-sdk/react": sdkVersions.react,
852
+ react: "^19.2.0",
853
+ "react-dom": "^19.2.0"
854
+ };
855
+ const devDependencies = {
856
+ "@eslint/js": "^9.39.0",
857
+ // tsconfig.node.json이 `types: ['node']`를 선언하고 vite.config.ts가 `process.cwd()`를 쓴다.
858
+ // 이게 없으면 `npm run typecheck`(tsc -b)가 TS2688로 깨진다.
859
+ "@types/node": "^24.0.0",
860
+ "@types/react": "^19.2.0",
861
+ "@types/react-dom": "^19.2.0",
862
+ "@vitejs/plugin-react": "^5.2.0",
863
+ eslint: "^9.39.0",
864
+ "eslint-plugin-react-hooks": "^7.0.0",
865
+ "eslint-plugin-react-refresh": "^0.4.0",
866
+ globals: "^16.0.0",
867
+ husky: "^9.1.7",
868
+ typescript: "~5.9.0",
869
+ "typescript-eslint": "^8.0.0",
870
+ vite: "^7.0.0"
871
+ };
872
+ return JSON.stringify(
873
+ {
874
+ name: projectName,
875
+ private: true,
876
+ version: "0.0.0",
877
+ type: "module",
878
+ bstage: { target },
879
+ scripts: {
880
+ dev: "vite dev",
881
+ build: "bstage build",
882
+ // `bstage build`는 tsc를 타지 않는다 — 타입 게이트는 이 스크립트가 유일하다.
883
+ // solution-style tsconfig(files: [])라 `tsc --noEmit`은 아무것도 검사하지 않으므로 `-b`를 쓴다.
884
+ typecheck: "tsc -b",
885
+ lint: "eslint .",
886
+ // install 시 husky가 .husky/ 훅(pre-commit 시크릿 가드)을 활성화한다.
887
+ prepare: "husky"
888
+ },
889
+ dependencies,
890
+ devDependencies
891
+ },
892
+ null,
893
+ 2
894
+ );
895
+ }
896
+
897
+ // src/templates/readme.ts
898
+ function readme({ projectName, packageManager }) {
899
+ const runCmd = packageManager === "pnpm" ? "pnpm" : "npm run";
900
+ return `# ${projectName}
901
+
902
+ b.stage \uC11C\uB4DC\uD30C\uD2F0 \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8
903
+
904
+ ## \uC2DC\uC791\uD558\uAE30
905
+
906
+ \`\`\`bash
907
+ ${runCmd} dev
908
+ \`\`\`
909
+
910
+ \uAC1C\uBC1C \uC11C\uBC84\uAC00 \uC2E4\uD589\uB418\uBA74 http://localhost:5173 \uC5D0\uC11C \uD15C\uD50C\uB9BF \uBAA9\uB85D\uC744 \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
911
+
912
+ ## \uD15C\uD50C\uB9BF \uCD94\uAC00
913
+
914
+ \uD398\uC774\uC9C0\uB0D0 \uC704\uC82F\uC774\uB0D0\uC5D0 \uB530\uB77C \uB450\uB294 \uACF3\uC774 \uB2E4\uB985\uB2C8\uB2E4. \uD30C\uC77C\uBA85\uC740 \uBC18\uB4DC\uC2DC \`template.tsx\`\uC5EC\uC57C \uD569\uB2C8\uB2E4.
915
+
916
+ - **\uD398\uC774\uC9C0** \u2014 \`src/pages/{\uACBD\uB85C}/template.tsx\`. \uD3F4\uB354 \uAD6C\uC870\uAC00 \uACE7 \uBC30\uD3EC \uACBD\uB85C\uAC00 \uB429\uB2C8\uB2E4.
917
+ - **\uC704\uC82F** \u2014 \`src/slots/{\uC544\uBB34 \uC774\uB984}/template.tsx\`. \uC5B4\uB290 \uC790\uB9AC\uC5D0 \uBD99\uC744\uC9C0\uB294 \`createTemplate\`\uC758 \`slot\` \uC635\uC158\uC774 \uC815\uD569\uB2C8\uB2E4.
918
+
919
+ \`name\`\uC740 Custom Element \uD0DC\uADF8\uB85C \uADF8\uB300\uB85C \uC0AC\uC6A9\uB418\uBBC0\uB85C **\uC18C\uBB38\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uD558\uC774\uD508\uC744 1\uAC1C \uC774\uC0C1 \uD3EC\uD568**\uD574\uC57C \uD569\uB2C8\uB2E4. \uC608: \`my-widget\`, \`bmf-hello\`.
920
+
921
+ \`\`\`
922
+ src/pages/my-page/template.tsx \u2192 /my-page
923
+ src/slots/my-widget/template.tsx \u2192 slot \uC635\uC158\uC774 \uC790\uB9AC\uB97C \uC815\uD568
924
+ \`\`\`
925
+
926
+ \`\`\`tsx
927
+ import { createTemplate } from '@bstage-sdk/react'
928
+
929
+ export default function MyWidget() {
930
+ return <div>Hello!</div>
931
+ }
932
+
933
+ createTemplate(MyWidget, {
934
+ name: 'my-widget',
935
+ })
936
+ \`\`\`
937
+
938
+ ## \uD658\uACBD \uC124\uC815
939
+
940
+ \`.env\` \uD30C\uC77C\uC5D0\uC11C API \uD658\uACBD\uC744 \uBCC0\uACBD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
941
+
942
+ \`\`\`
943
+ VITE_BSTAGE_PHASE=dev # dev | qa | real | sandbox
944
+ \`\`\`
945
+
946
+ API \uD0A4(APP-ID/Secret)\uB294 \`.env\`\uC5D0\uC11C \uC124\uC815\uD569\uB2C8\uB2E4. \`.env\`\uB294 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC73C\uB2C8, clone \uD6C4 \`.env.example\`\uC744 \`.env\`\uB85C \uBCF5\uC0AC\uD574 \uAC12\uC744 \uCC44\uC6B0\uC138\uC694.
947
+
948
+ > **\uBCF4\uC548**: \uC571\uD0A4 \uAC12\uC744 \uC18C\uC2A4 \uCF54\uB4DC(\uC608: \`client.ts\`)\uC5D0 \uC9C1\uC811 \uB123\uC9C0 \uB9C8\uC138\uC694 \u2014 \uBC18\uB4DC\uC2DC \`.env\`\uB85C\uB9CC \uC8FC\uC785\uD569\uB2C8\uB2E4. \`bstage init\`\uC774 \uC124\uCE58\uD55C pre-commit \uD6C5\uC774 \uCEE4\uBC0B \uC804 \uD0A4 \uB9AC\uD130\uB7F4\uC744 \uCC28\uB2E8\uD558\uBA70, \`--no-verify\`\uB85C \uC6B0\uD68C\uD558\uC9C0 \uB9C8\uC138\uC694.
949
+
950
+ ## SDK \uC5C5\uB370\uC774\uD2B8
951
+
952
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 \uC0DD\uC131 \uC2DC\uC810\uC758 SDK \uBC84\uC804\uC5D0 \uACE0\uC815\uB429\uB2C8\uB2E4. \uCD5C\uC2E0\uC73C\uB85C \uC62C\uB9AC\uB824\uBA74 \`npx @bstage-sdk/cli@latest doctor\`\uB85C \uC9C4\uB2E8\uD55C \uB4A4, Claude Code\uC5D0 "bstage \uCD5C\uC2E0 \uBC84\uC804\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD574\uC918"\uB77C\uACE0 \uC694\uCCAD\uD558\uBA74 \`bstage-migrate\` \uC2A4\uD0AC\uC774 \uCC98\uB9AC\uD569\uB2C8\uB2E4.
953
+
954
+ ## \uBC30\uD3EC
955
+
956
+ \uBC30\uD3EC\uB294 \uAD00\uB9AC\uB3C4\uAD6C\uC5D0\uC11C \uC774 \uB808\uD3EC\uC758 GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uD2B8\uB9AC\uAC70\uD574 \uC9C4\uD589\uB429\uB2C8\uB2E4. \uAC1C\uBC1C\uC790\uB294 \uCF54\uB4DC \uC791\uC131\xB7push\uC5D0 \uC9D1\uC911\uD558\uBA74 \uB418\uACE0, \uBC30\uD3EC \uD2B8\uB9AC\uAC70\uB294 \uAD00\uB9AC\uB3C4\uAD6C\uC5D0\uC11C \uC218\uD589\uD569\uB2C8\uB2E4.
957
+ `;
958
+ }
959
+
960
+ // src/templates/tsconfig.ts
961
+ function tsconfig() {
962
+ return JSON.stringify(
963
+ {
964
+ files: [],
965
+ references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }]
966
+ },
967
+ null,
968
+ 2
969
+ );
970
+ }
971
+ function tsconfigApp() {
972
+ return JSON.stringify(
973
+ {
974
+ compilerOptions: {
975
+ tsBuildInfoFile: "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
976
+ target: "ES2022",
977
+ useDefineForClassFields: true,
978
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
979
+ module: "ESNext",
980
+ types: ["vite/client"],
981
+ skipLibCheck: true,
982
+ moduleResolution: "bundler",
983
+ allowImportingTsExtensions: true,
984
+ verbatimModuleSyntax: true,
985
+ moduleDetection: "force",
986
+ noEmit: true,
987
+ jsx: "react-jsx",
988
+ strict: true,
989
+ noUnusedLocals: true,
990
+ noUnusedParameters: true,
991
+ erasableSyntaxOnly: true,
992
+ noFallthroughCasesInSwitch: true,
993
+ noUncheckedSideEffectImports: true
994
+ },
995
+ include: ["src"]
996
+ },
997
+ null,
998
+ 2
999
+ );
1000
+ }
1001
+ function tsconfigNode() {
1002
+ return JSON.stringify(
1003
+ {
1004
+ compilerOptions: {
1005
+ tsBuildInfoFile: "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
1006
+ target: "ES2023",
1007
+ lib: ["ES2023"],
1008
+ module: "ESNext",
1009
+ types: ["node"],
1010
+ skipLibCheck: true,
1011
+ moduleResolution: "bundler",
1012
+ allowImportingTsExtensions: true,
1013
+ verbatimModuleSyntax: true,
1014
+ moduleDetection: "force",
1015
+ noEmit: true,
1016
+ strict: true,
1017
+ noUnusedLocals: true,
1018
+ noUnusedParameters: true,
1019
+ erasableSyntaxOnly: true,
1020
+ noFallthroughCasesInSwitch: true,
1021
+ noUncheckedSideEffectImports: true
1022
+ },
1023
+ include: ["vite.config.ts"]
1024
+ },
1025
+ null,
1026
+ 2
1027
+ );
1028
+ }
1029
+
1030
+ // src/templates/viteConfig.ts
1031
+ function viteConfig() {
1032
+ return `import { defineConfig, loadEnv } from 'vite'
1033
+ import react from '@vitejs/plugin-react'
1034
+ import { bstageDevPlugin } from '@bstage-sdk/cli/vite'
1035
+
1036
+ // https://vite.dev/config/
1037
+ export default defineConfig(({ mode }) => {
1038
+ const env = loadEnv(mode, process.cwd(), '')
1039
+
1040
+ return {
1041
+ plugins: [react(), bstageDevPlugin({ phase: env.VITE_BSTAGE_PHASE })],
1042
+ }
1043
+ })
1044
+ `;
1045
+ }
1046
+
1047
+ // src/templates/indexHtml.ts
1048
+ function indexHtml() {
1049
+ return `<!DOCTYPE html>
1050
+ <html lang="en">
1051
+ <head>
1052
+ <meta charset="UTF-8" />
1053
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1054
+ <title>Dev</title>
1055
+ <style>body { margin: 0; }</style>
1056
+ </head>
1057
+ <body>
1058
+ <div id="root"></div>
1059
+ <script type="module" src="/src/main.tsx"></script>
1060
+ </body>
1061
+ </html>
1062
+ `;
1063
+ }
1064
+
1065
+ // src/templates/pnpmWorkspace.ts
1066
+ function pnpmWorkspace() {
1067
+ return `# pnpm 9\uB294 \uC774 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 workspace \uB8E8\uD2B8\uB85C \uBCF4\uACE0, packages\uAC00 \uC5C6\uC73C\uBA74 install\uC774 \uC8FD\uB294\uB2E4.
1068
+ # \uB2E8\uC77C \uD504\uB85C\uC81D\uD2B8\uC774\uBBC0\uB85C \uB8E8\uD2B8\uB9CC \uC120\uC5B8\uD55C\uB2E4.
1069
+ packages:
1070
+ - '.'
1071
+
1072
+ # pnpm\uC740 \uC758\uC874\uC131 build script\uB97C \uAE30\uBCF8 \uCC28\uB2E8\uD558\uACE0, \uCC28\uB2E8 \uC2DC install\uC774 \uC5D0\uB7EC\uB85C \uB05D\uB09C\uB2E4.
1073
+ # \uC544\uB798\uC5D0\uC11C \uD5C8\uC6A9\uD560 \uD328\uD0A4\uC9C0\uB97C \uBA85\uC2DC\uD55C\uB2E4. \uC0C8 \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD574\uC9C0\uBA74 \`pnpm approve-builds\`\uAC00
1074
+ # \uC774 \uD30C\uC77C\uC5D0 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD574 \uC900\uB2E4(\uAC12\uC744 true\uB85C \uBC14\uAFD4\uC57C \uC801\uC6A9\uB41C\uB2E4).
1075
+ allowBuilds:
1076
+ # vite\uAC00 \uC4F0\uB294 \uBC88\uB4E4\uB7EC. postinstall\uC774 \uD50C\uB7AB\uD3FC \uBC14\uC774\uB108\uB9AC\uB97C \uB193\uB294\uB2E4.
1077
+ esbuild: true
1078
+ `;
1079
+ }
1080
+
1081
+ // src/templates/gitignore.ts
1082
+ function gitignore() {
1083
+ return `# Logs
1084
+ logs
1085
+ *.log
1086
+ npm-debug.log*
1087
+ yarn-debug.log*
1088
+ yarn-error.log*
1089
+ pnpm-debug.log*
1090
+ lerna-debug.log*
1091
+
1092
+ node_modules
1093
+ dist
1094
+ dist-ssr
1095
+ *.local
1096
+
1097
+ # \uD658\uACBD\uBCC0\uC218 (\uC778\uC99D \uC815\uBCF4 \uD3EC\uD568 \u2014 \uCEE4\uBC0B \uAE08\uC9C0). .env.example\uB9CC \uCEE4\uBC0B\uD55C\uB2E4.
1098
+ # .env\uB9CC \uBB34\uC2DC\uD55C\uB2E4 \u2014 .env.local\uC740 \uC704 *.local\uC774 \uCC98\uB9AC\uD558\uACE0, .env.development \uB4F1 Vite \uBAA8\uB4DC
1099
+ # \uD30C\uC77C\uC740 \uBB34\uC2DC\uD558\uC9C0 \uC54A\uB294\uB2E4.
1100
+ .env
1101
+
1102
+ # bstage \uB85C\uCEEC \uCE90\uC2DC (\uBC88\uC5ED \uB4F1 \u2014 bstage i18n pull\uB85C \uC7AC\uC0DD\uC131)
1103
+ .bstage
1104
+
1105
+ # Editor directories and files
1106
+ .vscode/*
1107
+ !.vscode/extensions.json
1108
+ .idea
1109
+ .DS_Store
1110
+ *.suo
1111
+ *.ntvs*
1112
+ *.njsproj
1113
+ *.sln
1114
+ *.sw?
1115
+ `;
1116
+ }
1117
+
1118
+ // src/templates/huskyPreCommit.ts
1119
+ function huskyPreCommit() {
1120
+ return `node .husky/check-secrets.mjs
1121
+ `;
1122
+ }
1123
+
1124
+ // src/templates/checkSecretsScript.ts
1125
+ function checkSecretsScript() {
1126
+ return `/* eslint-disable */
1127
+ // bstage \uC2DC\uD06C\uB9BF \uAC00\uB4DC \u2014 SDK\uAC00 \uAD00\uB9AC\uD558\uB294 \uD30C\uC77C\uC785\uB2C8\uB2E4. \uC9C1\uC811 \uD3B8\uC9D1\uD558\uC9C0 \uB9C8\uC138\uC694(bstage skills install\uC774 \uAC31\uC2E0).
1128
+ // \uC2A4\uD14C\uC774\uC9C0\uB41C \uBCC0\uACBD\uC5D0\uC11C \uC571\uD0A4(bsa_/bsm_/bsp_)\xB7GitHub PAT \uB9AC\uD130\uB7F4\uC744 \uAC80\uCD9C\uD574 \uCEE4\uBC0B\uC744 \uCC28\uB2E8\uD55C\uB2E4.
1129
+ import { execFileSync } from 'node:child_process'
1130
+ import { basename } from 'node:path'
1131
+
1132
+ const LF = String.fromCharCode(10)
1133
+ const NUL = String.fromCharCode(0)
1134
+ const PATTERNS = [
1135
+ { label: '\uC571\uD0A4', re: /bs[amp]_[A-Za-z0-9]{16,}/g },
1136
+ { label: 'GitHub PAT', re: /ghp_[A-Za-z0-9]{36}/g },
1137
+ { label: 'GitHub PAT', re: /github_pat_[A-Za-z0-9_]{82}/g },
1138
+ ]
1139
+ const HINTS = ['YOUR', 'EXAMPLE', 'PLACEHOLDER', 'XXXX']
1140
+
1141
+ function isPlaceholder(token, line) {
1142
+ const upper = line.toUpperCase()
1143
+ if (HINTS.some((h) => upper.includes(h))) return true
1144
+ const body = token.slice(token.indexOf('_') + 1)
1145
+ return body.length > 0 && body.split('').every((c) => c === body[0])
1146
+ }
1147
+ function mask(token) {
1148
+ const us = token.indexOf('_')
1149
+ const prefix = us >= 0 ? token.slice(0, us + 1) : token.slice(0, 4)
1150
+ return prefix + '****'
1151
+ }
1152
+ function git(args) {
1153
+ return execFileSync('git', args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] })
1154
+ }
1155
+
1156
+ let out
1157
+ try {
1158
+ out = git(['-c', 'core.quotepath=false', 'diff', '--cached', '--name-only', '--diff-filter=ACM'])
1159
+ } catch {
1160
+ process.exit(0)
1161
+ }
1162
+ const files = out
1163
+ .split(LF)
1164
+ .map((s) => s.trim())
1165
+ .filter(Boolean)
1166
+ const findings = []
1167
+
1168
+ for (const f of files) {
1169
+ const base = basename(f)
1170
+ if (base === '.env' || base.endsWith('.local')) {
1171
+ findings.push({ file: f, line: 0, label: '.env \uD30C\uC77C', masked: base })
1172
+ }
1173
+ }
1174
+ for (const f of files) {
1175
+ if (basename(f) === '.env.example') continue
1176
+ let content
1177
+ try {
1178
+ content = git(['show', ':' + f])
1179
+ } catch {
1180
+ continue
1181
+ }
1182
+ if (content.includes(NUL)) continue
1183
+ const lines = content.split(LF)
1184
+ for (let i = 0; i < lines.length; i++) {
1185
+ const line = lines[i]
1186
+ for (const p of PATTERNS) {
1187
+ p.re.lastIndex = 0
1188
+ let m
1189
+ while ((m = p.re.exec(line)) !== null) {
1190
+ const token = m[0]
1191
+ if (p.label === '\uC571\uD0A4' && isPlaceholder(token, line)) continue
1192
+ findings.push({ file: f, line: i + 1, label: p.label, masked: mask(token) })
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ if (findings.length === 0) process.exit(0)
1199
+
1200
+ console.error('')
1201
+ console.error('\u2717 \uCEE4\uBC0B \uCC28\uB2E8: \uC2A4\uD14C\uC774\uC9C0\uB41C \uBCC0\uACBD\uC5D0 \uD0A4/\uC2DC\uD06C\uB9BF \uB9AC\uD130\uB7F4\uC774 \uC788\uC2B5\uB2C8\uB2E4.')
1202
+ for (const x of findings) {
1203
+ if (x.line === 0) {
1204
+ console.error(' ' + x.file + ' \u2014 .env\uB958 \uD30C\uC77C\uC740 \uCEE4\uBC0B\uD558\uC9C0 \uB9C8\uC138\uC694 (git add -f\uB85C \uAC15\uC81C\uB428?)')
1205
+ } else {
1206
+ console.error(' ' + x.file + ':' + x.line + ' \u2014 ' + x.label + ' \uB9AC\uD130\uB7F4 (' + x.masked + ')')
1207
+ }
1208
+ }
1209
+ console.error('')
1210
+ console.error(' \uC778\uC99D \uAC12\uC740 .env(\uCEE4\uBC0B \uC548 \uB428) + import.meta.env\uB85C \uC8FC\uC785\uD558\uC138\uC694. \uB178\uCD9C\uB41C \uD0A4\uB294 \uD3D0\uAE30\xB7\uC7AC\uBC1C\uAE09\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.')
1211
+ console.error('')
1212
+ process.exit(1)
1213
+ `;
1214
+ }
1215
+
1216
+ // src/templates/eslintConfig.ts
1217
+ function eslintConfig() {
1218
+ return `import js from '@eslint/js'
1219
+ import globals from 'globals'
1220
+ import reactHooks from 'eslint-plugin-react-hooks'
1221
+ import reactRefresh from 'eslint-plugin-react-refresh'
1222
+ import tseslint from 'typescript-eslint'
1223
+ import { defineConfig, globalIgnores } from 'eslint/config'
1224
+
1225
+ export default defineConfig([
1226
+ // .husky\uB294 SDK\uAC00 \uC0DD\uC131\xB7\uAC31\uC2E0\uD558\uB294 \uB3C4\uAD6C \uC2A4\uD06C\uB9BD\uD2B8\uB77C \uD504\uB85C\uC81D\uD2B8 \uC18C\uC2A4\uAC00 \uC544\uB2C8\uB2E4.
1227
+ // \uBE7C\uB450\uC9C0 \uC54A\uC73C\uBA74 check-secrets.mjs\uC758 \uD30C\uC77C \uC0C1\uB2E8 eslint-disable\uC774 "\uBD88\uD544\uC694\uD55C directive" \uACBD\uACE0\uB85C \uC7A1\uD78C\uB2E4.
1228
+ globalIgnores(['dist', '.husky']),
1229
+ {
1230
+ files: ['**/*.{ts,tsx}'],
1231
+ extends: [
1232
+ js.configs.recommended,
1233
+ tseslint.configs.recommended,
1234
+ reactHooks.configs.flat.recommended,
1235
+ reactRefresh.configs.vite,
1236
+ ],
1237
+ languageOptions: {
1238
+ ecmaVersion: 2020,
1239
+ globals: globals.browser,
1240
+ },
1241
+ },
1242
+ ])
1243
+ `;
1244
+ }
1245
+
1246
+ // src/templates/migrateSkillMd.ts
1247
+ function migrateSkillMd() {
1248
+ return `---
1249
+ name: bstage-migrate
1250
+ description: bstage SDK \uBC84\uC804 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD55C\uB2E4. SDK\uAC00 \uC5C5\uB370\uC774\uD2B8\uB418\uC5B4 \uC624\uB798\uB41C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD5C\uC2E0\uC73C\uB85C \uC62C\uB9AC\uAC70\uB098, "bstage \uC5C5\uB370\uC774\uD2B8", "SDK \uB9C8\uC774\uADF8\uB808\uC774\uC158", "\uCD5C\uC2E0 SDK\uB85C \uC62C\uB824\uC918" \uAC19\uC740 \uC694\uCCAD\uC744 \uBC1B\uC744 \uB54C \uC0AC\uC6A9\uD55C\uB2E4.
1251
+ ---
1252
+
1253
+ # \uC5ED\uD560
1254
+
1255
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 bstage SDK\uB97C \uCD5C\uC2E0 \uBC84\uC804\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD55C\uB2E4. \uAC10\uC9C0\uB294 \`bstage doctor\`(\uACB0\uC815\uB860)\uAC00, \uBCC0\uD658 \uC808\uCC28\uB294 SDK\uAC00 \uB3D9\uBD09\uD55C \`MIGRATION.md\`\uAC00 \uC815\uC758\uD55C\uB2E4. \uB108\uB294 \uC774 \uB458\uC744 \uC77D\uACE0 **\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uCDB0** \uC801\uC6A9\uD55C\uB2E4.
1256
+
1257
+ \uD575\uC2EC \uC6D0\uCE59:
1258
+
1259
+ 1. **\uCD94\uCE21 \uAE08\uC9C0 \u2014 \uD56D\uC0C1 \uC2E4\uC81C \uC18C\uC2A4\uB85C \uAC80\uC99D\uD55C\uB2E4.** \uBAA8\uB4E0 \uD310\uB2E8\uC740 \uAE30\uC5B5\xB7\uAC00\uC815\uC774 \uC544\uB2C8\uB77C **\uC9C1\uC811 \uC77D\uC5B4 \uD655\uC778\uD55C \uC0AC\uC2E4**\uC5D0 \uADFC\uAC70\uD574\uC57C \uD55C\uB2E4. \uBCC0\uACBD \uC804\uC5D0 (a) \uC124\uCE58\uB41C \uD328\uD0A4\uC9C0(\`node_modules/@bstage-sdk/*\`\uC758 \`MIGRATION.md\`\xB7\`dist/*.d.ts\` \uD0C0\uC785 \uC120\uC5B8)\uC640 (b) **\uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uC2E4\uC81C \uC18C\uC2A4 \uCF54\uB4DC**\uB97C \uC9C1\uC811 \uC77D\uC5B4 \uD655\uC778\uD55C\uB2E4. SDK API\xB7\uB3D9\uC791\uC744 \uAE30\uC5B5\uC73C\uB85C \uB2E8\uC815\uD558\uC9C0 \uC54A\uB294\uB2E4.
1260
+ 2. **\uC0AC\uC6A9\uC790\uAC00 \uC791\uC131/\uC218\uC815\uD55C \uCF54\uB4DC\uB97C \uD568\uBD80\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uB294\uB2E4.** \uCD94\uAC00\uB294 \uC548\uC804\uD558\uAC8C, \uCDA9\uB3CC\xB7\uC7AC\uAD6C\uC131\uC740 \uD655\uC778\uD55C\uB2E4.
1261
+
1262
+ \uD655\uC778\uC774 \uC548 \uB418\uBA74 **\uCD94\uCE21\uD574\uC11C \uC801\uC6A9\uD558\uC9C0 \uB9D0\uACE0 \uBA48\uCD98\uB2E4.** \uB2E8, \uC0AC\uC6A9\uC790\uC5D0\uAC8C "\uCF54\uB4DC\uB97C \uD655\uC778\uD574 \uB2EC\uB77C"\uACE0 \uB5A0\uB118\uAE30\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC0AC\uC6A9\uC790\uB294 \uAC1C\uBC1C\uC790\uAC00 \uC544\uB2D0 \uC218 \uC788\uC5B4 \uC9C1\uC811 \uAC80\uC99D\uC774 \uC5B4\uB835\uB2E4. **\uAC80\uC99D\uC740 \uB108(\uC5D0\uC774\uC804\uD2B8)\uC758 \uCC45\uC784**\uC774\uB2E4(\uD30C\uC77C\xB7\uC124\uCE58\uB41C \uD328\uD0A4\uC9C0\uB97C \uC77D\uC5B4 \uC2A4\uC2A4\uB85C \uD655\uC778). \uC0AC\uC6A9\uC790\uC5D0\uAC90 \uCF54\uB4DC\uB85C \uD655\uC778 \uAC00\uB2A5\uD55C *\uC0AC\uC2E4*\uC774 \uC544\uB2C8\uB77C **\uC81C\uD488\xB7\uC120\uD638 *\uACB0\uC815*** \uB9CC, \uAD6C\uCCB4\uC801 \uC120\uD0DD\uC9C0\uB85C \uBB3B\uB294\uB2E4.
1263
+
1264
+ # \uAD8C\uC704 \uC788\uB294 \uCD9C\uCC98 (\uBA3C\uC800 \uC77D\uAE30)
1265
+
1266
+ - \`MIGRATION.md\` \u2014 \uBC84\uC804\uBCC4 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD56D\uBAA9\xB7\uC808\uCC28(\uB2E8\uC77C \uC18C\uC2A4). \uBC84\uC804 \uBC94\uD504+\uC124\uCE58 \uD6C4 \`node_modules/@bstage-sdk/cli/dist/MIGRATION.md\`(\uB610\uB294 core docs)\uC5D0\uC11C \uC77D\uB294\uB2E4.
1267
+ - \`bstage doctor\` \u2014 \uD604\uC7AC \uB4DC\uB9AC\uD504\uD2B8 \uC9C4\uB2E8(\uACB0\uC815\uB860). \`npx @bstage-sdk/cli@latest doctor\`.
1268
+ - \uBB38\uC11C \uBAA9\uB85D: \`npx bstage docs\`
1269
+
1270
+ # \uC808\uCC28
1271
+
1272
+ ## 1. \uC9C4\uB2E8 (doctor)
1273
+
1274
+ \`\`\`bash
1275
+ npx @bstage-sdk/cli@latest doctor --json
1276
+ \`\`\`
1277
+
1278
+ \uCD9C\uB825\uC5D0\uC11C \uC138 \uAC00\uC9C0\uB97C \uC5BB\uB294\uB2E4.
1279
+
1280
+ - \`versions\` \u2014 \uB4A4\uCC98\uC9C4 \uD328\uD0A4\uC9C0\uC640 latest \uBC84\uC804
1281
+ - \`files\` \u2014 \uB204\uB77D\uB41C \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uD30C\uC77C
1282
+ - \`recommendedDeps\` \u2014 \uAD8C\uC7A5 \uC758\uC874\uC131\uC758 \uC120\uC5B8 \uC0C1\uD0DC(\`declared: false\`\uBA74 \uBBF8\uC120\uC5B8). **\uBC84\uC804 \uB300\uC870\uB85C\uB294 \uC548 \uC7A1\uD788\uB294 \uCD95**\uC774\uBBC0\uB85C \`migrations\`\uAC00 \uBE44\uC5B4 \uC788\uC5B4\uB3C4 \uB530\uB85C \uD655\uC778\uD55C\uB2E4
1283
+ - \`migrations\` \u2014 \uC801\uC6A9 \uAC00\uB2A5\uD55C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD56D\uBAA9. \uAC01 \uD56D\uBAA9\uC740 \`label\`(\uD5E4\uB354 \uD45C\uAE30, \uC608: \`cli 0.43.0\` \uB610\uB294 \`core 0.32.0 \xB7 react 0.32.0\`) \xB7 \`requirements\`(\uC62C\uB824\uC57C \uD560 \uD328\uD0A4\uC9C0\xB7\uCD5C\uC18C \uBC84\uC804 \uC30D) \xB7 \`items\`(\uD56D\uBAA9 \uC81C\uBAA9)\uB97C \uAC00\uC9C4\uB2E4. \uD5E4\uB354\uC758 \uD328\uD0A4\uC9C0\uB97C \uADF8 \uBC84\uC804 \uC774\uC0C1\uC73C\uB85C \uC62C\uB9B0\uB2E4
1284
+
1285
+ \`isBstageProject\`\uAC00 false\uBA74 bstage \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2C8\uBBC0\uB85C \uC911\uB2E8\uD55C\uB2E4. \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C\uC5D0 \uC2E4\uD328\uD574 \`latestChecked\`\uAC00 false\uBA74 \uBC84\uC804 \uBE44\uAD50\uB97C \uBCF4\uB958\uD55C\uB2E4(\uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8\xB7\uD56D\uBAA9\uC740 \uACC4\uC18D \uC9C4\uD589 \uAC00\uB2A5).
1286
+
1287
+ ## 2. \uACC4\uD68D \uC81C\uC2DC (\uC2B9\uC778 \uC804 \uBA48\uCDA4)
1288
+
1289
+ doctor \uACB0\uACFC\uB97C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC694\uC57D\uD558\uACE0, **\uBB34\uC5C7\uC744 \uD560\uC9C0** \uACC4\uD68D\uC744 \uC81C\uC2DC\uD55C \uB4A4 \uC2B9\uC778\uC744 \uBC1B\uB294\uB2E4. \uC774\uB54C \uC0AC\uC6A9\uC790\uAC00 \uB9AC\uBDF0\uD558\uB294 \uAC83\uC740 MIGRATION.md \uC6D0\uBB38\uC774 \uC544\uB2C8\uB77C **\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uCD98 \uC791\uC5C5 \uACC4\uD68D**\uC774\uB2E4.
1290
+
1291
+ - \uBC84\uC804 \uBC94\uD504 \uBAA9\uB85D(\uD604\uC7AC \u2192 \uBAA9\uD45C)
1292
+ - \uCD94\uAC00\uD560 \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uD30C\uC77C
1293
+ - \uC801\uC6A9\uD560 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD56D\uBAA9, \uAC01 \uD56D\uBAA9\uC758 \`\uC790\uB3D9\`/\`\uD310\uB2E8\` \uAD6C\uBD84
1294
+ - \uD310\uB2E8\uC774 \uD544\uC694\uD55C \uC9C0\uC810(\uC608: \uCEE4\uC2A4\uD140 \uCF54\uB4DC \uAD50\uCCB4)\uC740 \uBBF8\uB9AC \uC9DA\uC5B4 \uB454\uB2E4
1295
+
1296
+ ## 3. \uC801\uC6A9
1297
+
1298
+ ### 3a. \uBC84\uC804 \uBC94\uD504 + \uC124\uCE58 (\uC790\uB3D9)
1299
+
1300
+ \`package.json\`\uC774 \uC758\uC874\uD558\uB294 \`@bstage-sdk/*\` \uD328\uD0A4\uC9C0\uB97C \uBAA8\uB450 latest\uB85C \uC62C\uB9B0\uB2E4 \u2014 \uBCF4\uD1B5 \`cli\`/\`core\`/\`react\`/\`design\`\uC774\uB2E4. \uBC84\uC804 \uD45C\uAE30 \uC2A4\uD0C0\uC77C(\uCE90\uB7FF vs \uC815\uD655\uD55C \uBC84\uC804)\uC740 \uD504\uB85C\uC81D\uD2B8 \uAE30\uC874 \uCEE8\uBCA4\uC158\uC744 \uB530\uB978\uB2E4. \`react\`/\`vite\` \uB4F1 \uC0AC\uC6A9\uC790\uAC00 \uAD00\uB9AC\uD558\uB294 \uC758\uC874\uC131\uC740 **\uC784\uC758\uB85C \uC62C\uB9AC\uAC70\uB098 \uB0B4\uB9AC\uC9C0 \uC54A\uB294\uB2E4**(\uC608: \uD504\uB85C\uC81D\uD2B8\uAC00 \uC774\uBBF8 \uB354 \uB192\uC740 vite\uB97C \uC4F0\uBA74 init \uAE30\uBCF8\uAC12\uC73C\uB85C \uB2E4\uC6B4\uADF8\uB808\uC774\uB4DC \uAE08\uC9C0). \uADF8 \uD6C4 \uC124\uCE58\uD55C\uB2E4(\uD504\uB85C\uC81D\uD2B8\uC758 \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800).
1301
+
1302
+ > **install\uC774 bstage\uC640 \uBB34\uAD00\uD55C peer \uCDA9\uB3CC\uB85C \uC2E4\uD328**\uD558\uBA74(\uC608: \uB2E4\uB978 SDK\uAC00 \uC61B react\uB97C \uC694\uAD6C) \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uC81C\uC57D\uC774\uB2E4. \uAC15\uC81C \uBCC0\uACBD\uD558\uC9C0 \uB9D0\uACE0 \`--legacy-peer-deps\`(npm) \uB4F1 \uADF8 \uD504\uB85C\uC81D\uD2B8\uC758 \uAE30\uC874 \uC124\uCE58 \uBC29\uC2DD\uC73C\uB85C \uC7AC\uC2DC\uB3C4\uD558\uAC70\uB098, \uD310\uB2E8\uC774 \uD544\uC694\uD558\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uACE0\uD55C\uB2E4.
1303
+
1304
+ > \uC774 \uB2E8\uACC4 \uC9C1\uD6C4 \`node_modules/@bstage-sdk/cli/dist/MIGRATION.md\`\uAC00 \uC0DD\uAE34\uB2E4. \uC774\uD6C4 \uB2E8\uACC4\uB294 \uC774 \uD30C\uC77C\uC758 \uD574\uB2F9 \uBC84\uC804 \uC139\uC158\uC744 \uC815\uB3C5\uD558\uACE0 \uB530\uB978\uB2E4.
1305
+
1306
+ doctor\uC758 \`recommendedDeps\`\uC5D0 \`declared: false\`\uAC00 \uC788\uC73C\uBA74 **\uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uACE0** \uD568\uAED8 \uCD94\uAC00\uD55C\uB2E4. \uC774\uAC74 \uBC84\uC804 \uBC94\uD504\uB85C \uC800\uC808\uB85C \uB4E4\uC5B4\uC624\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC608: \`@bstage-sdk/design\`(\uB514\uC790\uC778 \uD1A0\uD070)\uC774 \uC5C6\uC73C\uBA74 AGENTS.md\xB7\`bstage-template\` \uC2A4\uD0AC\uC740 \uD1A0\uD070\uC744 \uC4F0\uB77C\uACE0 \uC548\uB0B4\uD558\uB294\uB370 \uC815\uC791 import\uAC00 \uC2E4\uD328\uD55C\uB2E4. \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC778 \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C\uB294 \`migrations\`\uAC00 \uBE44\uC5B4 \uC774 \uCD95\uB9CC \uB0A8\uC73C\uBBC0\uB85C, \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD560 \uAC8C \uC5C6\uB2E4\uACE0 \uD310\uB2E8\uD558\uACE0 \uB118\uC5B4\uAC00\uC9C0 \uC54A\uB294\uB2E4.
1307
+
1308
+ ### 3b. init \uAE30\uBCF8 \uD30C\uC77C \uCC28\uC774 (\uD310\uB2E8 \u2014 \uAC15\uC694 \uAE08\uC9C0)
1309
+
1310
+ doctor\uC758 "init \uAE30\uBCF8 \uD30C\uC77C" \uD56D\uBAA9\uC740 **\uB204\uB77D\uC774 \uC544\uB2C8\uB77C init \uAE30\uBCF8 \uAD6C\uC131\uACFC\uC758 \uCC28\uC774**\uB2E4. \uCD94\uAC00\uD558\uAE30 \uC804\uC5D0 **\uD504\uB85C\uC81D\uD2B8\uAC00 \uAC19\uC740 \uBAA9\uC801\uC744 \uB2E4\uB974\uAC8C \uB2EC\uC131\uD588\uB294\uC9C0** \uBA3C\uC800 \uD655\uC778\uD558\uACE0, \uD544\uC694\uD560 \uB54C\uB9CC \uCD94\uAC00\uD55C\uB2E4.
1311
+
1312
+ - \`src/vite-env.d.ts\` \u2014 \`tsconfig\`\uC758 \`types:["vite/client"]\`\uB85C \uC774\uBBF8 \uCC98\uB9AC \uC911\uC774\uBA74 \uBD88\uD544\uC694.
1313
+ - \`eslint.config.js\` \u2014 eslint\uB97C \uC4F0\uC9C0 \uC54A\uB294 \uD504\uB85C\uC81D\uD2B8(\uC608: prettier/\uD0C0 \uB9B0\uD130\uB9CC)\uC5D4 \uCD94\uAC00\uD558\uC9C0 \uC54A\uB294\uB2E4.
1314
+ - \`tsconfig.app.json\` / \`tsconfig.node.json\` \u2014 \uB2E8\uC77C \`tsconfig.json\`\uC774 \uC798 \uB3D9\uC791\uD558\uBA74 \uAD73\uC774 \uCABC\uAC1C\uC9C0 \uC54A\uB294\uB2E4. \uBD84\uB9AC\uD560 \uACBD\uC6B0 \uC0AC\uC6A9\uC790 \uC124\uC815(\uACBD\uB85C \uBCC4\uCE6D \uB4F1)\uC744 \uBCF4\uC874\uD55C\uB2E4.
1315
+
1316
+ \uD310\uB2E8\uC774 \uC11C\uC9C0 \uC54A\uC73C\uBA74 \uCD94\uAC00\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD55C\uB2E4.
1317
+
1318
+ ### 3c. \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD56D\uBAA9 (\uD56D\uBAA9\uBCC4\uB85C \uC790\uB3D9/\uD310\uB2E8 \uBD84\uB9AC)
1319
+
1320
+ \`MIGRATION.md\`\uC5D0\uC11C \uC801\uC6A9 \uB300\uC0C1 \uBC84\uC804 \uC139\uC158\uC744 \uC77D\uACE0, \uAC01 \uD56D\uBAA9\uC758 **\uC801\uC6A9** \uC808\uCC28\uB97C \uB530\uB978\uB2E4.
1321
+
1322
+ - \`\uC790\uB3D9\` \uB2E8\uACC4\uB294 \uADF8\uB300\uB85C \uC218\uD589\uD55C\uB2E4.
1323
+ - \`\uD310\uB2E8\` \uB2E8\uACC4\uB294 **\uC2E4\uC81C \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC\uB97C \uC77D\uACE0** \uC801\uC6A9\uD55C\uB2E4. \uC608: \uCEE4\uC2A4\uD140 i18n \uD5EC\uD37C\uAC00 \uC788\uC73C\uBA74 \uC6A9\uB3C4(\uC790\uCCB4 \uBB38\uAD6C vs \uD50C\uB7AB\uD3FC \uBB38\uAD6C)\uB97C \uBD84\uB958\uD574 \`useMessages\`/\`useBstageTranslations\`\uB85C \uC774\uC804\uD55C\uB2E4. \uC560\uB9E4\uD558\uBA74 \uC77C\uAD04 \uCE58\uD658\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294\uB2E4.
1324
+ - \`\uC120\uD0DD\` \uD56D\uBAA9\uC740 \uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uD544\uC694\uD55C\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD55C \uB4A4 \uC9C4\uD589\uD55C\uB2E4.
1325
+
1326
+ ### 3c-1. \uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870 (\uD310\uB2E8 \u2014 \uC0AC\uC6A9\uC790\uB9CC \uB2F5\uD560 \uC218 \uC788\uB294 \uAC83)
1327
+
1328
+ doctor\uC758 \`layout.legacyPresent\`\uAC00 \`true\`\uBA74 \`src/templates/\`\uAC00 \uB0A8\uC544 \uC788\uB294 \uC61B \uAD6C\uC870\uB2E4. \uD398\uC774\uC9C0\uB294 \`src/pages/{\uACBD\uB85C}/\`, \uC704\uC82F\uC740 \`src/slots/{\uC544\uBB34 \uC774\uB984}/\`\uC73C\uB85C \uC62E\uACA8\uC57C \uD558\uACE0, \uC548 \uC62E\uAE30\uBA74 \`bstage build\`\uAC00 \uC5D4\uD2B8\uB9AC\uB97C \uCC3E\uC9C0 \uBABB\uD574 \uBA48\uCD98\uB2E4.
1329
+
1330
+ **\uC5EC\uAE30\uC11C\uB9CC\uC740 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3C\uC5B4\uC57C \uD55C\uB2E4.** \uC774 \uC2A4\uD0AC\uC758 \uC6D0\uCE59\uC740 "\uC0AC\uC2E4\uC740 \uC5D0\uC774\uC804\uD2B8\uAC00 \uAC80\uC99D\uD558\uACE0, \uC0AC\uC6A9\uC790\uC5D0\uAC90 \uACB0\uC815\uB9CC \uBB3B\uB294\uB2E4"\uC774\uC9C0\uB9CC, \uC774 \uD56D\uBAA9\uC740 **\uC0AC\uC2E4 \uC790\uCCB4\uAC00 \uD504\uB85C\uC81D\uD2B8 \uC18C\uC2A4\uC5D0 \uC5C6\uB2E4**:
1331
+
1332
+ - \uD3F4\uB354 \uC774\uB984\uC740 \uC9C0\uAE08\uAE4C\uC9C0 \uC544\uBB34 \uC758\uBBF8\uAC00 \uC5C6\uC5C8\uB2E4 \u2014 \uBE4C\uB4DC\uAC00 \uC784\uC2DC \uB77C\uBCA8\uB85C\uB9CC \uC4F0\uACE0 \uBC84\uB838\uB2E4. \uADF8\uB798\uC11C \`src/templates/home/\`\uC774 \uC2E4\uC81C\uB85C\uB294 \`/landing\`\uC5D0 \uBC30\uD3EC\uB3FC \uC788\uC744 \uC218 \uC788\uB2E4.
1333
+ - \uC2AC\uB86F \uBC30\uCE58\uB294 \uCF54\uB4DC\uC5D0 \uC120\uC5B8\uD558\uC9C0 \uC54A\uC558\uB2E4(\uC124\uACC4\uC0C1 \uAD00\uB9AC\uB3C4\uAD6C \uBAAB\uC774\uC5C8\uB2E4). \uADF8\uB798\uC11C \uD398\uC774\uC9C0\uC778\uC9C0 \uC704\uC82F\uC778\uC9C0\uB3C4 \uC18C\uC2A4\uB85C\uB294 \uAC08\uB9AC\uC9C0 \uC54A\uB294\uB2E4.
1334
+ - \uC5B4\uB4DC\uBBFC \uC804\uC6A9 \`type: 'fullPage' | 'sharedNav'\`\uB294 \uB808\uC774\uC544\uC6C3 \uAD6C\uBD84\uC774\uB77C \uC774 \uD310\uB2E8\uC5D0 \uC4F8 \uC218 \uC5C6\uB2E4.
1335
+
1336
+ \uC2E4\uC81C \uB9E4\uD551\uC740 \uAD00\uB9AC\uB3C4\uAD6C(bstage portal)\uC5D0\uB9CC \uC788\uB2E4. \`layout.legacyTemplates\`\uC758 \uD3F4\uB354\uB97C \uD558\uB098\uC529 \uC9DA\uC5B4 \uBB3B\uB294\uB2E4.
1337
+
1338
+ - \uBA3C\uC800 **\uC18C\uC2A4\uB97C \uC77D\uACE0 \uCD94\uC815\uC548\uC744 \uB9CC\uB4E4\uC5B4 \uC81C\uC2DC\uD55C\uB2E4.** \uC804\uCCB4 \uD654\uBA74\uC744 \uADF8\uB9AC\uB294\uC9C0 \uC791\uC740 \uC870\uAC01\uC778\uC9C0, \uCEF4\uD3EC\uB10C\uD2B8 \uC774\uB984\uC774 \uBB34\uC5C7\uC778\uC9C0\uB294 \uB124\uAC00 \uC77D\uC5B4\uC11C \uC54C \uC218 \uC788\uB2E4. \uC0AC\uC6A9\uC790\uAC00 \uBC31\uC9C0\uC5D0\uC11C \uB2F5\uD558\uAC8C \uB450\uC9C0 \uC54A\uB294\uB2E4.
1339
+ - \uC704\uC82F\uC774\uB77C\uACE0 \uB2F5\uD558\uBA74 **\uC790\uB9AC \uBAA9\uB85D\uC744 \uC120\uD0DD\uC9C0\uB85C \uC81C\uC2DC\uD55C\uB2E4.** \uCE74\uD0C8\uB85C\uADF8\uB294 SDK\uAC00 \uB4E4\uACE0 \uC788\uC73C\uBBC0\uB85C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uCC3E\uC544\uC624\uB77C\uACE0 \uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \`node_modules/@bstage-sdk/core\`\uC758 \`SLOT_CATALOG_V2\`(\uB610\uB294 \`SLOT_CATALOG_V2.md\`)\uB97C \uC77D\uC5B4 \`description\`\uACFC \uD568\uAED8 \uBCF4\uC5EC\uC900\uB2E4.
1340
+ - \uD398\uC774\uC9C0\uB77C\uACE0 \uB2F5\uD558\uBA74 **\uAD00\uB9AC\uB3C4\uAD6C\uC5D0 \uB4F1\uB85D\uB41C \uACBD\uB85C**\uB97C \uBB3B\uB294\uB2E4. \uC774\uAC83\uB9CC\uC740 \uC0AC\uC6A9\uC790\uAC00 \uAD00\uB9AC\uB3C4\uAD6C\uB97C \uBD10\uC57C \uD55C\uB2E4 \u2014 \uB300\uC2E0 "\uAD00\uB9AC\uB3C4\uAD6C\uC5D0\uC11C \uC774 \uCEE4\uC2A4\uD140 \uD398\uC774\uC9C0\uC758 \uACBD\uB85C\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694"\uCC98\uB7FC \uC5B4\uB514\uB97C \uBCF4\uBA74 \uB418\uB294\uC9C0\uAE4C\uC9C0 \uC54C\uB824\uC900\uB2E4.
1341
+ - \uD655\uC778\uC774 \uC548 \uB418\uBA74 \uCD94\uCE21\uD574\uC11C \uC62E\uAE30\uC9C0 \uB9D0\uACE0 \uBA48\uCD98\uB2E4. \uACBD\uB85C\uAC00 \uD2C0\uB9AC\uBA74 \uBC30\uD3EC \uD6C4 \uD654\uBA74\uC774 \uC548 \uB728\uB294\uB370, \uC6D0\uC778\uC744 \uCC3E\uAE30 \uC5B4\uB835\uB2E4.
1342
+
1343
+ \uC62E\uAE34 \uB4A4\uC5D0\uB294 \uAC1C\uBC1C\uC6A9 \uC9C4\uC785\uC810\uC758 \`import.meta.glob\`(\uC720\uC800 \uD504\uB85C\uC81D\uD2B8\uB294 \`src/App.tsx\`, \uC5B4\uB4DC\uBBFC\uC740 \`src/main.tsx\`)\uACFC \`route-preview.json\`\xB7\`config-preview.json\` \uAC12\uC744 \uD568\uAED8 \uACE0\uCE5C\uB2E4(MIGRATION.md\uC758 \uD574\uB2F9 \uD56D\uBAA9 \uCC38\uC870). \uB9C8\uC9C0\uB9C9\uC5D0 \`npx bstage build\`\uB97C \uB3CC\uB824 \uC0B0\uCD9C\uBB3C\uC774 \uC758\uB3C4\uD55C \uACBD\uB85C\uB85C \uB098\uC624\uB294\uC9C0 \uD655\uC778\uD558\uACE0, \uADF8 \uBAA9\uB85D\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uC5EC\uC900\uB2E4.
1344
+
1345
+ > \uAD00\uB9AC\uB3C4\uAD6C\uAC00 \uD604\uC7AC \uB9E4\uD551\uC744 \uC870\uD68C\uD558\uB294 API\uB97C \uC5F4\uBA74 \uC774 \uBB3C\uC74C\uC740 \uC790\uB3D9 \uC870\uD68C\uB85C \uB300\uCCB4\uD560 \uC218 \uC788\uB2E4. \uADF8\uB54C\uAE4C\uC9C0\uB294 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294 \uAC8C \uC720\uC77C\uD55C \uBC29\uBC95\uC774\uB2E4.
1346
+
1347
+ ### 3d. AGENTS.md \uAD00\uB9AC \uC601\uC5ED \uB3D9\uAE30\uD654
1348
+
1349
+ \`AGENTS.md\`\uB294 **SDK \uAD00\uB9AC \uC601\uC5ED**(\uB9C8\uCEE4 \`BSTAGE:MANAGED:START\`~\`END\`\uB85C \uAC10\uC2FC SDK \uC800\uC791 \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8)\uACFC **\uC790\uC720 \uC601\uC5ED**(\uB9C8\uCEE4 \uBC14\uAE65, \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uADDC\uCE59)\uC73C\uB85C \uB098\uB25C\uB2E4. doctor\uC758 \`agentsMd.status\`\uB85C \uBD84\uAE30\uD55C\uB2E4.
1350
+
1351
+ - \`ok\` \u2014 \uD560 \uC77C \uC5C6\uC74C.
1352
+ - \`stale\` (\uB9C8\uCEE4\uB294 \uC788\uACE0 \uBC84\uC804\uB9CC \uB0AE\uC74C) \u2014 **\uC790\uB3D9**. \`bstage skills install\`\uC744 \uC2E4\uD589\uD558\uBA74 \uAD00\uB9AC \uC601\uC5ED\uB9CC \uCD5C\uC2E0\uC73C\uB85C \uCE58\uD658\uB418\uACE0 \uC790\uC720 \uC601\uC5ED\uC740 \uBCF4\uC874\uB41C\uB2E4. \uB05D.
1353
+ - \`legacy\` (\uB9C8\uCEE4 \uC5C6\uC74C \u2014 \uC61B \uD504\uB85C\uC81D\uD2B8) \u2014 **\uD310\uB2E8**. SDK \uB0B4\uC6A9\uACFC \uC0AC\uC6A9\uC790 \uB0B4\uC6A9\uC774 \uD55C \uB369\uC5B4\uB9AC\uB77C 1\uD68C reconcile\uC774 \uD544\uC694\uD558\uB2E4. **\uACBD\uACC4\uB294 \uB124(\uC5D0\uC774\uC804\uD2B8)\uAC00 \uBA3C\uC800 \uBD84\uB958\uD574 \uC81C\uC548\uD558\uACE0, \uC0AC\uC6A9\uC790\uC5D0\uAC90 \uD655\uC778\uB9CC \uBC1B\uB294\uB2E4** \u2014 \uC0AC\uC6A9\uC790\uC5D0\uAC8C "\uC5B4\uB514\uAE4C\uC9C0\uAC00 \uD504\uB85C\uC81D\uD2B8 \uCD94\uAC00\uBD84\uC774\uB0D0"\uB97C \uBC31\uC9C0\uC5D0\uC11C \uBB3B\uC9C0 \uC54A\uB294\uB2E4(\uC0AC\uC6A9\uC790\uB294 AGENTS.md \uAD6C\uC870\uB97C \uBAA8\uB97C \uC218 \uC788\uACE0, \uBD84\uB958\uB294 \uB124 \uCC45\uC784\uC774\uB2E4).
1354
+ 1. doctor \`--json\`\uC758 \`agentsMd.managedBlock\`\uC774 \uC801\uC6A9\uD560 **\uCD5C\uC2E0 SDK \uAD00\uB9AC \uC601\uC5ED(\uB9C8\uCEE4 \uD3EC\uD568)**\uC774\uB2E4. \uC774\uAC78 \uB808\uD37C\uB7F0\uC2A4\uB85C \uC0BC\uC544, \uC61B AGENTS.md\uC758 \uAC01 \uAD6C\uAC04\uC774 **SDK \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8**(\uD504\uB85C\uC81D\uD2B8 \uAC1C\uC694\xB7SDK \uBB38\uC11C \uBAA9\uB85D\xB7\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870\xB7\uD15C\uD50C\uB9BF \uC791\uC131 \uADDC\uCE59\xB7\uAE08\uC9C0 \uC0AC\uD56D\xB7\uC61B \`client.get('/api/...')\` \uC608\uC2DC\uB098 \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uBAA9\uB85D \uB4F1 \u2014 managedBlock\uC774 \uB300\uCCB4\uD558\uB294 \uAC83)\uC778\uC9C0, **\uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uB0B4\uC6A9**(\uC774 \uB808\uD3EC\uB9CC\uC758 \uADDC\uCE59\xB7\uB3C4\uBA54\uC778 \uBA54\uBAA8 \uB4F1)\uC778\uC9C0 \uBD84\uB958\uD55C\uB2E4.
1355
+ 2. \uBD84\uB958 \uACB0\uACFC\uB85C "\uB9E8 \uC704 = managedBlock, \uADF8 \uC544\uB798 \uC790\uC720 \uC601\uC5ED = \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uB0B4\uC6A9\uB9CC" \uAD6C\uC870\uC758 \uC7AC\uC791\uC131\uC548\uC744 \uB9CC\uB4E0\uB2E4. SDK \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8\uB294 managedBlock\uC774 \uB300\uC2E0\uD558\uBBC0\uB85C **\uC790\uC720 \uC601\uC5ED\uC5D0 \uC911\uBCF5 \uBCF4\uC874\uD558\uC9C0 \uC54A\uB294\uB2E4** \u2014 \uD2B9\uD788 \uC61B \`/api/\` \uACBD\uB85C \uBAA9\uB85D\uC774 \uB0A8\uC73C\uBA74 \uC7AC\uBC1C \uC6D0\uC778\uC774 \uB41C\uB2E4.
1356
+ 3. \uC790\uC720 \uC601\uC5ED\uC73C\uB85C \uBCF4\uC874\uD558\uB824\uB294 \uD56D\uBAA9(=\uB124\uAC00 "\uD504\uB85C\uC81D\uD2B8 \uACE0\uC720"\uB85C \uBD84\uB958\uD55C \uAC83)\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC81C\uC2DC\uD558\uACE0 **\uACBD\uACC4\uB97C \uD655\uC778\uBC1B\uB294\uB2E4**: "\uC774 \uBD80\uBD84\uB4E4\uC744 \uD504\uB85C\uC81D\uD2B8 \uACE0\uC720 \uB0B4\uC6A9\uC73C\uB85C \uBCF4\uACE0 \uBCF4\uC874\uD558\uB824\uB294\uB370 \uB9DE\uB098\uC694? SDK \uC601\uC5ED\uC73C\uB85C \uBE7C\uB3C4 \uB418\uB294 \uAC8C \uC11E\uC5EC \uC788\uAC70\uB098, \uBE60\uC9C4 \uAC8C \uC788\uB098\uC694?" (\uC774\uAC74 "\uCF54\uB4DC\uAC00 \uB9DE\uB0D0"\uB294 \uAC80\uC99D\uC774 \uC544\uB2C8\uB77C "\uC774\uAC8C \uB108\uD76C\uAC00 \uCD94\uAC00\uD55C \uAC70\uB0D0"\uB294 \uC18C\uC720 \uD655\uC778\uC774\uB77C \uC0AC\uC6A9\uC790\uAC00 \uB2F5\uD560 \uC218 \uC788\uB2E4.) \uC560\uB9E4\uD55C \uAD6C\uAC04\uC740 \uC784\uC758\uB85C \uC9C0\uC6B0\uC9C0 \uB9D0\uACE0 \uC790\uC720 \uC601\uC5ED\uC5D0 \uB0A8\uACA8 \uD6C4\uBCF4\uB85C \uC81C\uC2DC\uD55C\uB2E4.
1357
+ 4. \uD655\uC778\xB7\uC870\uC815 \uD6C4 \uC7AC\uC791\uC131\uD558\uACE0, \uCD5C\uC885 diff\uB97C \uBCF4\uC5EC\uC900\uB2E4.
1358
+
1359
+ ## 4. \uAC80\uC99D
1360
+
1361
+ - \uC124\uCE58\uAC00 \uB05D\uB0AC\uB294\uC9C0 \uD655\uC778\uD558\uACE0, \uD504\uB85C\uC81D\uD2B8\uC758 \uD0C0\uC785\uCCB4\uD06C\xB7\uB9B0\uD2B8\xB7\uBE4C\uB4DC\uB97C \uB3CC\uB824 \uAE68\uC9C4 \uACF3\uC774 \uC5C6\uB294\uC9C0 \uBCF8\uB2E4.
1362
+ - \`npx @bstage-sdk/cli@latest doctor\`\uB97C \uB2E4\uC2DC \uB3CC\uB824 \uBC84\uC804\uC774 \uCD5C\uC2E0\uC73C\uB85C \uC815\uB9AC\uB410\uB294\uC9C0 \uD655\uC778\uD55C\uB2E4(init \uAE30\uBCF8 \uD30C\uC77C \uCC28\uC774\uB294 \uC758\uB3C4\uC801\uC73C\uB85C \uB0A8\uAE38 \uC218 \uC788\uB2E4).
1363
+ - \uB0A8\uC740 \`\uD310\uB2E8\` \uD56D\uBAA9(\uC608: \uB3C4\uC785\uC744 \uBCF4\uB958\uD55C \uC120\uD0DD \uAE30\uB2A5)\uC774 \uC788\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uACE0\uD55C\uB2E4.
1364
+
1365
+ # \uD558\uC9C0 \uB9D0 \uAC83
1366
+
1367
+ - **\uAE30\uC5B5\xB7\uAC00\uC815\uC73C\uB85C \uC801\uC6A9\uD558\uAE30** \u2014 \uC124\uCE58\uB41C \uD328\uD0A4\uC9C0(\`MIGRATION.md\`\xB7\uD0C0\uC785 \uC120\uC5B8)\uC640 \uD504\uB85C\uC81D\uD2B8 \uC2E4\uC81C \uCF54\uB4DC\uB85C \uD655\uC778\uD558\uC9C0 \uC54A\uACE0 \uBCC0\uD658\uD558\uAE30
1368
+ - **\uC0AC\uC6A9\uC790\uC5D0\uAC8C \uCF54\uB4DC \uAC80\uC99D\uC744 \uB5A0\uB118\uAE30\uAE30** \u2014 "\uC774 \uCF54\uB4DC \uB9DE\uB098\uC694?"\uCC98\uB7FC \uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uD655\uC778\uD574\uC57C \uB2F5\uD560 \uC218 \uC788\uB294 \uC9C8\uBB38\uD558\uAE30 (\uC0AC\uC6A9\uC790\uB294 \uAC1C\uBC1C\uC790\uAC00 \uC544\uB2D0 \uC218 \uC788\uB2E4 \u2014 \uAC80\uC99D\uC740 \uB124 \uBAAB)
1369
+ - \uC0AC\uC6A9\uC790 \uCF54\uB4DC\uB97C \uD1B5\uC9F8\uB85C \uB36E\uC5B4\uC4F0\uAE30 (\uD2B9\uD788 \`src/pages/**\`\xB7\`src/slots/**\`, \uCEE4\uC2A4\uD140 \`shared\`/\`api\` \uB4F1)
1370
+ - \uD15C\uD50C\uB9BF\uC774 \uD398\uC774\uC9C0\uC778\uC9C0 \uC704\uC82F\uC778\uC9C0, \uD398\uC774\uC9C0\uBA74 \uC5B4\uB290 \uACBD\uB85C\uC778\uC9C0\uB97C **\uCD94\uCE21\uD574\uC11C** \uC62E\uAE30\uAE30 (\uAD00\uB9AC\uB3C4\uAD6C\uC5D0\uB9CC \uC788\uB294 \uC815\uBCF4\uB2E4)
1371
+ - \uD55C \uBC88\uC5D0 \uBAA8\uB4E0 \uBCC0\uACBD\uC744 \uC801\uC6A9\uD558\uACE0 \uAC80\uC99D\uC744 \uAC74\uB108\uB6F0\uAE30
1372
+ - \`\uD310\uB2E8\` \uD56D\uBAA9\uC744 \uC0AC\uC6A9\uC790 \uD655\uC778 \uC5C6\uC774 \uCD94\uCE21\uC73C\uB85C \uBCC0\uD658\uD558\uAE30
1373
+ - \`.bstage/i18n/**/*.json\` \uAC19\uC740 \uC7AC\uC0DD\uC131 \uAC00\uB2A5\uD55C \uCE90\uC2DC\uB97C \uC190\uC73C\uB85C \uD3B8\uC9D1\uD558\uAE30 (\`bstage i18n pull\`\uB85C \uC0DD\uC131)
1374
+ `;
1375
+ }
1376
+
1377
+ // src/templates/templateSkillMd.ts
1378
+ function templateSkillMd(target = "user") {
1379
+ return `---
1380
+ name: bstage-template
1381
+ description: b.stage \uC11C\uB4DC\uD30C\uD2F0 \uD15C\uD50C\uB9BF\uC744 \uC791\uC131\xB7\uC218\uC815\uD55C\uB2E4. \uC0C8 \uD15C\uD50C\uB9BF\uC744 \uB9CC\uB4E4\uAC70\uB098, createTemplate\xB7\uC2AC\uB86F\xB7\uD50C\uB7AB\uD3FC \uC774\uBCA4\uD2B8\xB7\uB2E4\uAD6D\uC5B4\xB7API \uD638\uCD9C\uC744 \uB2E4\uB8E8\uAC70\uB098, "\uD15C\uD50C\uB9BF \uCD94\uAC00/\uAC1C\uBC1C", "bstage \uC704\uC82F \uB9CC\uB4E4\uC5B4\uC918" \uAC19\uC740 \uC694\uCCAD\uC744 \uBC1B\uC744 \uB54C \uC0AC\uC6A9\uD55C\uB2E4.
1382
+ ---
1383
+
1384
+ # \uC5ED\uD560
1385
+
1386
+ \uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C b.stage \uD15C\uD50C\uB9BF\uC744 \uC791\uC131\uD55C\uB2E4. \uC791\uC131\uD55C React \uCEF4\uD3EC\uB10C\uD2B8\uB294 SDK\uAC00 Web Component\uB85C \uBE4C\uB4DC\uD574 \uD50C\uB7AB\uD3FC\uC5D0 \uB9C8\uC6B4\uD2B8\uD55C\uB2E4. \uC774 \uC2A4\uD0AC\uC740 **\uBAA9\uD45C\uC9C0\uD5A5 \uC808\uCC28 + \uD568\uC815**\uB9CC \uB2F4\uB294\uB2E4. \uC815\uD655\uD55C \uC2DC\uADF8\uB2C8\uCC98\uB294 \uD56D\uC0C1 \uC124\uCE58\uB41C docs/\uD0C0\uC785\uC744 SSOT\uB85C \uBCF8\uB2E4.
1387
+
1388
+ # \uAD8C\uC704 \uC788\uB294 \uCD9C\uCC98 (\uBA3C\uC800 \uC77D\uAE30)
1389
+
1390
+ \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uC124\uCE58\uB41C \uD328\uD0A4\uC9C0\uB97C \uBCF8\uB2E4. \uC678\uBD80 \uAC80\uC0C9\uBCF4\uB2E4 \uC2E0\uB8B0\uD560 \uC218 \uC788\uB2E4.
1391
+
1392
+ - \`node_modules/@bstage-sdk/core/docs/\` \u2014 GETTING_STARTED, API_REFERENCE, SLOT_*, I18N, BUILD_SYSTEM, DEV_SERVER, DESIGN_TOKENS, SDK_ARCHITECTURE
1393
+ - \uD0C0\uC785: \`node_modules/@bstage-sdk/react/dist/*.d.ts\`, \`node_modules/@bstage-sdk/core/dist/*.d.ts\`
1394
+ - \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59: \uB808\uD3EC \uB8E8\uD2B8\uC758 \`AGENTS.md\`
1395
+ - \uBB38\uC11C \uBAA9\uB85D: \`npx bstage docs\`
1396
+
1397
+ # \uC808\uCC28
1398
+
1399
+ ## 1. \uC0C8 \uD15C\uD50C\uB9BF \uCD94\uAC00
1400
+
1401
+ \uD398\uC774\uC9C0\uB294 \`src/pages/{\uACBD\uB85C}/template.tsx\`(\uD3F4\uB354 \uAD6C\uC870\uAC00 \uACE7 \uBC30\uD3EC \uACBD\uB85C), \uC704\uC82F\uC740 \`src/slots/{\uC544\uBB34 \uC774\uB984}/template.tsx\` + \`slot\` \uC635\uC158\uC73C\uB85C \uB9CC\uB4E0\uB2E4. \uD30C\uC77C\uBA85\uC740 \uBC18\uB4DC\uC2DC \`template.tsx\`(\uB77C\uC6B0\uD305\xB7\uBE4C\uB4DC\uAC00 \uC774 \uCEE8\uBCA4\uC158\uC73C\uB85C \uBC1C\uACAC). \`name\`\uC740 Custom Element \uD0DC\uADF8\uB85C \uADF8\uB300\uB85C \uC4F0\uC774\uBBC0\uB85C **\uC18C\uBB38\uC790 \uC2DC\uC791 + \uD558\uC774\uD508 1\uAC1C \uC774\uC0C1 + \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC**.
1402
+
1403
+ \`\`\`tsx
1404
+ import { createTemplate } from '@bstage-sdk/react'
1405
+
1406
+ export default function MyWidget() {
1407
+ return <div>...</div>
1408
+ }
1409
+
1410
+ createTemplate(MyWidget, { name: 'my-widget' }) // \uD3F4\uB354\uBA85\uACFC \uB3D9\uC77C, \uD558\uC774\uD508 \uD544\uC218
1411
+ \`\`\`
1412
+
1413
+ ## 2. \uAE30\uB2A5\uBCC4 \uD6C5 (\uC2DC\uADF8\uB2C8\uCC98\uB294 API_REFERENCE.md / .d.ts \uD655\uC778)
1414
+
1415
+ - **\uD50C\uB7AB\uD3FC \uB124\uBE44\uAC8C\uC774\uC158**: \`useNavigation()\` \u2014 \`navigate\`/\`goBack\`/\`openExternal\`
1416
+ - **\uD50C\uB7AB\uD3FC \uC774\uBCA4\uD2B8**: \`useBstageContext()\`\uC758 \`bridge\`\uB85C \`emit\`(\uD15C\uD50C\uB9BF\u2192\uD50C\uB7AB\uD3FC), \uAD6C\uB3C5\uC740 \`usePlatformEvent()\`(\uD50C\uB7AB\uD3FC\u2192\uD15C\uD50C\uB9BF)
1417
+ - **API \uD638\uCD9C**: \`shared/client.ts\`\uC758 \`client\` \uC778\uC2A4\uD134\uC2A4\uB97C import\uD574 \uC9C1\uC811 \uD638\uCD9C(\`client.get<T>(...)\`). \`BstageClient\`\uB294 \uC778\uC99D fetch\xB7\uAC8C\uC774\uD2B8\uC6E8\uC774 base URL\uC744 \uC2A4\uC2A4\uB85C \uD574\uC11D\uD558\uBBC0\uB85C \uBCC4\uB3C4 \uBC30\uC120\uC774 \uD544\uC694 \uC5C6\uB2E4. **\uACBD\uB85C\xB7\uC751\uB2F5 \uBAA8\uC591\uC740 SDK\uAC00 \uB4E4\uACE0 \uC788\uC9C0 \uC54A\uB2E4** \u2014 \uAC8C\uC774\uD2B8\uC6E8\uC774 API Reference Doc\uC774 \uCD9C\uCC98\uC774\uACE0, \uC751\uB2F5 \uD0C0\uC785\uC740 \uC81C\uB124\uB9AD\uC73C\uB85C \uBA85\uC2DC\uD55C\uB2E4(\uC0DD\uB7B5\uD558\uBA74 \`unknown\`). **\uC720\uC800\uB2E8 API\uB9CC \uC9C0\uC6D0.** **\uC5B4\uB4DC\uBBFC\uC6A9 API\uB294 \uBBF8\uC9C0\uC6D0 \u2014 \uD544\uC694\uD558\uBA74 \uACBD\uB85C\uB97C \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBA3C\uC800 \uD655\uC778\uD560 \uAC83.**
1418
+ - **\uC2AC\uB86F context**: \`useSlotContext<Id>()\` \u2014 \uD638\uC2A4\uD2B8\uAC00 \uB9C8\uC6B4\uD2B8 \uC2DC 1\uD68C \uC804\uB2EC\uD558\uB294 read-only \uAC12
1419
+ - **\uB2E4\uAD6D\uC5B4**: \uB85C\uCF00\uC77C \uD6C5(\`useLocale\`/\`useMessages\`/\`useBstageTranslations\`)\uC740 **\uBC18\uB4DC\uC2DC \`<BstageLocaleProvider>\` \uD558\uC704\uC5D0\uC11C** \uD638\uCD9C\uD55C\uB2E4(\uBC16\uC774\uBA74 throw). \uC720\uC800 \uD15C\uD50C\uB9BF\uC740 \`<BstageLocaleProvider>\`(target \uC0DD\uB7B5), \uC5B4\uB4DC\uBBFC\uC740 \`<BstageLocaleProvider target="admin">\`. \uC790\uCCB4 \uBB38\uAD6C\uB294 \`useMessages\`(\uC900\uBE44 \uBD88\uD544\uC694). \uD50C\uB7AB\uD3FC \uACF5\uC6A9 \uBB38\uAD6C\uB97C \uC4F0\uB824\uBA74 **\uBA3C\uC800 \`bstage i18n pull\`\uC744 \uC2E4\uD589**\uD574 \uBC88\uC5ED \uD0A4\xB7\uD0C0\uC785\xB7\uB85C\uCEEC \uCE90\uC2DC(\`.bstage/i18n\`)\uB97C \uB9CC\uB4E0 \uB4A4 \`useBstageTranslations\`\uB97C \uC4F4\uB2E4 \u2014 \uC548 \uD558\uBA74 \uB85C\uCEEC dev\uC5D0\uC11C \uBC88\uC5ED\uC774 \uC548 \uB728\uACE0 \uD0A4\uB3C4 \uC54C \uC218 \uC5C6\uB2E4. **\uC720\uC800 \uC2AC\uB86F \uD15C\uD50C\uB9BF\uC740 \`--target user\`(\uAE30\uBCF8, Bxxxxx), \uC5B4\uB4DC\uBBFC \uC2AC\uB86F \uD15C\uD50C\uB9BF\uC740 \`--target admin\`(Axxxxx)** \u2014 Provider \`target\`\uACFC \uAC19\uC740 \uCD95. \uD604\uC7AC \uC5B8\uC5B4\uB9CC \uD544\uC694\uD558\uBA74 \`useLocale\`. \uC0C1\uC138 \uC6CC\uD06C\uD50C\uB85C\uB294 I18N.md
1420
+ - **\uB2E4\uAD6D\uC5B4 \uBB38\uAD6C \uBC30\uBD84 (\uC791\uC5C5 \uC804 \uACB0\uC815)**: "\uB2E4\uAD6D\uC5B4 \uCC98\uB9AC\uD574\uC918" \uB958 \uC694\uCCAD\uC744 \uBC1B\uC73C\uBA74 \uC804\uBD80 \`useMessages\`\uB85C \uB9CC\uB4E4\uC9C0 \uB9D0\uACE0, **\uD50C\uB7AB\uD3FC \uD0A4 \uB300\uC870\uBD80\uD130** \uD55C\uB2E4 \u2014 \`bstage i18n pull\` \uC2E4\uD589 \u2192 \uC0DD\uC131\uB41C \`src/bstage-i18n.ts\`\uC5D0\uC11C \uB300\uC0C1 \uBB38\uAD6C\uC758 \uC6D0\uBB38 \uAC80\uC0C9 \u2192 \uB9E4\uCE6D\uB418\uB294 \uD0A4\uAC00 \uC788\uC73C\uBA74 \uACF5\uC6A9 \uD0A4(\`useBstageTranslations\`), \uC5C6\uB294 \uBB38\uAD6C\uB9CC \`useMessages\`\uC5D0 \uCD94\uAC00. \uC800\uC7A5\xB7\uCDE8\uC18C\xB7\uC801\uC6A9\uD558\uAE30 \uAC19\uC740 \uACF5\uC6A9 UI \uB77C\uBCA8\uC740 \uB300\uBD80\uBD84 \uD50C\uB7AB\uD3FC \uD0A4\uAC00 \uC774\uBBF8 \uC788\uB2E4. \uACF5\uC6A9 \uD0A4\uAC00 \uC788\uB294 \uBB38\uAD6C\uB97C \`useMessages\`\uC5D0 \uBCF5\uC0AC\uD574 \uB36E\uC5B4\uC4F0\uBA74 \uD50C\uB7AB\uD3FC \uBC88\uC5ED \uC218\uC815\uC774 \uD15C\uD50C\uB9BF\uC5D0 \uBC18\uC601\uB418\uC9C0 \uC54A\uB294\uB2E4(\uD63C\uC6A9 \uD328\uD134\uC740 I18N.md).
1421
+
1422
+ ## 3. \uB514\uC790\uC778 \uD1A0\uD070 (\uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790)
1423
+
1424
+ ${designGuideSection(target)}
1425
+
1426
+ ## 4. \uB85C\uCEEC \uC2E4\uD589 / \uBE4C\uB4DC
1427
+
1428
+ - \`npm run dev\` \u2014 \uC778\uC99D \uD504\uB85D\uC2DC \uD3EC\uD568 \uAC1C\uBC1C \uC11C\uBC84. \uB85C\uADF8\uC778: http://localhost:5173/__bstage__/login
1429
+ - \uC0B0\uCD9C\uBB3C \uD655\uC778: \`npx bstage build\` \u2192 \uD398\uC774\uC9C0\uB294 \`dist/{\uACBD\uB85C}/template.js\`, \uC704\uC82F\uC740 \`dist/{\uC2AC\uB86F id}/template.js\` (\uBC30\uD3EC\uB294 \uAD00\uB9AC\uB3C4\uAD6C\uAC00 \uD2B8\uB9AC\uAC70)
1430
+
1431
+ # \uD568\uC815 (SDK \uC124\uACC4 \uC81C\uC57D \u2014 \uC5B4\uAE30\uBA74 \uB7F0\uD0C0\uC784\xB7\uBE4C\uB4DC\uAC00 \uAE68\uC9D0)
1432
+
1433
+ - **CSS\uB294 Shadow DOM \uC548\uC73C\uB85C \uB123\uC5B4\uC57C \uD55C\uB2E4**: \uD15C\uD50C\uB9BF\uC740 Shadow DOM\uC5D0 \uB9C8\uC6B4\uD2B8\uB41C\uB2E4. **\`import './style.css'\`\uB294 \uBC30\uD3EC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC870\uC6A9\uD788 \uC0AC\uB77C\uC9C4\uB2E4** \u2014 \uB85C\uCEEC \uAC1C\uBC1C \uD654\uBA74\uC5D0\uC11C\uB294 \uC801\uC6A9\uB418\uBBC0\uB85C "\uB85C\uCEEC\uC5D0\uC120 \uB418\uB294\uB370 \uBC30\uD3EC\uD558\uBA74 \uC2A4\uD0C0\uC77C\uC774 \uC5C6\uB2E4"\uB85C \uB098\uD0C0\uB09C\uB2E4. \uC4F8 \uC218 \uC788\uB294 \uAC74 \uB458\uBFD0\uC774\uB2E4: \u2460 \`createTemplate\`\uC758 \`styles\` \uC635\uC158(\`import css from './x.css?inline'\`) \u2461 \uCEF4\uD3EC\uB10C\uD2B8 \uC548 \`<style>{cssText}</style>\`. inline \`style={{}}\`\uB3C4 \uBB3C\uB860 \uB41C\uB2E4. \uC804\uC5ED CSS(\`<link>\`)\uB294 \uAE08\uC9C0.
1434
+ - **IIFE \uB2E8\uC77C \uBC88\uB4E4**: \uBC88\uB4E4\uC740 \uD56D\uC0C1 \uB2E8\uC77C IIFE. \uCF54\uB4DC \uC2A4\uD50C\uB9BF\xB7\uB3D9\uC801 \`import()\` \uBD88\uAC00.
1435
+ - **\`__bstage_fetch__\` \uACC4\uC57D**: \uD50C\uB7AB\uD3FC\uC774 \uC778\uC99D fetch\uB97C \uC804\uC5ED\uC73C\uB85C \uC8FC\uC785\uD55C\uB2E4. \`BstageClient\`\uAC00 \uC774\uB97C \uC6B0\uC120 \uC0AC\uC6A9\uD558\uBBC0\uB85C, fetch \uACBD\uB85C\uB97C \uC6B0\uD68C\uD558\uC9C0 \uB9D0 \uAC83.
1436
+ - **\`slot.context\`\uB294 \uB2E8\uBC29\uD5A5\xB7read-only**: \uD638\uC2A4\uD2B8\u2192\uC704\uC82F 1\uD68C \uC804\uB2EC. \uC704\uC82F\uC774 \uB418\uB3CC\uB824 \uC4F0\uC9C0 \uC54A\uB294\uB2E4.
1437
+ - **\`createTemplate()\` \uD638\uCD9C \uD615\uD0DC \uC720\uC9C0**: \uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778\uC774 \uC18C\uC2A4\uC5D0\uC11C \`createTemplate(\` \uD638\uCD9C\uC744 \uD30C\uC2F1\uD574 \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uBF51\uB294\uB2E4. \uC778\uC790 \uAD6C\uC870\uB97C \uC784\uC758 \uBCC0\uD615\uD558\uBA74 \uD30C\uC2F1 \uC2E4\uD328.
1438
+
1439
+ # \uD558\uC9C0 \uB9D0 \uAC83
1440
+
1441
+ - \`src/main.tsx\`\xB7\`src/App.tsx\` \uC218\uC815 (\uAC1C\uBC1C\uC6A9 \uC9C4\uC785\uC810\xB7\uBAA9\uB85D \uD654\uBA74, SDK\uAC00 \uAD00\uB9AC)
1442
+ - \`customElements.define()\` / \`attachShadow\` \uC9C1\uC811 \uD638\uCD9C (createTemplate\uC774 \uCC98\uB9AC)
1443
+ - \`@bstage-sdk/core\`\uC758 \`createWebComponent()\` \uC9C1\uC811 \uC0AC\uC6A9 \u2014 react \uD328\uD0A4\uC9C0 API\uB9CC
1444
+ - UI \uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790\uB97C hex/rgb\uB85C \uD558\uB4DC\uCF54\uB529 \u2014 \uB514\uC790\uC778 \uD1A0\uD070(\uC704 3\uBC88) \uC0AC\uC6A9
1445
+ - \uC2DC\uADF8\uB2C8\uCC98\uB97C \uAE30\uC5B5\uC5D0 \uC758\uC874\uD574 \uCD94\uCE21 \u2014 \uC704 docs/.d.ts\uC5D0\uC11C \uD655\uC778
1446
+ `;
1447
+ }
1448
+
1449
+ // src/templates/skills.ts
1450
+ var BSTAGE_SKILLS = [
1451
+ { name: "bstage-migrate", content: migrateSkillMd },
1452
+ { name: "bstage-template", content: templateSkillMd }
1453
+ ];
1454
+
1455
+ // src/templates/src/mainTsx.ts
1456
+ function mainTsx() {
1457
+ return `import { StrictMode } from 'react'
1458
+ import { createRoot } from 'react-dom/client'
1459
+ import './index.css'
1460
+ import App from './App'
1461
+
1462
+ createRoot(document.getElementById('root')!).render(
1463
+ <StrictMode>
1464
+ <App />
1465
+ </StrictMode>,
1466
+ )
1467
+ `;
1468
+ }
1469
+
1470
+ // src/templates/src/appTsx.ts
1471
+ function appTsx({ hasApiKey }) {
1472
+ const apiKeyGuide = hasApiKey ? "" : `
1473
+ <li><code>src/shared/client.ts</code>\uC5D0 API \uD0A4\uB97C \uC124\uC815\uD558\uBA74 b.stage API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</li>
1474
+ <li>API \uD0A4\uAC00 \uD5C8\uC6A9\uB41C \uD658\uACBD\uC5D0 \uB9DE\uCDB0 <code>.env</code>\uC758 <code>VITE_BSTAGE_PHASE</code>\uB97C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.</li>`;
1475
+ return `import { useState, useEffect, type ComponentType } from 'react'
1476
+
1477
+ /* import.meta.glob\uC73C\uB85C \uBAA8\uB4E0 \uD15C\uD50C\uB9BF\uC744 \uC790\uB3D9 \uAC10\uC9C0 */
1478
+ /* \uD398\uC774\uC9C0\uB294 \uC911\uCCA9 \uD3F4\uB354\uAC00 \uADF8\uB300\uB85C \uBC30\uD3EC \uACBD\uB85C\uAC00 \uB418\uBBC0\uB85C \`**\`\uB85C \uD6D1\uB294\uB2E4. */
1479
+ const modules = import.meta.glob<{ default?: ComponentType; [key: string]: unknown }>([
1480
+ './pages/**/template.tsx',
1481
+ './slots/**/template.tsx',
1482
+ ])
1483
+
1484
+ /**
1485
+ * glob \uACBD\uB85C\uC5D0\uC11C \uBBF8\uB9AC\uBCF4\uAE30 \uC8FC\uC18C\uB97C \uBF51\uB294\uB2E4.
1486
+ * ./pages/settings/custom/template.tsx \u2192 pages/settings/custom
1487
+ * ./slots/curation-banner/template.tsx \u2192 slots/curation-banner
1488
+ *
1489
+ * src/ \uC544\uB798 \uACBD\uB85C\uB97C \uADF8\uB300\uB85C \uC4F0\uB294 \uC774\uC720: \uD398\uC774\uC9C0\uC640 \uC704\uC82F\uC774 \uAC19\uC740 \uC774\uB984\uC744 \uC368\uB3C4 \uACB9\uCE58\uC9C0 \uC54A\uACE0,
1490
+ * \uBAA9\uB85D \uD654\uBA74(/)\uACFC\uB3C4 \uBD80\uB52A\uD788\uC9C0 \uC54A\uB294\uB2E4.
1491
+ */
1492
+ function parseName(path: string): string {
1493
+ const match = path.match(/\\.\\/((?:pages|slots)(?:\\/.+)?)\\/template\\.tsx$/)
1494
+ return match?.[1] ?? path
1495
+ }
1496
+
1497
+ const templateEntries = Object.entries(modules)
1498
+ .map(([path, loader]) => ({ name: parseName(path), loader }))
1499
+ .sort((a, b) => a.name.localeCompare(b.name))
1500
+
1501
+ /** \uD15C\uD50C\uB9BF \uBAA9\uB85D + \uC2DC\uC791 \uC548\uB0B4 */
1502
+ function TemplatePicker() {
1503
+ return (
1504
+ <div style={{ maxWidth: 520, margin: '60px auto', padding: '0 24px', fontFamily: 'system-ui, sans-serif' }}>
1505
+ <h1 style={{ fontSize: 24, marginBottom: 24 }}>b.stage Templates</h1>
1506
+
1507
+ <section style={{ marginBottom: 28 }}>
1508
+ <h2 style={{ fontSize: 15, fontWeight: 600, marginBottom: 8 }}>\uD15C\uD50C\uB9BF \uBAA9\uB85D</h2>
1509
+ <ul style={{ listStyle: 'none', padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
1510
+ {templateEntries.map(({ name }) => (
1511
+ <li key={name}>
1512
+ <a
1513
+ href={\`/\${name}\`}
1514
+ style={{
1515
+ display: 'block',
1516
+ padding: '12px 16px',
1517
+ border: '1px solid #e2e2e2',
1518
+ borderRadius: 8,
1519
+ textDecoration: 'none',
1520
+ color: '#111',
1521
+ fontWeight: 500,
1522
+ transition: 'background 0.15s',
1523
+ }}
1524
+ onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
1525
+ onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
1526
+ >
1527
+ {name}
1528
+ </a>
1529
+ </li>
1530
+ ))}
1531
+ </ul>
1532
+ </section>
1533
+
1534
+ <section style={{ marginBottom: 28 }}>
1535
+ <h2 style={{ fontSize: 15, fontWeight: 600, marginBottom: 8 }}>\uC2DC\uC791\uD558\uAE30</h2>
1536
+ <ol style={{ margin: 0, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 6, color: '#444', fontSize: 14 }}>
1537
+ <li>\uC704 \uD15C\uD50C\uB9BF\uC744 \uD074\uB9AD\uD558\uC5EC \uB80C\uB354\uB9C1 \uACB0\uACFC\uB97C \uD655\uC778\uD558\uC138\uC694.</li>
1538
+ <li>\uD398\uC774\uC9C0\uB294 <code>src/pages/{'{\uACBD\uB85C}'}/template.tsx</code> \u2014 \uD3F4\uB354 \uAD6C\uC870\uAC00 \uACE7 \uBC30\uD3EC \uACBD\uB85C\uC785\uB2C8\uB2E4.</li>
1539
+ <li>\uC704\uC82F\uC740 <code>src/slots/{'{\uC544\uBB34\uC774\uB984}'}/template.tsx</code> \u2014 \uC790\uB9AC\uB294 <code>slot</code> \uC635\uC158\uC774 \uC815\uD569\uB2C8\uB2E4.</li>${apiKeyGuide}
1540
+ </ol>
1541
+ </section>
1542
+
1543
+ <p style={{ fontSize: 13, color: '#999' }}>
1544
+ \uC778\uC99D\uC774 \uD544\uC694\uD55C API\uB97C \uD14C\uC2A4\uD2B8\uD558\uB824\uBA74{' '}
1545
+ <a href="/__bstage__/login" style={{ color: '#999' }}>\uB85C\uADF8\uC778</a>
1546
+ \uD558\uC138\uC694.
1547
+ </p>
1548
+ </div>
1549
+ )
1550
+ }
1551
+
1552
+ export default function App() {
1553
+ const [Component, setComponent] = useState<ComponentType | null>(null)
1554
+ const pathname = location.pathname.replace(/^\\//, '')
1555
+
1556
+ useEffect(() => {
1557
+ if (!pathname) return
1558
+ const entry = templateEntries.find((e) => e.name === pathname)
1559
+ if (!entry) return
1560
+ entry.loader().then((mod) => {
1561
+ /* createTemplate\uC740 default export \uB610\uB294 \uCCAB \uBC88\uC9F8 export\uB41C \uCEF4\uD3EC\uB10C\uD2B8 */
1562
+ const Comp = mod.default ?? (Object.values(mod).find((v) => typeof v === 'function') as ComponentType)
1563
+ if (Comp) setComponent(() => Comp)
1564
+ })
1565
+ }, [pathname])
1566
+
1567
+ if (!pathname) return <TemplatePicker />
1568
+ if (!Component) return <p style={{ padding: 24 }}>Loading...</p>
1569
+ return <Component />
1570
+ }
1571
+ `;
1572
+ }
1573
+
1574
+ // src/templates/src/indexCss.ts
1575
+ function indexCss() {
1576
+ return `body {
1577
+ margin: 0;
1578
+ min-width: 320px;
1579
+ min-height: 100vh;
1580
+ font-family: system-ui, sans-serif;
1581
+ line-height: 1.5;
1582
+ color: #333;
1583
+ background-color: #fff;
1584
+ }
1585
+ `;
1586
+ }
1587
+
1588
+ // src/templates/src/viteEnvDts.ts
1589
+ function viteEnvDts() {
1590
+ return `/// <reference types="vite/client" />
1591
+
1592
+ interface ImportMetaEnv {
1593
+ readonly VITE_BSTAGE_PHASE: string
1594
+ readonly VITE_BSTAGE_APP_ID: string
1595
+ readonly VITE_BSTAGE_APP_SECRET: string
1596
+ readonly VITE_BSTAGE_TENANT_ID: string
1597
+ }
1598
+
1599
+ interface ImportMeta {
1600
+ readonly env: ImportMetaEnv
1601
+ }
1602
+ `;
1603
+ }
1604
+
1605
+ // src/templates/src/clientTs.ts
1606
+ function clientTs() {
1607
+ return `import { BstageClient } from '@bstage-sdk/core'
1608
+
1609
+ // \uC778\uC99D \uC815\uBCF4\uB294 .env(\uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC74C)\uC5D0\uC11C \uC8FC\uC785\uB429\uB2C8\uB2E4.
1610
+ // clone \uD6C4 .env.example\uC744 .env\uB85C \uBCF5\uC0AC\uD574 \uAC12\uC744 \uCC44\uC6B0\uC138\uC694.
1611
+ export const client = new BstageClient({
1612
+ appId: import.meta.env.VITE_BSTAGE_APP_ID,
1613
+ appSecret: import.meta.env.VITE_BSTAGE_APP_SECRET,
1614
+ tenantId: import.meta.env.VITE_BSTAGE_TENANT_ID,
1615
+ })
1616
+ `;
1617
+ }
1618
+
1619
+ // src/templates/src/templateTsx.ts
1620
+ function templateTsx({ name }) {
1621
+ const componentName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1622
+ return `import { createTemplate } from '@bstage-sdk/react'
1623
+ // import { useNavigation, useBstageContext } from '@bstage-sdk/react'
1624
+ // import { client } from '../../shared/client'
1625
+ // CSS \uD30C\uC77C\uC744 \uC4F0\uB824\uBA74 ?inline\uC73C\uB85C \uBB38\uC790\uC5F4\uC744 \uAC00\uC838\uC640 \uC544\uB798 styles\uC5D0 \uB118\uAE34\uB2E4.
1626
+ // \uBD80\uC218\uD6A8\uACFC import(\`import './x.css'\`)\uB294 \uBC30\uD3EC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC0AC\uB77C\uC9C4\uB2E4.
1627
+ // import css from '../../index.css?inline'
1628
+
1629
+ export default function ${componentName}Template() {
1630
+ // \u2500\u2500 \uD50C\uB7AB\uD3FC \uB124\uBE44\uAC8C\uC774\uC158 \u2500\u2500
1631
+ // const { navigate, goBack, openExternal } = useNavigation()
1632
+ // navigate('/profile', { userId: '123' })
1633
+
1634
+ // \u2500\u2500 PlatformBridge \uC9C1\uC811 \uC811\uADFC \u2500\u2500
1635
+ // const { bridge } = useBstageContext()
1636
+ // bridge.emit('toast', { message: '\uC800\uC7A5 \uC644\uB8CC', variant: 'success' })
1637
+
1638
+ // \u2500\u2500 API \uD638\uCD9C (shared/client.ts\uC758 \uC778\uC2A4\uD134\uC2A4\uB97C \uC9C1\uC811 import\uD574 \uC0AC\uC6A9) \u2500\u2500
1639
+ // useEffect(() => {
1640
+ // client.get('/content/v1/boards').then(res => console.log(res.data))
1641
+ // }, [])
1642
+
1643
+ return (
1644
+ <div style={{ padding: 24, fontFamily: 'system-ui, sans-serif' }}>
1645
+ <h1>Hello, ${name}!</h1>
1646
+ </div>
1647
+ )
1648
+ }
1649
+
1650
+ createTemplate(${componentName}Template, {
1651
+ name: '${name}',
1652
+ // styles: css, // Shadow DOM\uC5D0 \uC8FC\uC785 (\uC704 ?inline import\uC640 \uD568\uAED8 \uC0AC\uC6A9)
1653
+ })
1654
+ `;
1655
+ }
1656
+
1657
+ // src/templates/slotsReadme.ts
1658
+ function slotsReadme() {
1659
+ return `# src/slots
1660
+
1661
+ \uD50C\uB7AB\uD3FC \uD654\uBA74 \uC0AC\uC774\uC5D0 \uB07C\uC6CC \uB123\uB294 **\uC704\uC82F**\uC744 \uB450\uB294 \uACF3\uC785\uB2C8\uB2E4.
1662
+ \uD398\uC774\uC9C0 \uC804\uCCB4\uB97C \uADF8\uB9AC\uB294 \uD15C\uD50C\uB9BF\uC740 \`src/pages/\`\uC5D0 \uB461\uB2C8\uB2E4.
1663
+
1664
+ ## \uC704\uC82F \uCD94\uAC00\uD558\uAE30
1665
+
1666
+ \uD3F4\uB354\uB97C \uB9CC\uB4E4\uACE0 \uADF8 \uC548\uC5D0 \`template.tsx\`\uB97C \uB461\uB2C8\uB2E4.
1667
+
1668
+ \`\`\`
1669
+ src/slots/
1670
+ curation-banner/
1671
+ template.tsx
1672
+ \`\`\`
1673
+
1674
+ \`\`\`tsx
1675
+ import { createTemplate } from '@bstage-sdk/react'
1676
+
1677
+ export default function CurationBanner() {
1678
+ return <div>\uBC30\uB108</div>
1679
+ }
1680
+
1681
+ createTemplate(CurationBanner, {
1682
+ name: 'bmf-curation-banner',
1683
+ slot: 'user.contents-home.curation:after',
1684
+ })
1685
+ \`\`\`
1686
+
1687
+ ## \uD3F4\uB354 \uC774\uB984\uC740 \uC790\uC720\uC785\uB2C8\uB2E4
1688
+
1689
+ \uC5B4\uB290 \uC790\uB9AC\uC5D0 \uBD99\uC744\uC9C0\uB294 **\`slot\` \uC635\uC158\uB9CC** \uC815\uD569\uB2C8\uB2E4. \uD3F4\uB354 \uC774\uB984\uC740 \uC0AC\uB78C\uC774 \uC54C\uC544\uBCF4\uAE30 \uC26C\uC6B0\uBA74 \uB429\uB2C8\uB2E4.
1690
+ \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC740 \uC2AC\uB86F id\uB97C \uB530\uB77C \`dist/user.contents-home.curation--after/template.js\`\uB85C \uB098\uAC11\uB2C8\uB2E4
1691
+ (\uCF5C\uB860\uC740 Windows\uC5D0\uC11C \uD3F4\uB354 \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uC5B4 \`--\`\uB85C \uBC14\uB01D\uB2C8\uB2E4).
1692
+
1693
+ \`src/pages/\`\uC640 \uB2EC\uB9AC \uD3F4\uB354 \uAD6C\uC870\uB294 \uBC30\uD3EC \uC704\uCE58\uC5D0 \uC601\uD5A5\uC744 \uC8FC\uC9C0 \uC54A\uC73C\uBBC0\uB85C, \uC6D0\uD558\uB294 \uB9CC\uD07C \uC911\uCCA9\uD574\uB3C4 \uB429\uB2C8\uB2E4.
1694
+
1695
+ ## \uC4F8 \uC218 \uC788\uB294 \uC790\uB9AC \uBAA9\uB85D
1696
+
1697
+ \`slot\`\uC5D0 \uB123\uC744 \uC218 \uC788\uB294 \uAC12\uC740 \uD3B8\uC9D1\uAE30 \uC790\uB3D9\uC644\uC131\uC73C\uB85C \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
1698
+ \uC804\uCCB4 \uBAA9\uB85D\uACFC \uAC01 \uC790\uB9AC\uAC00 \uB118\uACA8\uC8FC\uB294 context\uB294 SDK \uBB38\uC11C\uB97C \uBCF4\uC138\uC694.
1699
+
1700
+ \`\`\`bash
1701
+ npx bstage docs # SLOT_CATALOG_V2.md
1702
+ \`\`\`
1703
+
1704
+ \uC624\uD0C0\uAC00 \uC788\uC73C\uBA74 \`bstage build\`\uAC00 \uD6C4\uBCF4\uC640 \uD568\uAED8 \uB9C9\uC544\uC90D\uB2C8\uB2E4.
1705
+ `;
1706
+ }
1707
+
1708
+ // src/commands/init.ts
1709
+ var PHASES = ["dev", "qa", "real", "sandbox"];
1710
+ var TARGETS = ["user", "admin"];
1711
+ async function initCommand(options) {
1712
+ p.intro(`${pc2.cyan(pc2.bold("bstage init"))} ${pc2.dim("\u2014 b.stage Template Project Setup")}`);
1713
+ let space;
1714
+ let templateName;
1715
+ let packageManager;
1716
+ let phase;
1717
+ let target;
1718
+ let appId = "";
1719
+ let appSecret = "";
1720
+ let tenantId = "";
1721
+ if (options.yes) {
1722
+ const targetOpt = options.target ?? "user";
1723
+ if (!TARGETS.includes(targetOpt)) {
1724
+ p.log.error(`--target \uAC12\uC740 ${TARGETS.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
1725
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1726
+ process.exit(1);
1727
+ }
1728
+ target = targetOpt;
1729
+ if (!options.space) {
1730
+ p.log.error("--yes \uBAA8\uB4DC\uC5D0\uC11C\uB294 --space\uAC00 \uD544\uC218\uC785\uB2C8\uB2E4.");
1731
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1732
+ process.exit(1);
1733
+ }
1734
+ const slugPattern = /^[a-zA-Z][a-zA-Z0-9-]*$/;
1735
+ if (!slugPattern.test(options.space)) {
1736
+ p.log.error(
1737
+ `${pc2.red("--space")} \uAC12\uC740 \uC601\uBB38\uC790\uB85C \uC2DC\uC791\uD574\uC57C \uD558\uBA70, \uC601\uBB38, \uC22B\uC790, \uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
1738
+ );
1739
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1740
+ process.exit(1);
1741
+ }
1742
+ space = options.space;
1743
+ const templateOpt = resolveTemplateName(space, options.template);
1744
+ if (!ELEMENT_NAME_RE2.test(templateOpt)) {
1745
+ p.log.error(
1746
+ `${pc2.red("--template")} \uAC12\uC740 Custom Element \uC2A4\uD399\uC744 \uB530\uB77C\uC57C \uD569\uB2C8\uB2E4: \uC18C\uBB38\uC790 \uC2DC\uC791, \uD558\uC774\uD508 1\uAC1C \uC774\uC0C1, \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC.`
1747
+ );
1748
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1749
+ process.exit(1);
1750
+ }
1751
+ templateName = templateOpt;
1752
+ packageManager = options.pm ?? "npm";
1753
+ const phaseOpt = options.phase ?? "sandbox";
1754
+ if (!PHASES.includes(phaseOpt)) {
1755
+ p.log.error(`--phase \uAC12\uC740 ${PHASES.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
1756
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1757
+ process.exit(1);
1758
+ }
1759
+ phase = phaseOpt;
1760
+ } else {
1761
+ target = await p.select({
1762
+ message: "\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF\uC740 \uC5B4\uB290 \uD50C\uB7AB\uD3FC\uC6A9\uC778\uAC00\uC694? (\uB808\uD3EC\uC5D0 \uB450 \uC885\uB958\uB97C \uC11E\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4)",
1763
+ options: [
1764
+ { value: "user", label: "user (b.stage \uC720\uC800 \uD50C\uB7AB\uD3FC)" },
1765
+ { value: "admin", label: "admin (b.stage \uC5B4\uB4DC\uBBFC \uD50C\uB7AB\uD3FC)" }
1766
+ ],
1767
+ initialValue: options.target === "admin" ? "admin" : "user"
1768
+ });
1769
+ if (p.isCancel(target)) return cancelled();
1770
+ space = await p.text({
1771
+ message: "\uD15C\uD50C\uB9BF\uC744 \uC801\uC6A9\uD560 Space ID\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
1772
+ validate: validateSlug("Space ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.")
1773
+ });
1774
+ if (p.isCancel(space)) return cancelled();
1775
+ phase = await p.select({
1776
+ message: "\uD15C\uD50C\uB9BF\uC744 \uBC30\uD3EC\uD560 \uD658\uACBD\uC744 \uC120\uD0DD\uD574 \uC8FC\uC138\uC694.",
1777
+ options: [
1778
+ { value: "sandbox", label: "sandbox", hint: "\uAE30\uBCF8" },
1779
+ { value: "real", label: "real" },
1780
+ { value: "dev", label: "dev", hint: "\uC0AC\uB0B4 \uC804\uC6A9 \u2014 .env\uC5D0 \uD638\uC2A4\uD2B8\xB7CF Access \uC790\uACA9\uC99D\uBA85 \uD544\uC694" },
1781
+ { value: "qa", label: "qa", hint: "\uC0AC\uB0B4 \uC804\uC6A9 \u2014 .env\uC5D0 \uD638\uC2A4\uD2B8\xB7CF Access \uC790\uACA9\uC99D\uBA85 \uD544\uC694" }
1782
+ ],
1783
+ initialValue: "sandbox"
1784
+ });
1785
+ if (p.isCancel(phase)) return cancelled();
1786
+ templateName = await p.text({
1787
+ message: `\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF \uC774\uB984\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694. ${pc2.dim(`(\uC608: ${space}-hello \u2014 \uD558\uC774\uD508 \uD544\uC218)`)}`,
1788
+ placeholder: resolveTemplateName(space, void 0),
1789
+ validate: validateElementName
1790
+ });
1791
+ if (p.isCancel(templateName)) return cancelled();
1792
+ packageManager = await p.select({
1793
+ message: "\uC0AC\uC6A9\uD560 \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800\uB97C \uC120\uD0DD\uD574 \uC8FC\uC138\uC694.",
1794
+ options: [
1795
+ { value: "npm", label: "npm" },
1796
+ { value: "pnpm", label: "pnpm" }
1797
+ ],
1798
+ initialValue: "npm"
1799
+ });
1800
+ if (p.isCancel(packageManager)) return cancelled();
1801
+ const configureAuth = await p.confirm({
1802
+ message: "b.stage \uD30C\uD2B8\uB108\uC2A4\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 API \uD0A4\uAC00 \uC788\uC73C\uC2E0\uAC00\uC694? (\uC5C6\uC5B4\uB3C4 \uC2DC\uC791\uD560 \uC218 \uC788\uC5B4\uC694)",
1803
+ initialValue: false
1804
+ });
1805
+ if (p.isCancel(configureAuth)) return cancelled();
1806
+ if (configureAuth) {
1807
+ p.log.info(pc2.dim("API \uC778\uC99D \uC124\uC815 (\uC120\uD0DD)"));
1808
+ appId = await p.text({
1809
+ message: "\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 APP-ID\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694."
1810
+ }) ?? "";
1811
+ if (p.isCancel(appId)) return cancelled();
1812
+ appSecret = await p.text({
1813
+ message: "\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 Secret \uAC12\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."
1814
+ }) ?? "";
1815
+ if (p.isCancel(appSecret)) return cancelled();
1816
+ warnIfCredentialsSwapped(appId, appSecret);
1817
+ tenantId = space;
1818
+ }
1819
+ }
1820
+ const projectName = `${space}-custom-templates-${phase}`;
1821
+ const projectDir = resolve2(process.cwd(), projectName);
1822
+ if (existsSync(projectDir)) {
1823
+ p.log.error(
1824
+ `${pc2.red(projectDir)} \uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uD574\uB2F9 \uB514\uB809\uD1A0\uB9AC\uB97C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.`
1825
+ );
1826
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1827
+ process.exit(1);
1828
+ }
1829
+ const s = p.spinner();
1830
+ s.start("SDK \uCD5C\uC2E0 \uBC84\uC804 \uC870\uD68C \uC911...");
1831
+ let sdkVersions;
1832
+ try {
1833
+ const [cli, core, react2, design] = await Promise.all([
1834
+ fetchLatestVersion("@bstage-sdk/cli"),
1835
+ fetchLatestVersion("@bstage-sdk/core"),
1836
+ fetchLatestVersion("@bstage-sdk/react"),
1837
+ fetchLatestVersion("@bstage-sdk/design")
1838
+ ]);
1839
+ sdkVersions = { cli, core, react: react2, design };
1840
+ s.stop(`SDK \uBC84\uC804 \uD655\uC815 \u2014 cli@${cli}, core@${core}, react@${react2}, design@${design}`);
1841
+ } catch (err) {
1842
+ s.stop(pc2.red("SDK \uCD5C\uC2E0 \uBC84\uC804 \uC870\uD68C \uC2E4\uD328"));
1843
+ p.log.error(
1844
+ (err instanceof Error ? err.message : String(err)) + `
1845
+ ${pc2.dim("\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uACFC registry.npmjs.org \uC811\uADFC\uC774 \uAC00\uB2A5\uD55C\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.")}`
1846
+ );
1847
+ p.outro(pc2.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1848
+ process.exit(1);
1849
+ }
1850
+ s.start("\uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC911...");
1851
+ const dirs = [
1852
+ "",
1853
+ "src",
1854
+ ".husky",
1855
+ "src/shared",
1856
+ `src/pages/${templateName}`,
1857
+ "src/slots",
1858
+ ...BSTAGE_SKILLS.map((s2) => `.claude/skills/${s2.name}`)
1859
+ ];
1860
+ for (const dir of dirs) {
1861
+ mkdirSync(join2(projectDir, dir), { recursive: true });
1862
+ }
1863
+ const envFiles = [
1864
+ // .env(실제 값)는 커밋되지 않음(.gitignore). .env.example은 커밋되어 팀원 세팅 안내.
1865
+ [
1866
+ ".env",
1867
+ env({
1868
+ phase,
1869
+ appId: appId || "YOUR_APP_ID",
1870
+ appSecret: appSecret || "YOUR_APP_SECRET",
1871
+ tenantId: tenantId || "YOUR_TENANT_ID"
1872
+ })
1873
+ ],
1874
+ [
1875
+ ".env.example",
1876
+ env({
1877
+ phase,
1878
+ appId: "YOUR_APP_ID",
1879
+ appSecret: "YOUR_APP_SECRET",
1880
+ tenantId: "YOUR_TENANT_ID"
1881
+ })
1882
+ ]
1883
+ ];
1884
+ const files = [
1885
+ ["AGENTS.md", agentsMd({ space, projectName, target })],
1886
+ ["CLAUDE.md", claudeMd()],
1887
+ ["README.md", readme({ projectName, packageManager })],
1888
+ ["package.json", packageJson({ projectName, target, sdkVersions })],
1889
+ ["tsconfig.json", tsconfig()],
1890
+ ["tsconfig.app.json", tsconfigApp()],
1891
+ ["tsconfig.node.json", tsconfigNode()],
1892
+ ["vite.config.ts", viteConfig()],
1893
+ ...envFiles,
1894
+ ["index.html", indexHtml()],
1895
+ // pnpm은 의존성 build script를 막고 그걸 에러로 낸다 → 허용 목록이 없으면 install이 실패한다.
1896
+ ...packageManager === "pnpm" ? [["pnpm-workspace.yaml", pnpmWorkspace()]] : [],
1897
+ [".gitignore", gitignore()],
1898
+ [".husky/pre-commit", huskyPreCommit()],
1899
+ [".husky/check-secrets.mjs", checkSecretsScript()],
1900
+ ["eslint.config.js", eslintConfig()],
1901
+ ["src/main.tsx", mainTsx()],
1902
+ // 화면은 App.tsx가 갖는다 — main.tsx에 컴포넌트를 두면 export 없는 파일이 되어 lint가 막는다.
1903
+ ["src/App.tsx", appTsx({ hasApiKey: !!appId })],
1904
+ ["src/index.css", indexCss()],
1905
+ ["src/vite-env.d.ts", viteEnvDts()],
1906
+ ["src/shared/client.ts", clientTs()],
1907
+ [`src/pages/${templateName}/template.tsx`, templateTsx({ name: templateName })],
1908
+ // 빈 디렉토리는 git이 추적하지 않아 커밋하면 src/slots가 사라진다. 안내를 겸해 README를 둔다.
1909
+ ["src/slots/README.md", slotsReadme()],
1910
+ ...BSTAGE_SKILLS.map(
1911
+ (s2) => [`.claude/skills/${s2.name}/SKILL.md`, s2.content(target)]
1912
+ )
1913
+ ];
1914
+ for (const [filePath, content] of files) {
1915
+ writeFileSync(join2(projectDir, filePath), content, "utf-8");
1916
+ }
1917
+ s.stop("\uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC");
1918
+ s.start("Git \uCD08\uAE30\uD654 \uC911...");
1919
+ try {
1920
+ execSync("git init", { cwd: projectDir, stdio: "ignore" });
1921
+ execSync("git add -A", { cwd: projectDir, stdio: "ignore" });
1922
+ execSync('git commit -m "init: bstage \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654"', {
1923
+ cwd: projectDir,
1924
+ stdio: "ignore"
1925
+ });
1926
+ s.stop("Git \uCD08\uAE30\uD654 \uC644\uB8CC");
1927
+ } catch {
1928
+ s.stop(pc2.yellow("Git \uCD08\uAE30\uD654 \uAC74\uB108\uB700 (git\uC774 \uC124\uCE58\uB418\uC9C0 \uC54A\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4)"));
1929
+ }
1930
+ s.start(`\uC758\uC874\uC131 \uC124\uCE58 \uC911... (${packageManager} install)`);
1931
+ try {
1932
+ const installCmd = packageManager === "pnpm" ? "pnpm install" : "npm install";
1933
+ execSync(installCmd, { cwd: projectDir, stdio: "ignore", env: { ...process.env } });
1934
+ s.stop("\uC758\uC874\uC131 \uC124\uCE58 \uC644\uB8CC");
1935
+ } catch {
1936
+ s.stop(pc2.yellow("\uC758\uC874\uC131 \uC124\uCE58 \uC2E4\uD328 \u2014 \uC9C1\uC811 \uC124\uCE58\uD574 \uC8FC\uC138\uC694"));
1937
+ }
1938
+ const runCmd = packageManager === "pnpm" ? "pnpm dev" : "npm run dev";
1939
+ const label = target === "admin" ? "\uC5B4\uB4DC\uBBFC \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8" : "\uD504\uB85C\uC81D\uD2B8";
1940
+ p.outro(`${pc2.green(`${label}\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4!`)}
1941
+
1942
+ \uB2E4\uC74C \uB2E8\uACC4:
1943
+ ${pc2.cyan(`cd ${projectName}`)}
1944
+ ${pc2.cyan(runCmd)}
1945
+ ${pc2.dim("# \uBE0C\uB77C\uC6B0\uC800: http://localhost:5173")}`);
1946
+ }
1947
+ async function fetchLatestVersion(packageName) {
1948
+ const res = await fetch(`https://registry.npmjs.org/${packageName}`, {
1949
+ headers: { Accept: "application/json" }
1950
+ });
1951
+ if (!res.ok) {
1952
+ throw new Error(`${packageName} \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C \uC2E4\uD328 (HTTP ${res.status})`);
1953
+ }
1954
+ const data = await res.json();
1955
+ const latest = data["dist-tags"]?.latest;
1956
+ if (!latest) {
1957
+ throw new Error(`${packageName}\uC758 latest \uBC84\uC804\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
1958
+ }
1959
+ return latest;
1960
+ }
1961
+ function validateSlug(requiredMessage) {
1962
+ return (v) => {
1963
+ if (!v?.trim()) return requiredMessage;
1964
+ if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(v.trim()))
1965
+ return "\uC601\uBB38\uC790\uB85C \uC2DC\uC791\uD574\uC57C \uD558\uBA70, \uC601\uBB38, \uC22B\uC790, \uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
1966
+ };
1967
+ }
1968
+ var ELEMENT_NAME_RE2 = /^[a-z][a-z0-9-]*-[a-z0-9-]*$/;
1969
+ function resolveTemplateName(space, option) {
1970
+ return option ?? `${space.toLowerCase()}-hello`;
1971
+ }
1972
+ function validateElementName(v) {
1973
+ if (!v?.trim()) return "\uD15C\uD50C\uB9BF \uC774\uB984\uC740 \uD544\uC218\uC785\uB2C8\uB2E4.";
1974
+ if (!ELEMENT_NAME_RE2.test(v.trim())) {
1975
+ return "Custom Element \uC2A4\uD399: \uC18C\uBB38\uC790 \uC2DC\uC791 + \uD558\uC774\uD508 1\uAC1C \uC774\uC0C1 + \uC18C\uBB38\uC790/\uC22B\uC790/\uD558\uC774\uD508\uB9CC.";
1976
+ }
1977
+ }
1978
+ var APP_ID_PREFIX2 = "bsa_";
1979
+ var APP_SECRET_PREFIX2 = "bsp_";
1980
+ function warnIfCredentialsSwapped(appId, appSecret) {
1981
+ if (!appId.startsWith(APP_SECRET_PREFIX2) && !appSecret.startsWith(APP_ID_PREFIX2)) return;
1982
+ p.log.warn(
1983
+ `APP-ID\uC640 Secret\uC774 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
1984
+ APP-ID\uB294 "${APP_ID_PREFIX2}", Secret\uC740 "${APP_SECRET_PREFIX2}"\uB85C \uC2DC\uC791\uD569\uB2C8\uB2E4.
1985
+ \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC744 \uB2E4\uC2DC \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
1986
+ );
1987
+ }
1988
+ function cancelled() {
1989
+ p.outro(pc2.red("\uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1990
+ process.exit(0);
1991
+ }
1992
+
1993
+ // src/commands/i18n.ts
1994
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1995
+ import { dirname, join as join3, resolve as resolve3 } from "path";
1996
+ import pc3 from "picocolors";
1997
+ function latestKey(phase, tier, target) {
1998
+ const segments = tier === DEFAULT_TRANSLATION_TIER ? [TRANSLATION_CDN_BASE_KEY, phase, target] : [TRANSLATION_CDN_BASE_KEY, phase, target, tier];
1999
+ return [...segments, "latest.json"].join("/");
2000
+ }
2001
+ function cacheStamp(now) {
2002
+ const pad = (n) => String(n).padStart(2, "0");
2003
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}`;
2004
+ const min = now.getMinutes();
2005
+ const floored = min - min % 5;
2006
+ return `${date}-${pad(floored)}`;
2007
+ }
2008
+ async function fetchJson(url) {
2009
+ const res = await fetch(url);
2010
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2011
+ return await res.json();
2012
+ }
2013
+ function escapeJsdoc(value) {
2014
+ return value.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
2015
+ }
2016
+ function generateTypeModule(dict, meta2) {
2017
+ const keys = Object.keys(dict).sort();
2018
+ const registry = keys.map((k) => ` /** ${escapeJsdoc(dict[k])} */
2019
+ ${JSON.stringify(k)}: true`).join("\n");
2020
+ const consts = keys.map((k) => ` /** ${escapeJsdoc(dict[k])} */
2021
+ ${JSON.stringify(k)}: ${JSON.stringify(k)},`).join("\n");
2022
+ return `// AUTO-GENERATED by \`bstage i18n pull\` \u2014 \uC9C1\uC811 \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694.
2023
+ // version: ${meta2.version} (ref=${meta2.ref}, \uD0A4 ${keys.length}\uAC1C)
2024
+ /* eslint-disable */
2025
+
2026
+ declare module '@bstage-sdk/core' {
2027
+ interface TranslationKeyRegistry {
2028
+ ${registry}
2029
+ }
2030
+ }
2031
+
2032
+ /** \uD50C\uB7AB\uD3FC \uBC88\uC5ED \uD0A4 \u2192 \uCF54\uB4DC. \uD638\uBC84\uB85C \uC6D0\uBB38 \uD655\uC778, \uCF54\uB4DC \uB0B4 \uC6D0\uBB38 \uAC80\uC0C9\uC5D0 \uC0AC\uC6A9\uD55C\uB2E4. */
2033
+ export const bstageTranslationKeys = {
2034
+ ${consts}
2035
+ } as const
2036
+ `;
2037
+ }
2038
+ async function i18nPullCommand(options) {
2039
+ const root = process.cwd();
2040
+ const tier = options.tier || DEFAULT_TRANSLATION_TIER;
2041
+ const target = options.target || "user";
2042
+ const latestUrl = `${TRANSLATION_CDN_ORIGIN}/${latestKey(options.phase, tier, target)}?date=${cacheStamp(/* @__PURE__ */ new Date())}`;
2043
+ console.log(pc3.cyan(`[bstage] \uBC88\uC5ED pull: ${options.phase}/${target} (tier=${tier})`));
2044
+ let latest;
2045
+ try {
2046
+ latest = await fetchJson(latestUrl);
2047
+ } catch (err) {
2048
+ const msg = err instanceof Error ? err.message : String(err);
2049
+ console.error(pc3.red(`[bstage] latest.json \uC870\uD68C \uC2E4\uD328: ${msg}
2050
+ ${latestUrl}`));
2051
+ process.exit(1);
2052
+ }
2053
+ const versionBase = `${TRANSLATION_CDN_ORIGIN}/${latest.cdnPath.replace(TRANSLATION_CDN_BUCKET_PREFIX, "")}`;
2054
+ console.log(pc3.dim(` version: ${latest.version}`));
2055
+ const cacheDir = join3(root, ".bstage", "i18n", target);
2056
+ mkdirSync2(cacheDir, { recursive: true });
2057
+ let refDict = null;
2058
+ const fetched = [];
2059
+ for (const locale of TRANSLATION_LANGUAGES) {
2060
+ try {
2061
+ const dict = await fetchJson(`${versionBase}/${locale}/translation.json`);
2062
+ writeFileSync2(join3(cacheDir, `${locale}.json`), JSON.stringify(dict, null, 2));
2063
+ fetched.push(locale);
2064
+ if (locale === options.ref) refDict = dict;
2065
+ console.log(pc3.green(` \u2713 ${locale} (${Object.keys(dict).length})`));
2066
+ } catch (err) {
2067
+ const msg = err instanceof Error ? err.message : String(err);
2068
+ console.warn(pc3.yellow(` - ${locale}: ${msg} \u2014 \uAC74\uB108\uB700`));
2069
+ }
2070
+ }
2071
+ if (fetched.length === 0) {
2072
+ console.error(pc3.red("[bstage] \uBC1B\uC740 \uBC88\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2073
+ process.exit(1);
2074
+ }
2075
+ if (!refDict) {
2076
+ console.warn(
2077
+ pc3.yellow(`[bstage] ref \uB85C\uCF00\uC77C(${options.ref}) \uC0AC\uC804\uC774 \uC5C6\uC5B4 \uD0C0\uC785 \uC0DD\uC131\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4.`)
2078
+ );
2079
+ return;
2080
+ }
2081
+ const outPath = resolve3(root, options.out);
2082
+ mkdirSync2(dirname(outPath), { recursive: true });
2083
+ writeFileSync2(outPath, generateTypeModule(refDict, { version: latest.version, ref: options.ref }));
2084
+ console.log(pc3.green(`[bstage] \uD0C0\uC785 \uC0DD\uC131: ${options.out} (${Object.keys(refDict).length} \uD0A4)`));
2085
+ console.log(
2086
+ pc3.dim(
2087
+ ` \uCE90\uC2DC: .bstage/i18n/${target}/*.json \u2014 \uD0C0\uC785 \uC0DD\uC131 \uC785\uB825 + \uC6D0\uBB38 \uAC80\uC0C9\uC6A9(\uB7F0\uD0C0\uC784\uC740 CDN\uC5D0\uC11C \uC9C1\uC811 \uBC1B\uB294\uB2E4)`
2088
+ )
2089
+ );
2090
+ console.log(pc3.dim(" \uC0DD\uC131 \uD30C\uC77C\uC744 \uD55C \uBC88 import\uD558\uBA74 useBstageTranslations().t() \uD0A4\uAC00 \uC881\uD600\uC9D1\uB2C8\uB2E4."));
2091
+ }
2092
+
2093
+ // src/commands/doctor.ts
2094
+ import { existsSync as existsSync3 } from "fs";
2095
+ import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
2096
+ import { dirname as dirname2, join as join5 } from "path";
2097
+ import { fileURLToPath } from "url";
2098
+ import pc5 from "picocolors";
2099
+
2100
+ // src/commands/skills.ts
2101
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
2102
+ import { join as join4 } from "path";
2103
+ import pc4 from "picocolors";
2104
+ async function skillsInstallCommand(options) {
2105
+ const root = process.cwd();
2106
+ const baseDir = options.dir || ".claude/skills";
2107
+ const target = resolveProjectTarget(root);
2108
+ console.log(pc4.cyan(`[bstage] \uC2A4\uD0AC \uC124\uCE58: ${baseDir}/`) + pc4.dim(` (\uB514\uC790\uC778 \uD0C0\uAE43: ${target})`));
2109
+ console.log(
2110
+ pc4.dim(
2111
+ " SDK\uAC00 \uC18C\uC720\uD558\uB294 \uC790\uC0B0\uC785\uB2C8\uB2E4. \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC874 SKILL.md\uB294 \uB36E\uC5B4\uC4F0\uC5EC \uCD5C\uC2E0\uC73C\uB85C \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4."
2112
+ )
2113
+ );
2114
+ for (const skill of BSTAGE_SKILLS) {
2115
+ const dir = join4(root, baseDir, skill.name);
2116
+ mkdirSync3(dir, { recursive: true });
2117
+ const file = join4(dir, "SKILL.md");
2118
+ const action = existsSync2(file) ? "\uAC31\uC2E0" : "\uC0DD\uC131";
2119
+ writeFileSync3(file, skill.content(target), "utf-8");
2120
+ console.log(pc4.green(` \u2713 ${skill.name} (${action})`));
2121
+ }
2122
+ console.log(
2123
+ pc4.dim(
2124
+ ` ${BSTAGE_SKILLS.length}\uAC1C \uC2A4\uD0AC \uC124\uCE58 \uC644\uB8CC. Claude Code\uAC00 ${baseDir}/ \uC758 \uC2A4\uD0AC\uC744 \uC790\uB3D9 \uC778\uC2DD\uD569\uB2C8\uB2E4.`
2125
+ )
2126
+ );
2127
+ syncAgentsMd(root, target);
2128
+ installGuardHook(root);
2129
+ }
2130
+ function syncAgentsMd(root, target) {
2131
+ const file = join4(root, "AGENTS.md");
2132
+ if (!existsSync2(file)) {
2133
+ console.log(pc4.dim(" \xB7 AGENTS.md \uC5C6\uC74C \u2014 `bstage init` \uB610\uB294 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694."));
2134
+ return;
2135
+ }
2136
+ const content = readFileSync(file, "utf-8");
2137
+ const status = agentsMdStatus(content);
2138
+ if (status === "legacy") {
2139
+ console.log(
2140
+ pc4.yellow(" ! AGENTS.md\uAC00 \uB808\uAC70\uC2DC \uD3EC\uB9F7\uC785\uB2C8\uB2E4(\uAD00\uB9AC \uC601\uC5ED \uB9C8\uCEE4 \uC5C6\uC74C).") + pc4.dim(" `bstage-migrate` \uC2A4\uD0AC\uB85C reconcile\uD558\uC138\uC694 \u2014 skills install\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
2141
+ );
2142
+ return;
2143
+ }
2144
+ const parsed = parseAgentsIdentity(content);
2145
+ const projectName = parsed.projectName ?? readPkgName(root) ?? "custom-templates";
2146
+ const space = parsed.space ?? projectName.split("-custom-templates-")[0] ?? projectName;
2147
+ const swapped = swapManagedBlock(content, renderManagedBlock({ space, projectName, target }));
2148
+ if (swapped === null) {
2149
+ console.log(pc4.dim(" \xB7 AGENTS.md \uAD00\uB9AC \uC601\uC5ED\uC744 \uCC3E\uC9C0 \uBABB\uD574 \uAC74\uB108\uB701\uB2C8\uB2E4."));
2150
+ return;
2151
+ }
2152
+ if (swapped === content) {
2153
+ console.log(pc4.green(" \u2713 AGENTS.md (\uAD00\uB9AC \uC601\uC5ED \uCD5C\uC2E0)"));
2154
+ return;
2155
+ }
2156
+ writeFileSync3(file, swapped, "utf-8");
2157
+ console.log(pc4.green(" \u2713 AGENTS.md (\uAD00\uB9AC \uC601\uC5ED \uAC31\uC2E0, \uC790\uC720 \uC601\uC5ED \uBCF4\uC874)"));
2158
+ }
2159
+ function installGuardHook(root) {
2160
+ const pkgPath = join4(root, "package.json");
2161
+ if (!existsSync2(pkgPath)) {
2162
+ console.log(pc4.dim(" \xB7 package.json \uC5C6\uC74C \u2014 pre-commit \uAC00\uB4DC \uC124\uCE58\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4."));
2163
+ return;
2164
+ }
2165
+ let pkgChanged = false;
2166
+ try {
2167
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2168
+ pkg.devDependencies ?? (pkg.devDependencies = {});
2169
+ pkg.scripts ?? (pkg.scripts = {});
2170
+ if (!pkg.devDependencies.husky) {
2171
+ pkg.devDependencies.husky = "^9.1.7";
2172
+ pkgChanged = true;
2173
+ }
2174
+ if (!pkg.scripts.prepare) {
2175
+ pkg.scripts.prepare = "husky";
2176
+ pkgChanged = true;
2177
+ } else if (!pkg.scripts.prepare.includes("husky")) {
2178
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
2179
+ pkgChanged = true;
2180
+ }
2181
+ if (pkgChanged) {
2182
+ writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2183
+ }
2184
+ } catch {
2185
+ console.log(pc4.yellow(" ! package.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 pre-commit \uAC00\uB4DC \uC124\uCE58\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4."));
2186
+ return;
2187
+ }
2188
+ const huskyDir = join4(root, ".husky");
2189
+ mkdirSync3(huskyDir, { recursive: true });
2190
+ const scriptFile = join4(huskyDir, "check-secrets.mjs");
2191
+ const scriptExisted = existsSync2(scriptFile);
2192
+ writeFileSync3(scriptFile, checkSecretsScript(), "utf-8");
2193
+ console.log(pc4.green(` \u2713 .husky/check-secrets.mjs (${scriptExisted ? "\uAC31\uC2E0" : "\uC0DD\uC131"})`));
2194
+ const hookFile = join4(huskyDir, "pre-commit");
2195
+ const body = huskyPreCommit();
2196
+ if (!existsSync2(hookFile)) {
2197
+ writeFileSync3(hookFile, body, "utf-8");
2198
+ console.log(pc4.green(" \u2713 .husky/pre-commit (\uC0DD\uC131)"));
2199
+ } else {
2200
+ const cur = readFileSync(hookFile, "utf-8");
2201
+ if (cur.includes("check-secrets.mjs")) {
2202
+ console.log(pc4.green(" \u2713 .husky/pre-commit (\uAC00\uB4DC \uC874\uC7AC)"));
2203
+ } else {
2204
+ const sep = cur.endsWith("\n") ? "" : "\n";
2205
+ writeFileSync3(hookFile, cur + sep + body, "utf-8");
2206
+ console.log(pc4.green(" \u2713 .husky/pre-commit (\uAC00\uB4DC \uB77C\uC778 \uCD94\uAC00)"));
2207
+ }
2208
+ }
2209
+ if (pkgChanged) {
2210
+ console.log(
2211
+ pc4.dim(" \xB7 husky\uB97C package.json\uC5D0 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4 \u2014 install \uD6C4 pre-commit \uD6C5\uC774 \uD65C\uC131\uD654\uB429\uB2C8\uB2E4.")
2212
+ );
2213
+ }
2214
+ }
2215
+ function readPkgName(root) {
2216
+ try {
2217
+ return JSON.parse(readFileSync(join4(root, "package.json"), "utf-8")).name ?? null;
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ }
2222
+
2223
+ // src/commands/doctor.ts
2224
+ var SDK_PACKAGES = [
2225
+ "@bstage-sdk/cli",
2226
+ "@bstage-sdk/core",
2227
+ "@bstage-sdk/react",
2228
+ "@bstage-sdk/design"
2229
+ ];
2230
+ var RECOMMENDED_DEPS = [
2231
+ {
2232
+ pkg: "@bstage-sdk/design",
2233
+ note: "bstage \uB514\uC790\uC778 \uD1A0\uD070 (\uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790)",
2234
+ why: "\uC5C6\uC73C\uBA74 \uC5D0\uC774\uC804\uD2B8\uAC00 hex/rgb\uB97C \uD558\uB4DC\uCF54\uB529\uD574 \uD50C\uB7AB\uD3FC \uD14C\uB9C8\xB7\uB2E4\uD06C\uBAA8\uB4DC\uC640 \uC5B4\uAE0B\uB09C\uB2E4"
2235
+ },
2236
+ {
2237
+ pkg: "@types/node",
2238
+ note: 'vite.config.ts \uD0C0\uC785 (tsconfig.node.json\uC774 `types: ["node"]` \uC120\uC5B8)',
2239
+ why: "\uC5C6\uC73C\uBA74 `npm run typecheck`(tsc -b)\uAC00 TS2688\uB85C \uAE68\uC9C4\uB2E4 \u2014 vite.config.ts\uAC00 process.cwd()\uB97C \uC4F4\uB2E4"
2240
+ }
2241
+ ];
2242
+ var EXPECTED_SCRIPTS = [
2243
+ {
2244
+ name: "typecheck",
2245
+ command: "tsc -b",
2246
+ why: "`bstage build`\uB294 tsc\uB97C \uD0C0\uC9C0 \uC54A\uC544, \uC774 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC73C\uBA74 \uC5C6\uB294 \uBC88\uC5ED \uD0A4\xB7\uD0C0\uC785 \uC624\uB958\uAC00 \uC870\uC6A9\uD788 \uD1B5\uACFC\uD55C\uB2E4"
2247
+ }
2248
+ ];
2249
+ var EXPECTED_FILES = [
2250
+ { path: "tsconfig.app.json", note: "tsconfig \uBD84\uB9AC (app)" },
2251
+ { path: "tsconfig.node.json", note: "tsconfig \uBD84\uB9AC (node)" },
2252
+ { path: "eslint.config.js", note: "ESLint flat config" },
2253
+ { path: "src/vite-env.d.ts", note: "Vite client \uD0C0\uC785" },
2254
+ { path: "src/App.tsx", note: "dev \uD654\uBA74 \uBD84\uB9AC (\uC5C6\uC73C\uBA74 lint \uC2E4\uD328)" }
2255
+ ];
2256
+ async function doctorCommand(options) {
2257
+ const root = process.cwd();
2258
+ const report = await diagnose(root);
2259
+ if (options.json) {
2260
+ console.log(JSON.stringify(report, null, 2));
2261
+ return;
2262
+ }
2263
+ printReport(report);
2264
+ await autoSyncSkills(report);
2265
+ }
2266
+ async function autoSyncSkills(report) {
2267
+ if (!report.isBstageProject) return;
2268
+ if (!report.skills.some((s) => s.status !== "ok")) return;
2269
+ await skillsInstallCommand({ dir: ".claude/skills" });
2270
+ }
2271
+ async function diagnose(root) {
2272
+ let pkgJson = null;
2273
+ try {
2274
+ pkgJson = JSON.parse(await readFile2(join5(root, "package.json"), "utf-8"));
2275
+ } catch {
2276
+ }
2277
+ const deps = {
2278
+ ...pkgJson?.dependencies,
2279
+ ...pkgJson?.devDependencies,
2280
+ ...pkgJson?.peerDependencies,
2281
+ ...pkgJson?.optionalDependencies
2282
+ };
2283
+ const isBstageProject = SDK_PACKAGES.some((p2) => p2 in deps);
2284
+ const installed = /* @__PURE__ */ new Map();
2285
+ for (const pkg of SDK_PACKAGES) {
2286
+ installed.set(pkg, await readInstalledVersion(root, pkg) ?? cleanRange(deps[pkg]));
2287
+ }
2288
+ const relevantPackages = SDK_PACKAGES.filter((pkg) => installed.get(pkg) !== null || pkg in deps);
2289
+ const latest = /* @__PURE__ */ new Map();
2290
+ let latestChecked = false;
2291
+ if (isBstageProject) {
2292
+ latestChecked = true;
2293
+ await Promise.all(
2294
+ relevantPackages.map(async (pkg) => {
2295
+ latest.set(pkg, await fetchLatestVersion2(pkg).catch(() => null));
2296
+ })
2297
+ );
2298
+ }
2299
+ const versions = relevantPackages.map((pkg) => {
2300
+ const inst = installed.get(pkg) ?? null;
2301
+ const lat = latest.get(pkg) ?? null;
2302
+ return {
2303
+ pkg,
2304
+ installed: inst,
2305
+ latest: lat,
2306
+ outdated: !!inst && !!lat && compareSemver(inst, lat) < 0
2307
+ };
2308
+ });
2309
+ const files = EXPECTED_FILES.map((f) => ({
2310
+ path: f.path,
2311
+ note: f.note,
2312
+ present: existsSync3(join5(root, f.path))
2313
+ }));
2314
+ const isPnpmProject = existsSync3(join5(root, "pnpm-lock.yaml"));
2315
+ let pnpmWorkspace2 = {
2316
+ relevant: false,
2317
+ allowBuildsDeclared: false,
2318
+ packagesDeclared: false,
2319
+ filePresent: false
2320
+ };
2321
+ if (isPnpmProject) {
2322
+ let yaml = null;
2323
+ try {
2324
+ yaml = await readFile2(join5(root, "pnpm-workspace.yaml"), "utf-8");
2325
+ } catch {
2326
+ }
2327
+ pnpmWorkspace2 = {
2328
+ relevant: true,
2329
+ filePresent: yaml !== null,
2330
+ allowBuildsDeclared: yaml !== null && /^allowBuilds:/m.test(yaml),
2331
+ packagesDeclared: yaml !== null && /^packages:/m.test(yaml)
2332
+ };
2333
+ }
2334
+ const scripts = EXPECTED_SCRIPTS.map((sc) => ({
2335
+ ...sc,
2336
+ expected: sc.command,
2337
+ actual: pkgJson?.scripts?.[sc.name] ?? null
2338
+ }));
2339
+ const recommendedDeps = RECOMMENDED_DEPS.map((d) => ({
2340
+ ...d,
2341
+ declared: d.pkg in deps
2342
+ }));
2343
+ const resolveCache = /* @__PURE__ */ new Map();
2344
+ const migrations = await readApplicableMigrations(async (pkg) => {
2345
+ const cached = resolveCache.get(pkg);
2346
+ if (cached) return cached;
2347
+ const version = installed.get(pkg) ?? await readInstalledVersion(root, pkg) ?? cleanRange(deps[pkg]);
2348
+ const resolved = { version, relevant: version !== null || pkg in deps };
2349
+ resolveCache.set(pkg, resolved);
2350
+ return resolved;
2351
+ });
2352
+ const agentsMd2 = await diagnoseAgentsMd(root, pkgJson?.name ?? null);
2353
+ const skills2 = await diagnoseSkills(root);
2354
+ return {
2355
+ project: pkgJson?.name ?? null,
2356
+ isBstageProject,
2357
+ versions,
2358
+ files,
2359
+ layout: await diagnoseLayout(root),
2360
+ pnpmWorkspace: pnpmWorkspace2,
2361
+ scripts,
2362
+ recommendedDeps,
2363
+ migrations,
2364
+ agentsMd: agentsMd2,
2365
+ skills: skills2,
2366
+ latestChecked
2367
+ };
2368
+ }
2369
+ async function diagnoseLayout(root) {
2370
+ const legacyRoot = join5(root, "src", "templates");
2371
+ let legacyTemplates = [];
2372
+ let legacyPresent = false;
2373
+ try {
2374
+ const dirents = await readdir2(legacyRoot, { withFileTypes: true });
2375
+ legacyPresent = true;
2376
+ legacyTemplates = dirents.filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort();
2377
+ } catch {
2378
+ }
2379
+ return {
2380
+ legacyPresent,
2381
+ legacyTemplates,
2382
+ pagesPresent: existsSync3(join5(root, "src", "pages")),
2383
+ slotsPresent: existsSync3(join5(root, "src", "slots"))
2384
+ };
2385
+ }
2386
+ async function diagnoseSkills(root) {
2387
+ const target = resolveProjectTarget(root);
2388
+ return Promise.all(
2389
+ BSTAGE_SKILLS.map(async (skill) => {
2390
+ let installed;
2391
+ try {
2392
+ installed = await readFile2(join5(root, ".claude", "skills", skill.name, "SKILL.md"), "utf-8");
2393
+ } catch {
2394
+ return { name: skill.name, status: "missing" };
2395
+ }
2396
+ return { name: skill.name, status: installed === skill.content(target) ? "ok" : "stale" };
2397
+ })
2398
+ );
2399
+ }
2400
+ async function diagnoseAgentsMd(root, pkgName) {
2401
+ let content;
2402
+ try {
2403
+ content = await readFile2(join5(root, "AGENTS.md"), "utf-8");
2404
+ } catch {
2405
+ return { status: "missing", managedBlock: null };
2406
+ }
2407
+ const status = agentsMdStatus(content);
2408
+ if (status === "ok") return { status, managedBlock: null };
2409
+ const parsed = parseAgentsIdentity(content);
2410
+ const projectName = parsed.projectName ?? pkgName ?? "custom-templates";
2411
+ const space = parsed.space ?? projectName.split("-custom-templates-")[0] ?? projectName;
2412
+ const target = resolveProjectTarget(root);
2413
+ return { status, managedBlock: renderManagedBlock({ space, projectName, target }) };
2414
+ }
2415
+ async function readInstalledVersion(root, pkg) {
2416
+ try {
2417
+ const raw = await readFile2(join5(root, "node_modules", pkg, "package.json"), "utf-8");
2418
+ const parsed = JSON.parse(raw);
2419
+ return parsed.version ?? null;
2420
+ } catch {
2421
+ return null;
2422
+ }
2423
+ }
2424
+ async function fetchLatestVersion2(packageName) {
2425
+ const res = await fetch(`https://registry.npmjs.org/${packageName}`, {
2426
+ headers: { Accept: "application/json" }
2427
+ });
2428
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2429
+ const data = await res.json();
2430
+ const latest = data["dist-tags"]?.latest;
2431
+ if (!latest) throw new Error("latest \uC5C6\uC74C");
2432
+ return latest;
2433
+ }
2434
+ async function readApplicableMigrations(resolve4) {
2435
+ const md = await readBundledMigrationDoc();
2436
+ if (md === null) return [];
2437
+ return selectApplicableMigrations(parseMigrationDoc(md), resolve4);
2438
+ }
2439
+ function parseMigrationDoc(md) {
2440
+ const lines = md.split("\n");
2441
+ const sectionRe = /^##\s+(?:→\s*)?(.+?)\s*$/;
2442
+ const itemRe = /^###\s+(.+?)\s*$/;
2443
+ const entries = [];
2444
+ let current = null;
2445
+ for (const line of lines) {
2446
+ const sec = sectionRe.exec(line);
2447
+ if (sec) {
2448
+ const requirements = parseMigrationRequirements(sec[1]);
2449
+ current = requirements.length > 0 ? { label: sec[1].trim(), requirements, items: [] } : null;
2450
+ if (current) entries.push(current);
2451
+ continue;
2452
+ }
2453
+ if (current) {
2454
+ const item = itemRe.exec(line);
2455
+ if (item) {
2456
+ current.items.push(item[1].replace(/\s*[—–-]\s*`.*$/, "").trim());
2457
+ }
2458
+ }
2459
+ }
2460
+ return entries;
2461
+ }
2462
+ async function selectApplicableMigrations(entries, resolve4) {
2463
+ const applicable = [];
2464
+ for (const e of entries) {
2465
+ let applies = false;
2466
+ for (const req of e.requirements) {
2467
+ const { version, relevant } = await resolve4(req.pkg);
2468
+ if (!relevant) continue;
2469
+ if (version === null || compareSemver(version, req.version) < 0) {
2470
+ applies = true;
2471
+ break;
2472
+ }
2473
+ }
2474
+ if (applies) applicable.push(e);
2475
+ }
2476
+ return applicable;
2477
+ }
2478
+ function parseMigrationRequirements(text2) {
2479
+ const reqs = [];
2480
+ const re = /(?:([a-z][a-z-]*)\s+)?(\d+\.\d+\.\d+)/g;
2481
+ let m;
2482
+ while ((m = re.exec(text2)) !== null) {
2483
+ reqs.push({ pkg: normalizeMigrationPkg(m[1]), version: m[2] });
2484
+ }
2485
+ return reqs;
2486
+ }
2487
+ function normalizeMigrationPkg(short) {
2488
+ if (!short) return "@bstage-sdk/core";
2489
+ return short.startsWith("@") ? short : `@bstage-sdk/${short}`;
2490
+ }
2491
+ async function readBundledMigrationDoc() {
2492
+ const here = dirname2(fileURLToPath(import.meta.url));
2493
+ const candidates = [
2494
+ join5(here, "MIGRATION.md"),
2495
+ // 배포물: dist/ 옆 (cli/dist/MIGRATION.md)
2496
+ join5(here, "..", "..", "..", "docs", "MIGRATION.md"),
2497
+ // dev(dist): cli/dist → repo/docs
2498
+ join5(here, "..", "..", "..", "..", "docs", "MIGRATION.md")
2499
+ // dev(src): cli/src/commands → repo/docs
2500
+ ];
2501
+ for (const path of candidates) {
2502
+ try {
2503
+ return await readFile2(path, "utf-8");
2504
+ } catch {
2505
+ }
2506
+ }
2507
+ return null;
2508
+ }
2509
+ function cleanRange(spec) {
2510
+ if (!spec) return null;
2511
+ const m = /(\d+\.\d+\.\d+)/.exec(spec);
2512
+ return m ? m[1] : null;
2513
+ }
2514
+ function compareSemver(a, b) {
2515
+ const pa = a.split(".").map((n) => parseInt(n, 10));
2516
+ const pb = b.split(".").map((n) => parseInt(n, 10));
2517
+ for (let i = 0; i < 3; i++) {
2518
+ const d = (pa[i] || 0) - (pb[i] || 0);
2519
+ if (d !== 0) return d < 0 ? -1 : 1;
2520
+ }
2521
+ return 0;
2522
+ }
2523
+ function printReport(r) {
2524
+ console.log("");
2525
+ console.log(`${pc5.cyan(pc5.bold("bstage doctor"))} ${pc5.dim(`\u2014 ${r.project ?? "(\uC774\uB984 \uC5C6\uC74C)"}`)}`);
2526
+ console.log("");
2527
+ if (!r.isBstageProject) {
2528
+ console.log(pc5.yellow("\uC774 \uB514\uB809\uD1A0\uB9AC\uB294 bstage \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2CC \uAC83 \uAC19\uC2B5\uB2C8\uB2E4."));
2529
+ console.log(pc5.dim(" package.json\uC5D0 @bstage-sdk/* \uC758\uC874\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2530
+ return;
2531
+ }
2532
+ console.log(pc5.bold("\u{1F4E6} SDK \uBC84\uC804"));
2533
+ if (!r.latestChecked) {
2534
+ console.log(pc5.dim(" \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C\uB97C \uAC74\uB108\uB6F0\uC5B4 \uC124\uCE58 \uBC84\uC804\uB9CC \uD45C\uC2DC\uD569\uB2C8\uB2E4."));
2535
+ }
2536
+ const nameWidth = Math.max(12, ...r.versions.map((v) => v.pkg.replace("@bstage-sdk/", "").length));
2537
+ for (const v of r.versions) {
2538
+ const name = v.pkg.replace("@bstage-sdk/", "").padEnd(nameWidth);
2539
+ const inst = v.installed ?? "?";
2540
+ if (v.latest && v.installed) {
2541
+ const arrow = v.outdated ? pc5.yellow(`\u2192 ${v.latest} (\uB4A4\uCC98\uC9D0)`) : pc5.green("(\uCD5C\uC2E0)");
2542
+ console.log(` ${name} ${inst.padEnd(8)} ${arrow}`);
2543
+ } else {
2544
+ console.log(` ${name} ${inst}`);
2545
+ }
2546
+ }
2547
+ console.log("");
2548
+ console.log(pc5.bold("\u{1F4C1} init \uAE30\uBCF8 \uD30C\uC77C (\uCC38\uACE0)"));
2549
+ const absent = r.files.filter((f) => !f.present);
2550
+ if (absent.length === 0) {
2551
+ console.log(pc5.green(" \u2713 init \uAE30\uBCF8 \uAD6C\uC131\uACFC \uB3D9\uC77C"));
2552
+ } else {
2553
+ console.log(pc5.dim(" init \uAE30\uBCF8\uAC12\uACFC\uC758 \uCC28\uC774\uC77C \uBFD0 \u2014 \uD504\uB85C\uC81D\uD2B8\uAC00 \uB2E4\uB974\uAC8C \uCC98\uB9AC \uC911\uC774\uBA74 \uBB34\uC2DC\uD558\uC138\uC694."));
2554
+ for (const f of r.files) {
2555
+ if (f.present) console.log(` ${pc5.green("\u2713")} ${f.path}`);
2556
+ else console.log(` ${pc5.dim("\xB7")} ${f.path.padEnd(20)} ${pc5.dim(`\uC5C6\uC74C (${f.note})`)}`);
2557
+ }
2558
+ }
2559
+ console.log("");
2560
+ console.log(pc5.bold("\u{1F5C2} \uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC"));
2561
+ if (r.layout.legacyPresent) {
2562
+ console.log(` ${pc5.yellow("\u2192")} ${pc5.yellow("src/templates/ \uAC00 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4 (\uC61B \uAD6C\uC870)")}`);
2563
+ console.log(pc5.dim(" \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C \uACBD\uB85C\uAC00 \uD3F4\uB354 \uAD6C\uC870\uB97C \uB530\uB77C\uAC00\uB3C4\uB85D \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4."));
2564
+ console.log(pc5.dim(" \uD398\uC774\uC9C0 \u2192 src/pages/{\uACBD\uB85C}/ \xB7 \uC704\uC82F \u2192 src/slots/{\uC544\uBB34\uC774\uB984}/"));
2565
+ if (r.layout.legacyTemplates.length > 0) {
2566
+ console.log(pc5.dim(` \uC62E\uACA8\uC57C \uD560 \uD15C\uD50C\uB9BF: ${r.layout.legacyTemplates.join(", ")}`));
2567
+ }
2568
+ console.log(pc5.dim(' \uBCC0\uD658: \uC5D0\uC774\uC804\uD2B8\uC5D0\uAC8C "bstage \uB9C8\uC774\uADF8\uB808\uC774\uC158" \uC694\uCCAD'));
2569
+ } else if (!r.layout.pagesPresent && !r.layout.slotsPresent) {
2570
+ console.log(` ${pc5.dim("\xB7")} src/pages/ \xB7 src/slots/ \uB458 \uB2E4 \uC5C6\uC74C`);
2571
+ } else {
2572
+ console.log(pc5.green(" \u2713 src/pages/ \xB7 src/slots/ \uAD6C\uC870"));
2573
+ }
2574
+ console.log("");
2575
+ if (r.pnpmWorkspace.relevant) {
2576
+ console.log(pc5.bold("\u{1F4E6} pnpm workspace \uC124\uC815"));
2577
+ if (r.pnpmWorkspace.allowBuildsDeclared) {
2578
+ console.log(pc5.green(" \u2713 pnpm-workspace.yaml\uC5D0 allowBuilds \uC120\uC5B8\uB428"));
2579
+ } else {
2580
+ console.log(` ${pc5.yellow("\u2192")} ${pc5.yellow("pnpm-workspace.yaml\uC5D0 allowBuilds \uC5C6\uC74C")}`);
2581
+ console.log(
2582
+ pc5.dim(" pnpm\uC740 \uC758\uC874\uC131 build script\uB97C \uAE30\uBCF8 \uCC28\uB2E8\uD558\uACE0, pnpm 11\uC740 \uADF8\uAC83\uC744 \uC5D0\uB7EC\uB85C \uB0B8\uB2E4")
2583
+ );
2584
+ console.log(
2585
+ pc5.dim(" \u2192 `pnpm install`\uC774 ERR_PNPM_IGNORED_BUILDS\uB85C \uC2E4\uD328\uD558\uACE0 `pnpm dev`\uB3C4 \uC8FD\uB294\uB2E4")
2586
+ );
2587
+ console.log(pc5.dim(" \uCD94\uAC00: pnpm-workspace.yaml\uC5D0 `allowBuilds:` + `esbuild: true`"));
2588
+ console.log(pc5.dim(" \uB610\uB294 `pnpm approve-builds`\uB85C \uB300\uD654\uC2DD \uC120\uD0DD"));
2589
+ }
2590
+ if (r.pnpmWorkspace.filePresent && !r.pnpmWorkspace.packagesDeclared) {
2591
+ console.log(` ${pc5.yellow("\u2192")} ${pc5.yellow("pnpm-workspace.yaml\uC5D0 packages \uC5C6\uC74C")}`);
2592
+ console.log(
2593
+ pc5.dim(" pnpm 9\uB294 \uC774 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 workspace \uB8E8\uD2B8\uB85C \uBCF4\uACE0 packages\uB97C \uC694\uAD6C\uD55C\uB2E4")
2594
+ );
2595
+ console.log(pc5.dim(" \u2192 pnpm 9\uC5D0\uC11C `ERROR packages field missing or empty`\uB85C \uC8FD\uB294\uB2E4"));
2596
+ console.log(pc5.dim(" \uCD94\uAC00: pnpm-workspace.yaml \uB9E8 \uC704\uC5D0 `packages:` + `- '.'`"));
2597
+ } else if (r.pnpmWorkspace.packagesDeclared) {
2598
+ console.log(pc5.green(" \u2713 pnpm-workspace.yaml\uC5D0 packages \uC120\uC5B8\uB428"));
2599
+ }
2600
+ console.log("");
2601
+ }
2602
+ console.log(pc5.bold("\u{1F6E0} package.json \uC2A4\uD06C\uB9BD\uD2B8"));
2603
+ const scriptIssues = r.scripts.filter((sc) => sc.actual !== sc.expected);
2604
+ if (scriptIssues.length === 0) {
2605
+ console.log(pc5.green(" \u2713 init \uAE30\uBCF8 \uAD6C\uC131\uACFC \uB3D9\uC77C"));
2606
+ } else {
2607
+ for (const sc of r.scripts) {
2608
+ if (sc.actual === sc.expected) {
2609
+ console.log(` ${pc5.green("\u2713")} ${sc.name}`);
2610
+ continue;
2611
+ }
2612
+ if (sc.actual === null) {
2613
+ console.log(` ${pc5.yellow("\u2192")} ${sc.name} ${pc5.yellow("\uC5C6\uC74C")}`);
2614
+ } else {
2615
+ console.log(
2616
+ ` ${pc5.yellow("\u2192")} ${sc.name} ${pc5.yellow("\uB2E4\uB984")} ${pc5.dim(`\u2014 \uD604\uC7AC \`${sc.actual}\``)}`
2617
+ );
2618
+ if (sc.actual.includes("--noEmit") && !sc.actual.includes("-p ")) {
2619
+ console.log(
2620
+ pc5.dim(
2621
+ " \uC8FC\uC758: solution-style tsconfig\uC5D0\uC11C `tsc --noEmit`\uC740 \uC544\uBB34 \uD30C\uC77C\uB3C4 \uAC80\uC0AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
2622
+ )
2623
+ );
2624
+ }
2625
+ }
2626
+ console.log(pc5.dim(` ${sc.why}`));
2627
+ console.log(pc5.dim(` \uAD8C\uC7A5: "${sc.name}": "${sc.expected}"`));
2628
+ }
2629
+ }
2630
+ console.log("");
2631
+ console.log(pc5.bold("\u{1F9F1} \uAD8C\uC7A5 \uC758\uC874\uC131"));
2632
+ if (r.recommendedDeps.every((d) => d.declared)) {
2633
+ console.log(pc5.green(" \u2713 \uBAA8\uB450 \uC120\uC5B8\uB428"));
2634
+ } else {
2635
+ for (const d of r.recommendedDeps) {
2636
+ if (d.declared) {
2637
+ console.log(` ${pc5.green("\u2713")} ${d.pkg}`);
2638
+ continue;
2639
+ }
2640
+ console.log(` ${pc5.yellow("\u2192")} ${d.pkg} ${pc5.yellow("\uBBF8\uC120\uC5B8")} ${pc5.dim(`\u2014 ${d.note}`)}`);
2641
+ console.log(pc5.dim(` ${d.why}`));
2642
+ console.log(pc5.dim(` \uCD94\uAC00: npm i ${d.pkg} (\uD504\uB85C\uC81D\uD2B8 \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800\uB85C)`));
2643
+ }
2644
+ }
2645
+ console.log("");
2646
+ console.log(pc5.bold("\u{1F4D6} \uC801\uC6A9 \uAC00\uB2A5\uD55C \uB9C8\uC774\uADF8\uB808\uC774\uC158"));
2647
+ if (r.migrations.length === 0) {
2648
+ console.log(pc5.dim(" \uD574\uB2F9 \uD56D\uBAA9 \uC5C6\uC74C."));
2649
+ } else {
2650
+ for (const m of r.migrations) {
2651
+ console.log(` ${pc5.cyan(`\u2192 ${m.label}`)}`);
2652
+ for (const item of m.items) console.log(` \u2022 ${item}`);
2653
+ }
2654
+ }
2655
+ console.log("");
2656
+ console.log(pc5.bold("\u{1F4DD} AGENTS.md"));
2657
+ switch (r.agentsMd.status) {
2658
+ case "ok":
2659
+ console.log(pc5.green(" \u2713 \uAD00\uB9AC \uC601\uC5ED \uCD5C\uC2E0"));
2660
+ break;
2661
+ case "missing":
2662
+ console.log(pc5.dim(" \xB7 \uD30C\uC77C \uC5C6\uC74C \u2014 bstage \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74 init/\uB9C8\uC774\uADF8\uB808\uC774\uC158\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694."));
2663
+ break;
2664
+ case "stale":
2665
+ console.log(
2666
+ pc5.yellow(" \u2192 \uAD00\uB9AC \uC601\uC5ED\uC774 \uC624\uB798\uB428") + pc5.dim(" \u2014 `bstage skills install`\uB85C \uAC31\uC2E0(\uC790\uC720 \uC601\uC5ED\uC740 \uBCF4\uC874).")
2667
+ );
2668
+ break;
2669
+ case "legacy":
2670
+ console.log(
2671
+ pc5.yellow(" \u2192 \uB808\uAC70\uC2DC \uD3EC\uB9F7(\uAD00\uB9AC \uC601\uC5ED \uB9C8\uCEE4 \uC5C6\uC74C)") + pc5.dim(
2672
+ " \u2014 `bstage skills install`\uB85C \uC2A4\uD0AC\uC744 \uBA3C\uC800 \uC124\uCE58\uD55C \uB4A4, `bstage-migrate` \uC2A4\uD0AC\uB85C reconcile(SDK \uC601\uC5ED \uAD50\uCCB4 + \uC0AC\uC6A9\uC790 \uB0B4\uC6A9 \uBCF4\uC874)."
2673
+ )
2674
+ );
2675
+ break;
2676
+ }
2677
+ console.log("");
2678
+ console.log(pc5.bold("\u{1F9E9} \uC5D0\uC774\uC804\uD2B8 \uC2A4\uD0AC (.claude/skills/)"));
2679
+ const skillsNeedWork = r.skills.filter((s) => s.status !== "ok");
2680
+ if (skillsNeedWork.length === 0) {
2681
+ console.log(pc5.green(" \u2713 \uBAA8\uB4E0 \uC2A4\uD0AC \uCD5C\uC2E0"));
2682
+ } else {
2683
+ for (const s of r.skills) {
2684
+ if (s.status === "ok") console.log(` ${pc5.green("\u2713")} ${s.name}`);
2685
+ else if (s.status === "missing")
2686
+ console.log(` ${pc5.yellow("\u2192")} ${s.name.padEnd(16)} ${pc5.yellow("\uBBF8\uC124\uCE58")}`);
2687
+ else console.log(` ${pc5.yellow("\u2192")} ${s.name.padEnd(16)} ${pc5.yellow("\uAD6C\uBC84\uC804/\uC218\uC815\uB428")}`);
2688
+ }
2689
+ console.log(
2690
+ pc5.dim(
2691
+ " \uC544\uB798\uC5D0\uC11C \uC790\uB3D9 \uB3D9\uAE30\uD654\uD569\uB2C8\uB2E4 \u2014 bstage-* \uC2A4\uD0AC\uB9CC \uB36E\uC5B4\uC4F0\uBA70, \uADF8 \uC678 \uC0AC\uC6A9\uC790 \uC2A4\uD0AC\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
2692
+ )
2693
+ );
2694
+ }
2695
+ console.log("");
2696
+ const agentsNeedsWork = r.agentsMd.status === "legacy" || r.agentsMd.status === "stale";
2697
+ const hasWork = r.versions.some((v) => v.outdated) || r.migrations.length > 0 || agentsNeedsWork;
2698
+ if (hasWork) {
2699
+ console.log(
2700
+ pc5.dim(
2701
+ "\uB2E4\uC74C \uB2E8\uACC4: \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC2A4\uD0AC\uC744 \uC2E4\uD589\uD574 \uC704 \uD56D\uBAA9\uC744 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uCDB0 \uC801\uC6A9\uD558\uC138\uC694.\n \uC0C1\uC138 \uAC00\uC774\uB4DC: SDK \uBB38\uC11C\uC758 MIGRATION.md (\uC704 \uD56D\uBAA9\uBCC4 \uC801\uC6A9 \uC808\uCC28)"
2702
+ )
2703
+ );
2704
+ } else {
2705
+ console.log(pc5.green("\u2713 \uCD5C\uC2E0 \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC774 \uD544\uC694 \uC5C6\uC2B5\uB2C8\uB2E4."));
2706
+ if (absent.length > 0) {
2707
+ console.log(pc5.dim(" (\uC704 init \uAE30\uBCF8 \uD30C\uC77C \uCC28\uC774\uB294 \uC120\uD0DD \uC0AC\uD56D\uC785\uB2C8\uB2E4.)"));
2708
+ }
2709
+ if (r.recommendedDeps.some((d) => !d.declared)) {
2710
+ console.log(pc5.dim(" (\uC704 \uAD8C\uC7A5 \uC758\uC874\uC131\uC740 \uBC84\uC804\uACFC \uBB34\uAD00\uD569\uB2C8\uB2E4 \u2014 \uC4F8 \uAC70\uBA74 \uB530\uB85C \uCD94\uAC00\uD558\uC138\uC694.)"));
2711
+ }
2712
+ }
2713
+ console.log("");
2714
+ }
2715
+
2716
+ // src/commands/docs.ts
2717
+ import { existsSync as existsSync4, readdirSync } from "fs";
2718
+ import { join as join6 } from "path";
2719
+ import pc6 from "picocolors";
2720
+ var DOC_ORDER = [
2721
+ ["GETTING_STARTED.md", "\uBE60\uB978 \uC2DC\uC791 \uAC00\uC774\uB4DC"],
2722
+ ["API_REFERENCE.md", "createTemplate, hooks, BstageClient, PlatformBridge API"],
2723
+ ["I18N.md", "\uB2E4\uAD6D\uC5B4 \uD6C5 + bstage i18n pull (\uBC88\uC5ED \uCF54\uB4DC\uC820) \uC6CC\uD06C\uD50C\uB85C"],
2724
+ ["DESIGN_TOKENS.md", "\uB514\uC790\uC778 \uD1A0\uD070 (\uC0C9\xB7\uD0C0\uC774\uD3EC\xB7\uADF8\uB9BC\uC790) \uC0AC\uC6A9 \uAC00\uC774\uB4DC (user/admin)"],
2725
+ ["SLOT_PROTOCOL.md", "\uC2AC\uB86F \uC2DC\uC2A4\uD15C v1 \uD504\uB85C\uD1A0\uCF5C (3-part \uD0A4\xB7context \uB2E8\uBC29\uD5A5)"],
2726
+ ["SLOT_CATALOG_V2.md", "v2 \uC2AC\uB86F \uCE74\uD0C8\uB85C\uADF8 (\uD0A4\xB7anchor\xB7context \uACC4\uC57D)"],
2727
+ ["SLOT_CATALOG.md", "\uC2AC\uB86F \uBAA9\uB85D, \uC774\uBCA4\uD2B8 \uC778\uD130\uD398\uC774\uC2A4, resourceId \uC758\uBBF8"],
2728
+ ["SLOT_SYSTEM.md", "\uC2AC\uB86F \uC2DC\uC2A4\uD15C \uC124\uACC4, \uB7F0\uD0C0\uC784 \uD750\uB984"],
2729
+ ["BUILD_SYSTEM.md", "\uBE4C\uB4DC \uD30C\uC774\uD504\uB77C\uC778, \uC0B0\uCD9C\uBB3C \uACBD\uB85C \uADDC\uCE59"],
2730
+ ["DEV_SERVER.md", "\uB85C\uCEEC \uAC1C\uBC1C \uC11C\uBC84, \uC778\uC99D \uD504\uB85D\uC2DC"],
2731
+ ["INIT.md", "bstage init \uBA85\uB839 \uC0C1\uC138"],
2732
+ ["MIGRATION.md", "\uBC84\uC804 \uC0AC\uC774 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uAC00\uC774\uB4DC (bstage doctor \uC5F0\uACC4)"],
2733
+ ["SDK_ARCHITECTURE.md", "\uD328\uD0A4\uC9C0 \uAD6C\uC870, \uC124\uACC4 \uACB0\uC815"]
2734
+ ];
2735
+ async function docsCommand() {
2736
+ const docsDir = join6(process.cwd(), "node_modules", "@bstage-sdk", "core", "docs");
2737
+ if (!existsSync4(docsDir)) {
2738
+ console.log(pc6.yellow("SDK \uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2739
+ console.log(pc6.dim(" @bstage-sdk/core\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694 (npm install \uD6C4 \uC7AC\uC2DC\uB3C4)."));
2740
+ return;
2741
+ }
2742
+ const present = new Set(readdirSync(docsDir).filter((f) => f.endsWith(".md")));
2743
+ const ordered = DOC_ORDER.filter(([f]) => present.has(f));
2744
+ const known = new Set(DOC_ORDER.map(([f]) => f));
2745
+ const extras = [...present].filter((f) => !known.has(f)).sort();
2746
+ console.log("");
2747
+ console.log(pc6.bold("bstage SDK \uBB38\uC11C"));
2748
+ console.log(pc6.dim(` ${docsDir}`));
2749
+ console.log("");
2750
+ for (const [file, desc] of ordered) {
2751
+ console.log(` ${pc6.cyan(file.padEnd(22))} ${pc6.dim(desc)}`);
2752
+ }
2753
+ for (const file of extras) {
2754
+ console.log(` ${pc6.cyan(file.padEnd(22))}`);
2755
+ }
2756
+ console.log("");
2757
+ console.log(
2758
+ pc6.dim("\uD30C\uC77C\uC744 \uC5F4\uC5B4 \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694. \uCEE4\uB9E8\uB4DC \uB3C4\uC6C0\uB9D0\uC740 `bstage <command> --help`.")
2759
+ );
2760
+ console.log("");
2761
+ }
2762
+
2763
+ // src/index.ts
2764
+ function readVersion() {
2765
+ try {
2766
+ const here = dirname3(fileURLToPath2(import.meta.url));
2767
+ return JSON.parse(readFileSync2(join7(here, "..", "package.json"), "utf-8")).version || "0.0.0";
2768
+ } catch {
2769
+ return "0.0.0";
2770
+ }
2771
+ }
2772
+ var program = new Command().name("bstage").description("b.stage \uC11C\uB4DC\uD30C\uD2F0 \uD15C\uD50C\uB9BF SDK CLI \u2014 \uD15C\uD50C\uB9BF \uAC1C\uBC1C\xB7\uBE4C\uB4DC\xB7\uB85C\uCEEC \uC11C\uBC84\xB7\uB9C8\uC774\uADF8\uB808\uC774\uC158").version(readVersion(), "-v, --version", "\uBC84\uC804 \uCD9C\uB825");
2773
+ program.command("init").description("\uC0C8 bstage \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8 \uC0DD\uC131").option("-y, --yes", "\uAE30\uBCF8\uAC12\uC73C\uB85C \uBE44\uC778\uD130\uB799\uD2F0\uBE0C \uC2E4\uD589").option("--space <space>", "Space \uC774\uB984").option("--target <target>", "\uC2A4\uCE90\uD3F4\uB4DC \uB300\uC0C1 (user, admin)", "user").option("--template <template>", "\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF \uC774\uB984 (\uAE30\uBCF8: {space}-hello)").option("--phase <phase>", "\uBC30\uD3EC \uD658\uACBD (dev, qa, real, sandbox)", "sandbox").option("--pm <pm>", "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 (npm, pnpm)", "npm").action(initCommand);
2774
+ program.command("build").description("\uD15C\uD50C\uB9BF\uC744 IIFE \uBC88\uB4E4\uB85C \uBE4C\uB4DC (\uD398\uC774\uC9C0\uB294 \uACBD\uB85C\uBCC4, \uC704\uC82F\uC740 \uC2AC\uB86F\uBCC4 \uB514\uB809\uD1A0\uB9AC)").action(buildCommand);
2775
+ program.command("docs").description("\uC124\uCE58\uB41C SDK \uBB38\uC11C \uBAA9\uB85D\xB7\uACBD\uB85C \uCD9C\uB825").action(docsCommand);
2776
+ program.command("doctor").description("SDK \uBC84\uC804\xB7\uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uB4DC\uB9AC\uD504\uD2B8 \uC9C4\uB2E8 (\uAC10\uC9C0\uB9CC, \uD30C\uC77C \uC218\uC815 \uC5C6\uC74C)").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825 (\uC2A4\uD0AC\xB7CI\uC6A9)").action(doctorCommand);
2777
+ program.command("dev").description("\uC778\uC99D \uD504\uB85D\uC2DC \uD3EC\uD568 \uB85C\uCEEC \uAC1C\uBC1C \uC11C\uBC84 \uC2E4\uD589").option("-p, --port <port>", "Dev server port", "5173").option(
2778
+ "--phase <phase>",
2779
+ "Target phase (dev, qa, real, sandbox). \uC0DD\uB7B5 \uC2DC .env\uC758 VITE_BSTAGE_PHASE, \uADF8\uAC83\uB3C4 \uC5C6\uC73C\uBA74 sandbox"
2780
+ ).action(devCommand);
2781
+ var skills = program.command("skills").description("\uC5D0\uC774\uC804\uD2B8\uC6A9 \uC2A4\uD0AC \uC720\uD2F8");
2782
+ skills.command("install").description("\uC5D0\uC774\uC804\uD2B8\uC6A9 \uC2A4\uD0AC\uC744 .claude/skills/\uC5D0 \uC124\uCE58\xB7\uB3D9\uAE30\uD654 (\uAE30\uC874 \uD504\uB85C\uC81D\uD2B8\uC6A9)").option("--dir <dir>", "\uC124\uCE58 \uC704\uCE58", ".claude/skills").action(skillsInstallCommand);
2783
+ var i18n = program.command("i18n").description("\uB2E4\uAD6D\uC5B4(\uBC88\uC5ED) \uC720\uD2F8");
2784
+ i18n.command("pull").description("\uD50C\uB7AB\uD3FC \uBC88\uC5ED \uC0AC\uC804\uC744 CDN\uC5D0\uC11C \uBC1B\uC544 \uD0C0\uC785 \uC0DD\uC131 + \uB85C\uCEEC \uCE90\uC2DC").option("--phase <phase>", "\uB300\uC0C1 phase (real, qa, dev)", "real").option("--target <target>", "\uBC88\uC5ED \uC2DC\uC2A4\uD15C (user, admin)", "user").option("--tier <tier>", "\uBC88\uC5ED tier (inhouse, mnetplus, hiand)", "inhouse").option("--ref <locale>", "\uD0C0\uC785 \uC6D0\uBB38 \uAE30\uC900 \uB85C\uCF00\uC77C", "ko").option("--out <file>", "\uC0DD\uC131 \uD0C0\uC785 \uD30C\uC77C \uACBD\uB85C", "src/bstage-i18n.ts").action(i18nPullCommand);
2785
+ program.addHelpText(
2786
+ "after",
2787
+ `
2788
+ \uC608\uC2DC:
2789
+ $ bstage init \uC0C8 \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8 \uC0DD\uC131
2790
+ $ bstage dev \uB85C\uCEEC \uAC1C\uBC1C \uC11C\uBC84 (\uC778\uC99D \uD504\uB85D\uC2DC)
2791
+ $ bstage build IIFE \uBC88\uB4E4 \uBE4C\uB4DC (dist/{\uACBD\uB85C} \xB7 dist/{\uC2AC\uB86F})
2792
+ $ bstage doctor SDK \uBC84\uC804\xB7\uB4DC\uB9AC\uD504\uD2B8 \uC9C4\uB2E8
2793
+ $ bstage skills install \uC5D0\uC774\uC804\uD2B8 \uC2A4\uD0AC \uC124\uCE58/\uB3D9\uAE30\uD654
2794
+ $ bstage i18n pull \uD50C\uB7AB\uD3FC \uBC88\uC5ED \uBC1B\uAE30 + \uD0C0\uC785 \uC0DD\uC131
2795
+
2796
+ \uBB38\uC11C:
2797
+ $ bstage docs \uC124\uCE58\uB41C SDK \uBB38\uC11C \uBAA9\uB85D\xB7\uACBD\uB85C
2798
+ \uCEE4\uB9E8\uB4DC\uBCC4 \uC0C1\uC138\uB294 \`bstage <command> --help\``
2799
+ );
2800
+ program.parse();