@bstage-sdk/cli 0.5.0 → 0.6.1

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/MIGRATION.md CHANGED
@@ -50,6 +50,53 @@
50
50
  > **정렬**: 릴리즈 **시점 최신이 위**. 패키지가 독립 버전이라 헤더 숫자만으로는 시간순 정렬이 되지 않으므로 릴리즈 순서를 기준으로 한다.
51
51
 
52
52
  ---
53
+
54
+ ## → cli 0.6.0
55
+
56
+ ### sdk와 liquid 템플릿이 한 레포에 있어도 빌드·배포된다(혼합 레포) — `자동` · `선택`
57
+
58
+ **영향**
59
+
60
+ - **단일 종류(sdk만·liquid만) 레포가 할 일은 없다.** 명령·산출물이 그대로다.
61
+ - `mixed`(sdk 템플릿 + liquid 템플릿)로 판정돼 `bstage build`·`dev`·`deploy`가 종료 코드 2로 멈추던 레포는 이제 진행한다.
62
+ - 감지: `bstage build`가 "liquid 템플릿 N개 … 포털이 … 패키징합니다" 안내를 낸다. `bstage deploy`도 혼합 안내 한 줄을 낸다.
63
+
64
+ **변경 내용**
65
+
66
+ - **`bstage build`**: 혼합 레포에서 sdk 엔트리만 번들하고 liquid는 검증만 한다 — `dist/`에 liquid를 넣지 않는다(포털 빌더가 push된 커밋의 `public/{user|admin}/{이름}/`을 sdk 산출물과 함께 패키징한다). 검증은 번들보다 먼저 하고, 오류가 있으면 번들 없이 종료 코드 2다. `--json`은 `{ "kind": "mixed", "issues", "sdk": { "templates", "outputs" }, "liquid": { "templates" } }` 객체 하나를 stdout에 쓰고 진행 로그는 stderr로 보낸다.
67
+ - **예약 이름**: 혼합 레포에서 `src/pages/user/…`·`src/pages/admin/…` 페이지는 error다. 산출물이 `dist/{user|admin}/…`로 나가 liquid 자리와 겹치고, 같은 디렉터리에 `template.js`와 `template.liquid`가 함께 있으면 포털이 sdk로만 판정해 liquid가 조용히 사라진다. 포털 빌더도 같은 자리에서 막는다. 위젯(`src/slots`)은 해당 없다.
68
+ - **`bstage deploy`**: 혼합 레포를 막지 않는다. 위젯(SLOT) 배치 사전점검은 liquid 전용 레포에서만 하고, 혼합 레포의 항목별 판정(liquid 항목에 SLOT을 걸면 400)은 포털이 한다.
69
+ - **`bstage dev --kind <sdk|liquid>`**: 혼합 레포에서 어느 프리뷰를 띄울지 고른다. 기본은 sdk(Vite)이고 시작할 때 liquid 프리뷰 방법을 안내한다. 두 서버를 함께 띄우지 않는다. 단일 종류 레포에서 판정과 어긋난 값은 종료 코드 2다.
70
+ - **liquid 검증기**(`bstage build`·`bstage doctor`의 liquid 절)는 sdk와 섞인 것을 더 이상 error로 보고하지 않는다.
71
+ - **포털 빌더는 별도로 갱신된다.** 갱신 전 포털은 혼합 레포에서 liquid만 패키징하므로, CLI와 빌더가 모두 반영된 뒤 혼합 레포를 배포한다.
72
+
73
+ **적용**
74
+
75
+ 1. (자동) 단일 종류 레포는 그대로 둔다 — 올리기만 하면 된다.
76
+ 2. (판단) 혼합 레포를 만들 때: liquid는 `public/{user|admin}/{이름}/template.liquid`, sdk 페이지는 `src/pages/{이름}/template.tsx`(`user`·`admin` 제외), 위젯은 `src/slots/…`. `bstage build`가 겹치는 자리를 짚어 준다.
77
+ 3. (자동) 혼합 레포의 liquid 프리뷰는 `bstage dev --kind liquid`.
78
+
79
+ ### AGENTS.md 관리 영역 v14 · 혼합 레포 스킬 세트(`bstage ai install`·`doctor`) — `자동` · `선택`
80
+
81
+ **영향**
82
+
83
+ - `bstage init`으로 만든 모든 프로젝트. 관리 영역이 v14보다 낮으면 해당한다(본문 문구 변경 — liquid 본문의 "sdk와 섞지 않는다"가 공존 규칙으로 바뀌었다).
84
+ - `mixed`로 판정돼 `bstage ai install`·`update`가 종료 코드 2로 멈추고 `bstage doctor`가 스킬 검사를 건너뛰던 혼합 레포.
85
+ - 감지: `bstage ai doctor`가 `AGENTS.md`를 `stale`로 보고한다. 혼합 레포에서는 `bstage-template`·`bstage-liquid` 둘 다 검사 대상이 된다.
86
+
87
+ **변경 내용**
88
+
89
+ - `bstage ai install`·`update`·`doctor`가 혼합 레포를 지원한다 — 스킬은 sdk 세트와 liquid 세트의 **합집합**(5종)을 설치하고, `AGENTS.md` 관리 영역은 두 구조와 혼합 규칙(어느 방식으로 만들지 · sdk 페이지 `user`·`admin` 금지 · `bstage build`·`dev --kind` 동작)을 담은 **혼합 본문**이 된다. `mixed`는 판정으로만 나오며 `--kind`로 지정하는 값이 아니다 — 혼합 레포에 `--kind sdk|liquid`를 주면 판정과 어긋나 멈춘다.
90
+ - `bstage doctor`는 혼합 레포에서 스킬 검사를 건너뛰지 않고 두 세트를 모두 보며, liquid 검증 결과도 함께 낸다. `--json`의 `skillsSkipped`는 항상 `false`다(필드는 비파괴적으로 남긴다).
91
+ - **레포 종류가 바뀐 뒤의 `AGENTS.md`** — sdk 레포에 liquid 템플릿을 더해 혼합이 되면 관리 영역 마커 버전은 그대로라, 예전에는 옛 sdk 본문이 "최신"으로 남았다. 이제 `bstage ai install`·`update`·`doctor`가 본문을 현재 종류의 렌더 결과와 비교해 다르면 갱신(`update`)·`stale`로 본다. 자유 영역은 보존된다.
92
+ - **관리 영역이 이 CLI보다 새 버전이면 하향임을 알린다.** 프로젝트에 고정된 옛 CLI로 `ai install`·`update`·`doctor`를 돌리면 새 CLI가 놓은 관리 영역을 옛 본문으로 되돌리게 된다 — 그 사실을 알리고 최신 CLI로 다시 실행하라고 안내한다. 덮어쓰기 자체는 막지 않는다(마커 버전은 파일 안의 자기선언 값이다).
93
+ - `bstage-onboarding`·`bstage-liquid` 스킬과 `AGENTS.md`의 "한 레포에 한 방식만" 문구를 공존 규칙(예약 이름 · `dev --kind liquid`)으로 바꿨다.
94
+
95
+ **적용**
96
+
97
+ 1. (자동) `npx @bstage-sdk/cli@latest ai update` — 스킬·`AGENTS.md` 관리 영역을 v14로 갱신한다. 혼합 레포에서는 없는 스킬도 만들어야 하므로 `ai install`을 쓴다.
98
+ 2. (판단) 혼합 레포의 `AGENTS.md` 자유 영역에 "한 레포에 한 방식만" 류의 자체 규칙을 적어 두었다면 지운다 — 관리 영역과 어긋난다.
99
+
53
100
  ## → cli 0.5.0
54
101
 
55
102
  ### liquid 에셋은 포털 미디어에 올린다 — 레포 상대 경로는 배포 후 깨진다 — `판단` · `필수`
package/dist/index.js CHANGED
@@ -662,6 +662,13 @@ function detectProjectKind(root) {
662
662
  hasSdkDependency: sdkDependency
663
663
  };
664
664
  }
665
+ function collidingSdkPages(report2) {
666
+ if (report2.liquidTemplates.length === 0) return [];
667
+ return report2.sdkTemplates.filter((t) => {
668
+ const [root, first] = t.split("/");
669
+ return root === "pages" && first !== void 0 && LIQUID_SURFACES.has(first.toLowerCase());
670
+ });
671
+ }
665
672
 
666
673
  // src/commands/ai/context.ts
667
674
  var DEFAULT_SKILLS_DIR = ".claude/skills";
@@ -703,10 +710,13 @@ function resolveKind(root, requested) {
703
710
  const wanted = parseKindOption(requested);
704
711
  const detected = detectProjectKind(root).kind;
705
712
  if (detected === "mixed") {
706
- fail(
707
- ExitCode.PRECONDITION,
708
- "sdk\uC640 liquid\uAC00 \uD55C \uB808\uD3EC\uC5D0 \uC11E\uC5EC \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uD3EC\uD138\uC774 \uBE4C\uB4DC\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4. \uD558\uB098\uB9CC \uB0A8\uAE30\uC138\uC694."
709
- );
713
+ if (wanted) {
714
+ fail(
715
+ ExitCode.PRECONDITION,
716
+ `--kind ${wanted}\uB85C \uC9C0\uC815\uD588\uC9C0\uB9CC \uC774 \uB808\uD3EC\uB294 mixed(sdk + liquid \uD63C\uD569)\uB85C \uD310\uC815\uB429\uB2C8\uB2E4 \u2014 \uC635\uC158\uC744 \uBE7C\uACE0 \uC2E4\uD589\uD558\uC138\uC694(\uB450 \uC138\uD2B8\uB97C \uBAA8\uB450 \uC124\uCE58\uD569\uB2C8\uB2E4).`
717
+ );
718
+ }
719
+ return "mixed";
710
720
  }
711
721
  if (detected === "unknown") {
712
722
  if (!wanted) {
@@ -1099,7 +1109,7 @@ async function aiDoctorCommand(o, deps = {}) {
1099
1109
  }
1100
1110
 
1101
1111
  // src/commands/build.ts
1102
- import { resolve as resolve2, join as join6, relative as relative2 } from "path";
1112
+ import { resolve as resolve2, join as join6, relative as relative2, sep as sep3 } from "path";
1103
1113
  import { readFile as readFile2, access, readdir, rename, mkdir, rm } from "fs/promises";
1104
1114
  import { build as viteBuild, loadEnv } from "vite";
1105
1115
  import pc7 from "picocolors";
@@ -1523,13 +1533,6 @@ function findMisnamedTemplates(root) {
1523
1533
  function validateLiquid(root) {
1524
1534
  const detected = detectProjectKind(root);
1525
1535
  const issues = [];
1526
- if (detected.kind === "mixed") {
1527
- issues.push({
1528
- level: "error",
1529
- path: ".",
1530
- message: "sdk \uD15C\uD50C\uB9BF(src/pages\xB7src/slots\uC758 template.tsx)\uACFC liquid \uD15C\uD50C\uB9BF\uC774 \uD55C \uB808\uD3EC\uC5D0 \uC11E\uC5EC \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uD3EC\uD138 \uBE4C\uB354\uB294 \uB458\uC744 \uD568\uAED8 \uD328\uD0A4\uC9D5\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4. \uD558\uB098\uB9CC \uB0A8\uACA8 \uC8FC\uC138\uC694."
1531
- });
1532
- }
1533
1536
  if (detected.liquidTemplates.length === 0) {
1534
1537
  issues.push({
1535
1538
  level: "error",
@@ -1746,8 +1749,9 @@ ${pc7.red("Error")} ${label}: name "${elementName}"\uC744 ${elementNameOwner}\uA
1746
1749
  const finalDir = outputName ? join6(cwd, DIST_DIR, outputName) : join6(cwd, DIST_DIR);
1747
1750
  await mkdir(finalDir, { recursive: true });
1748
1751
  await rename(join6(cwd, outDir, "template.js"), join6(finalDir, "template.js"));
1749
- console.log(` ${relative2(cwd, finalDir)}/template.js (${formatSize(bundleSize)})`);
1750
- return { usesClient };
1752
+ const output = relative2(cwd, finalDir).split(sep3).join("/");
1753
+ console.log(` ${output}/template.js (${formatSize(bundleSize)})`);
1754
+ return { usesClient, output };
1751
1755
  }
1752
1756
  function printCredentialReport(env2, referenced) {
1753
1757
  const issues = checkCredentials(env2, referenced);
@@ -1813,19 +1817,101 @@ function printLiquidIssues(issues) {
1813
1817
  console.log(pc7.green("\u2713 liquid \uAC80\uC99D \uD1B5\uACFC"));
1814
1818
  }
1815
1819
  }
1820
+ async function bundleAll(cwd, entries, bundleEntry) {
1821
+ await rm(join6(cwd, DIST_DIR), { recursive: true, force: true });
1822
+ const ctx = {
1823
+ cwd,
1824
+ takenOutputs: /* @__PURE__ */ new Map(),
1825
+ takenElementNames: /* @__PURE__ */ new Map()
1826
+ };
1827
+ console.log(
1828
+ `Found ${entries.length} template${entries.length > 1 ? "s" : ""}: ${entries.map(entryLabel).join(", ")}`
1829
+ );
1830
+ const results = [];
1831
+ for (const entry of entries) {
1832
+ results.push(await bundleEntry(ctx, entry));
1833
+ }
1834
+ await rm(join6(cwd, DIST_DIR, STAGING_DIR), { recursive: true, force: true });
1835
+ return results;
1836
+ }
1837
+ async function reportCredentials(cwd, results) {
1838
+ if (!results.some((r) => r.usesClient)) return;
1839
+ const referenced = collectReferencedVars(await readProjectSources(cwd));
1840
+ printCredentialReport(loadEnv("production", cwd, "VITE_"), referenced);
1841
+ }
1842
+ function collisionIssues(report2) {
1843
+ return collidingSdkPages(report2).map((t) => ({
1844
+ level: "error",
1845
+ path: `src/${t}`,
1846
+ message: `\uC0B0\uCD9C\uBB3C\uC774 ${DIST_DIR}/${t.slice("pages/".length)}/ \uB85C \uB098\uAC00 liquid \uD15C\uD50C\uB9BF \uC790\uB9AC(${DIST_DIR}/{user|admin}/{\uC774\uB984}/)\uC640 \uACB9\uCE69\uB2C8\uB2E4 \u2014 ${PAGES_ROOT}/ \uBC14\uB85C \uC544\uB798 \uD3F4\uB354 \uC774\uB984\uC5D0 user\xB7admin \uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD3F4\uB354 \uC774\uB984\uC744 \uBC14\uAFD4 \uC8FC\uC138\uC694.`
1847
+ }));
1848
+ }
1849
+ async function withStdoutQuiet(fn) {
1850
+ const { log: log3, info } = console;
1851
+ console.log = (...args) => console.error(...args);
1852
+ console.info = (...args) => console.error(...args);
1853
+ try {
1854
+ return await fn();
1855
+ } finally {
1856
+ console.log = log3;
1857
+ console.info = info;
1858
+ }
1859
+ }
1860
+ async function buildMixed(cwd, options, report2, bundleEntry) {
1861
+ const issues = [...validateLiquid(cwd), ...collisionIssues(report2)];
1862
+ const hasError = issues.some((i) => i.level === "error");
1863
+ const liquid = { templates: report2.liquidTemplates };
1864
+ if (options.json) {
1865
+ if (hasError) {
1866
+ printJson({ kind: "mixed", issues, liquid });
1867
+ process.exitCode = ExitCode.PRECONDITION;
1868
+ return;
1869
+ }
1870
+ const entries2 = await discoverEntries(cwd);
1871
+ const results2 = await withStdoutQuiet(() => bundleAll(cwd, entries2, bundleEntry));
1872
+ printJson({
1873
+ kind: "mixed",
1874
+ issues,
1875
+ sdk: { templates: entries2.map(entryLabel), outputs: results2.map((r) => r.output) },
1876
+ liquid
1877
+ });
1878
+ return;
1879
+ }
1880
+ printLiquidIssues(issues);
1881
+ if (hasError) {
1882
+ console.error(
1883
+ `
1884
+ ${pc7.red("\u2717")} \uAC80\uC99D \uC2E4\uD328 \u2014 \uC704 \uD56D\uBAA9\uC744 \uACE0\uCE5C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694. \uC774\uB300\uB85C push\uD558\uBA74 \uD3EC\uD138 \uBE4C\uB4DC\uB3C4 \uAC19\uC740 \uC790\uB9AC\uC5D0\uC11C \uB9C9\uD799\uB2C8\uB2E4.`
1885
+ );
1886
+ process.exit(ExitCode.PRECONDITION);
1887
+ }
1888
+ const entries = await discoverEntries(cwd);
1889
+ const results = await bundleAll(cwd, entries, bundleEntry);
1890
+ console.log(`
1891
+ Build complete.`);
1892
+ console.log(
1893
+ `liquid \uD15C\uD50C\uB9BF ${report2.liquidTemplates.length}\uAC1C(${report2.liquidTemplates.join(", ")})\uB294 \uB85C\uCEEC \uC0B0\uCD9C\uBB3C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uD3EC\uD138\uC774 push\uB41C \uCEE4\uBC0B\uC758 public/{user|admin}/{name}/ \uC744 \uADF8\uB300\uB85C \uD328\uD0A4\uC9D5\uD569\uB2C8\uB2E4(data.json\xB7layout.json \uC81C\uC678)`
1894
+ );
1895
+ await reportCredentials(cwd, results);
1896
+ }
1816
1897
  async function buildCommand(options = {}, deps = {}) {
1817
1898
  const cwd = deps.cwd ?? process.cwd();
1899
+ const bundleEntry = deps.bundleEntry ?? buildEntry;
1818
1900
  const report2 = detectProjectKind(cwd);
1819
1901
  const kind = report2.kind;
1820
- if (kind === "liquid" || kind === "mixed") {
1902
+ if (kind === "liquid") {
1821
1903
  reportLiquid(cwd, options);
1822
1904
  return;
1823
1905
  }
1906
+ if (kind === "mixed") {
1907
+ await buildMixed(cwd, options, report2, bundleEntry);
1908
+ return;
1909
+ }
1824
1910
  const entries = await discoverEntries(cwd);
1825
1911
  if (entries.length === 0) {
1826
- const ignored = ignoredLiquidNotice(report2.ignoredLiquid);
1827
- if (ignored !== null) {
1828
- console.error(`Error: ${ignored}`);
1912
+ const ignored2 = ignoredLiquidNotice(report2.ignoredLiquid);
1913
+ if (ignored2 !== null) {
1914
+ console.error(`Error: ${ignored2}`);
1829
1915
  await printLegacyHint(cwd);
1830
1916
  process.exit(ExitCode.PRECONDITION);
1831
1917
  }
@@ -1835,26 +1921,13 @@ async function buildCommand(options = {}, deps = {}) {
1835
1921
  await printLegacyHint(cwd);
1836
1922
  process.exit(ExitCode.PRECONDITION);
1837
1923
  }
1838
- await rm(join6(cwd, DIST_DIR), { recursive: true, force: true });
1839
- const ctx = {
1840
- cwd,
1841
- takenOutputs: /* @__PURE__ */ new Map(),
1842
- takenElementNames: /* @__PURE__ */ new Map()
1843
- };
1844
- console.log(
1845
- `Found ${entries.length} template${entries.length > 1 ? "s" : ""}: ${entries.map(entryLabel).join(", ")}`
1846
- );
1847
- const results = [];
1848
- for (const entry of entries) {
1849
- results.push(await buildEntry(ctx, entry));
1850
- }
1851
- await rm(join6(cwd, DIST_DIR, STAGING_DIR), { recursive: true, force: true });
1924
+ const ignored = ignoredLiquidNotice(report2.ignoredLiquid);
1925
+ if (ignored !== null) console.log(`${pc7.yellow("\u26A0")} ${ignored}
1926
+ `);
1927
+ const results = await bundleAll(cwd, entries, bundleEntry);
1852
1928
  console.log(`
1853
1929
  Build complete.`);
1854
- if (results.some((r) => r.usesClient)) {
1855
- const referenced = collectReferencedVars(await readProjectSources(cwd));
1856
- printCredentialReport(loadEnv("production", cwd, "VITE_"), referenced);
1857
- }
1930
+ await reportCredentials(cwd, results);
1858
1931
  }
1859
1932
  function formatSize(bytes) {
1860
1933
  if (bytes < 1024) return `${bytes} B`;
@@ -2511,13 +2584,8 @@ async function deployCommand(options, deps = {}) {
2511
2584
  fail(ExitCode.PRECONDITION, "--json \uC740 --yes \uC640 \uD568\uAED8 \uC4F0\uC138\uC694.");
2512
2585
  }
2513
2586
  if (!options.yes) assertCanConfirm(deps);
2514
- const kind = detectProjectKind(deps.cwd ?? process.cwd()).kind;
2515
- if (kind === "mixed") {
2516
- fail(
2517
- ExitCode.PRECONDITION,
2518
- "sdk \uD15C\uD50C\uB9BF\uACFC liquid \uD15C\uD50C\uB9BF\uC774 \uD55C \uB808\uD3EC\uC5D0 \uC11E\uC5EC \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uD3EC\uD138 \uBE4C\uB354\uAC00 \uD328\uD0A4\uC9D5\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4. bstage build \uB85C \uC5B4\uB290 \uD30C\uC77C\uC774 \uAC78\uB9AC\uB294\uC9C0 \uD655\uC778\uD55C \uB4A4 \uD558\uB098\uB9CC \uB0A8\uACA8 \uC8FC\uC138\uC694."
2519
- );
2520
- }
2587
+ const report2 = detectProjectKind(deps.cwd ?? process.cwd());
2588
+ const kind = report2.kind;
2521
2589
  const out = deps.out ?? console.log;
2522
2590
  const ctx = await resolveContext({ cwd: deps.cwd, env: deps.env, fetch: deps.fetch });
2523
2591
  const defaultBranch = await repoDefaultBranch(ctx);
@@ -2525,13 +2593,20 @@ async function deployCommand(options, deps = {}) {
2525
2593
  const targets = await fetchTargets(ctx, options.placement);
2526
2594
  if (kind === "liquid") {
2527
2595
  assertNoSlotPlacement(targets);
2528
- if (!options.json) {
2529
- out(
2530
- pc10.dim(
2531
- "[bstage] liquid \uB808\uD3EC \u2014 \uB85C\uCEEC \uBE4C\uB4DC \uC5C6\uC774 \uD3EC\uD138\uC774 push\uB41C \uCEE4\uBC0B\uC758 public/{user|admin}/ \uC744 \uADF8\uB300\uB85C \uD328\uD0A4\uC9D5\uD569\uB2C8\uB2E4."
2532
- )
2533
- );
2534
- }
2596
+ }
2597
+ if (!options.json && kind === "liquid") {
2598
+ out(
2599
+ pc10.dim(
2600
+ "[bstage] liquid \uB808\uD3EC \u2014 \uB85C\uCEEC \uBE4C\uB4DC \uC5C6\uC774 \uD3EC\uD138\uC774 push\uB41C \uCEE4\uBC0B\uC758 public/{user|admin}/ \uC744 \uADF8\uB300\uB85C \uD328\uD0A4\uC9D5\uD569\uB2C8\uB2E4."
2601
+ )
2602
+ );
2603
+ }
2604
+ if (!options.json && kind === "mixed") {
2605
+ out(
2606
+ pc10.dim(
2607
+ `[bstage] \uD63C\uD569 \uB808\uD3EC \u2014 sdk \uD15C\uD50C\uB9BF\uC740 \uD3EC\uD138\uC774 \uBE4C\uB4DC\uD558\uACE0, liquid \uD15C\uD50C\uB9BF ${report2.liquidTemplates.length}\uAC1C\uB294 push\uB41C \uCEE4\uBC0B\uC758 public/{user|admin}/ \uC744 \uADF8\uB300\uB85C \uD328\uD0A4\uC9D5\uD569\uB2C8\uB2E4.`
2608
+ )
2609
+ );
2535
2610
  }
2536
2611
  const created = await createAndAnnounceBuild(ctx, options);
2537
2612
  if (options.noWait) {
@@ -2555,7 +2630,7 @@ import pc11 from "picocolors";
2555
2630
  import { existsSync as existsSync5, realpathSync as realpathSync2, statSync, watch } from "fs";
2556
2631
  import { readFile as readFile5 } from "fs/promises";
2557
2632
  import { createServer } from "http";
2558
- import { join as join9, relative as relative3, sep as sep3 } from "path";
2633
+ import { join as join9, relative as relative3, sep as sep4 } from "path";
2559
2634
 
2560
2635
  // src/liquid/routes.ts
2561
2636
  var RELOAD_PATH = "/__bstage__/reload";
@@ -2788,7 +2863,7 @@ function isInside(publicDir, full) {
2788
2863
  try {
2789
2864
  const base = realpathSync2(publicDir);
2790
2865
  const real = realpathSync2(full);
2791
- return real === base || real.startsWith(base + sep3);
2866
+ return real === base || real.startsWith(base + sep4);
2792
2867
  } catch {
2793
2868
  return false;
2794
2869
  }
@@ -2961,6 +3036,27 @@ async function startLiquidDevServer(options) {
2961
3036
  }
2962
3037
 
2963
3038
  // src/commands/dev.ts
3039
+ function resolveDevMode(kind, requested) {
3040
+ if (requested !== void 0 && requested !== "sdk" && requested !== "liquid") {
3041
+ fail(ExitCode.PRECONDITION, `--kind \uAC12\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${requested} (sdk \uB610\uB294 liquid)`);
3042
+ }
3043
+ const detected = kind === "liquid" ? "liquid" : "sdk";
3044
+ if (kind === "mixed") {
3045
+ if (requested === "liquid") return { mode: "liquid" };
3046
+ if (requested === "sdk") return { mode: "sdk" };
3047
+ return {
3048
+ mode: "sdk",
3049
+ hint: "sdk\uC640 liquid \uD15C\uD50C\uB9BF\uC774 \uD568\uAED8 \uC788\uB294 \uB808\uD3EC\uC785\uB2C8\uB2E4 \u2014 \uC9C0\uAE08\uC740 sdk(Vite) \uD504\uB9AC\uBDF0\uB97C \uB744\uC6C1\uB2C8\uB2E4. liquid \uD504\uB9AC\uBDF0\uB294 `bstage dev --kind liquid` \uB85C \uB530\uB85C \uB744\uC6B0\uC138\uC694."
3050
+ };
3051
+ }
3052
+ if (requested !== void 0 && requested !== detected) {
3053
+ fail(
3054
+ ExitCode.PRECONDITION,
3055
+ `--kind ${requested}\uB85C \uC9C0\uC815\uD588\uC9C0\uB9CC \uC774 \uB808\uD3EC\uB294 ${kind}\uB85C \uD310\uC815\uB429\uB2C8\uB2E4 \u2014 \uC635\uC158\uC744 \uBE7C\uACE0 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`
3056
+ );
3057
+ }
3058
+ return { mode: detected };
3059
+ }
2964
3060
  async function runLiquidPreview(root, port, phase) {
2965
3061
  if (phase !== void 0) {
2966
3062
  console.log("[bstage] liquid \uD504\uB9AC\uBDF0\uB294 phase\uB97C \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 --phase \uB97C \uBB34\uC2DC\uD569\uB2C8\uB2E4.");
@@ -2977,7 +3073,6 @@ async function runLiquidPreview(root, port, phase) {
2977
3073
  console.log(` admin \uBAA9\uB85D: ${base}/admin`);
2978
3074
  console.log(" phase\uC640 \uC778\uC99D \uD504\uB85D\uC2DC\uB294 liquid \uD504\uB9AC\uBDF0\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. \uAC12\uC740 data.json\uC5D0\uC11C \uC635\uB2C8\uB2E4.\n");
2979
3075
  }
2980
- var MIXED_MESSAGE = "liquid \uD15C\uD50C\uB9BF(public/{user|admin}/{\uC774\uB984}/template.liquid)\uACFC React \uD15C\uD50C\uB9BF(src/**/template.tsx)\uC774 \uD55C \uB808\uD3EC\uC5D0 \uC11E\uC5EC \uC788\uC2B5\uB2C8\uB2E4. \uD3EC\uD138 \uBE4C\uB354\uAC00 \uBE4C\uB4DC\uD558\uC9C0 \uBABB\uD558\uBBC0\uB85C \uD55C\uCABD\uC73C\uB85C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.";
2981
3076
  async function runViteDevServer(root, port, phaseOption) {
2982
3077
  let resolved;
2983
3078
  try {
@@ -3020,12 +3115,13 @@ async function devCommand(options) {
3020
3115
  const port = parseInt(options.port, 10);
3021
3116
  const root = process.cwd();
3022
3117
  const report2 = detectProjectKind(root);
3023
- const kind = report2.kind;
3118
+ const decision = resolveDevMode(report2.kind, options.kind);
3024
3119
  const ignored = ignoredLiquidNotice(report2.ignoredLiquid);
3025
3120
  if (ignored !== null) console.log(`[bstage] ${pc11.yellow("\uACBD\uACE0")}: ${ignored}
3026
3121
  `);
3027
- if (kind === "liquid") return await runLiquidPreview(root, port, options.phase);
3028
- if (kind === "mixed") fail(ExitCode.PRECONDITION, MIXED_MESSAGE);
3122
+ if (decision.hint !== void 0) console.log(`[bstage] ${decision.hint}
3123
+ `);
3124
+ if (decision.mode === "liquid") return await runLiquidPreview(root, port, options.phase);
3029
3125
  await runViteDevServer(root, port, options.phase);
3030
3126
  }
3031
3127
 
@@ -4525,11 +4621,15 @@ import { readFile as readFile8 } from "fs/promises";
4525
4621
  import { join as join14 } from "path";
4526
4622
  import {
4527
4623
  VERSION as TOOLKIT_VERSION,
4624
+ AGENTS_MANAGED_VERSION,
4528
4625
  agentsMdStatus,
4529
4626
  compareVersions,
4627
+ extractManagedBlock,
4530
4628
  parseAgentsIdentity as parseAgentsIdentity2,
4629
+ readManagedVersion as readManagedVersion2,
4531
4630
  readSkillStamp as readSkillStamp2,
4532
4631
  renderManagedBlock,
4632
+ sanitizeIdentity as sanitizeIdentity2,
4533
4633
  skillsFor as skillsFor4
4534
4634
  } from "@bstage-sdk/ai-toolkit";
4535
4635
  async function autoSyncSkills(root, report2) {
@@ -4555,10 +4655,10 @@ async function diagnoseSkills(root, kind) {
4555
4655
  );
4556
4656
  }
4557
4657
  function resolveSkillKind(detected) {
4558
- if (detected === "mixed") return null;
4658
+ if (detected === "mixed") return "mixed";
4559
4659
  return detected === "liquid" ? "liquid" : "sdk";
4560
4660
  }
4561
- async function diagnoseAgentsMd(root, pkgName) {
4661
+ async function diagnoseAgentsMd(root, pkgName, kind = "sdk") {
4562
4662
  let content;
4563
4663
  try {
4564
4664
  content = await readFile8(join14(root, "AGENTS.md"), "utf-8");
@@ -4566,12 +4666,18 @@ async function diagnoseAgentsMd(root, pkgName) {
4566
4666
  return { status: "missing", managedBlock: null };
4567
4667
  }
4568
4668
  const status = agentsMdStatus(content);
4569
- if (status === "ok") return { status, managedBlock: null };
4570
4669
  const parsed = parseAgentsIdentity2(content);
4571
- const projectName = parsed.projectName ?? pkgName ?? "custom-templates";
4572
- const space = parsed.space ?? projectName.split("-custom-templates-")[0] ?? projectName;
4670
+ const projectName = sanitizeIdentity2(parsed.projectName) ?? sanitizeIdentity2(pkgName) ?? "custom-templates";
4671
+ const space = sanitizeIdentity2(parsed.space) ?? projectName.split("-custom-templates-")[0] ?? projectName;
4573
4672
  const target = resolveProjectTarget(root);
4574
- return { status, managedBlock: renderManagedBlock({ space, projectName, target }) };
4673
+ const block = renderManagedBlock({ space, projectName, target, kind });
4674
+ if (status === "ok") {
4675
+ if (extractManagedBlock(content) === block) return { status, managedBlock: null };
4676
+ const fileVersion = readManagedVersion2(content);
4677
+ const downgrade = fileVersion !== null && fileVersion > AGENTS_MANAGED_VERSION;
4678
+ return downgrade ? { status: "stale", managedBlock: block, downgrade: true } : { status: "stale", managedBlock: block };
4679
+ }
4680
+ return { status, managedBlock: block };
4575
4681
  }
4576
4682
 
4577
4683
  // src/commands/doctor/report.ts
@@ -4646,7 +4752,8 @@ async function diagnose2(root) {
4646
4752
  const deps = pkg.deps;
4647
4753
  const kind = detectProjectKind(root).kind;
4648
4754
  const isLiquid = kind === "liquid";
4649
- const isBstageProject = isLiquid || SDK_PACKAGES2.some((p5) => p5 in deps);
4755
+ const hasLiquid = isLiquid || kind === "mixed";
4756
+ const isBstageProject = hasLiquid || SDK_PACKAGES2.some((p5) => p5 in deps);
4650
4757
  const installed = await collectInstalledVersions(root, deps);
4651
4758
  const { versions, latestChecked } = await diagnoseVersions(
4652
4759
  deps,
@@ -4667,11 +4774,11 @@ async function diagnose2(root) {
4667
4774
  scripts: diagnoseScripts(pkg.scripts, tables.scripts),
4668
4775
  recommendedDeps: diagnoseRecommendedDeps(deps, tables.deps),
4669
4776
  migrations: await readApplicableMigrations(migrationResolver(root, deps, installed)),
4670
- agentsMd: await diagnoseAgentsMd(root, pkg.name),
4671
- skills: skillKind === null ? [] : await diagnoseSkills(root, skillKind),
4672
- skillsSkipped: skillKind === null,
4777
+ agentsMd: await diagnoseAgentsMd(root, pkg.name, skillKind),
4778
+ skills: await diagnoseSkills(root, skillKind),
4779
+ skillsSkipped: false,
4673
4780
  latestChecked,
4674
- liquid: isLiquid ? { issues: validateLiquid(root) } : null
4781
+ liquid: hasLiquid ? { issues: validateLiquid(root) } : null
4675
4782
  };
4676
4783
  }
4677
4784
  async function readInstalledVersion(root, pkg) {
@@ -4862,6 +4969,14 @@ function printAgentsMd(r) {
4862
4969
  console.log(pc15.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."));
4863
4970
  break;
4864
4971
  case "stale":
4972
+ if (r.agentsMd.downgrade) {
4973
+ console.log(
4974
+ pc15.yellow(" ! \uAD00\uB9AC \uC601\uC5ED\uC774 \uC774 CLI\uBCF4\uB2E4 \uC0C8 \uBC84\uC804") + pc15.dim(
4975
+ " \u2014 \uC774 CLI\uAC00 \uC624\uB798\uB3FC \uAC31\uC2E0\uD558\uBA74 \uC61B \uBCF8\uBB38\uC73C\uB85C \uB418\uB3CC\uC544\uAC11\uB2C8\uB2E4(\uD558\uD5A5). \uCD5C\uC2E0 CLI(npx @bstage-sdk/cli@latest)\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
4976
+ )
4977
+ );
4978
+ break;
4979
+ }
4865
4980
  console.log(
4866
4981
  pc15.yellow(" \u2192 \uAD00\uB9AC \uC601\uC5ED\uC774 \uC624\uB798\uB428") + pc15.dim(" \u2014 `bstage ai update`\uB85C \uAC31\uC2E0(\uC790\uC720 \uC601\uC5ED\uC740 \uBCF4\uC874).")
4867
4982
  );
@@ -4943,8 +5058,8 @@ function printReport(r) {
4943
5058
  }
4944
5059
  printVersions(r);
4945
5060
  printFiles(r);
4946
- if (r.liquid === null) printLayout(r);
4947
- else printLiquid(r);
5061
+ if (r.kind !== "liquid") printLayout(r);
5062
+ if (r.liquid !== null) printLiquid(r);
4948
5063
  printPnpmWorkspace(r);
4949
5064
  printScripts(r);
4950
5065
  printRecommendedDeps(r);
@@ -5882,8 +5997,8 @@ function readVersion() {
5882
5997
  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");
5883
5998
  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("--kind <kind>", "\uD504\uB85C\uC81D\uD2B8 \uC885\uB958 (sdk: React \uCEF4\uD3EC\uB10C\uD2B8, liquid: liquid \uD15C\uD50C\uB9BF)", "sdk").option("--template <template>", "\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF \uC774\uB984 (\uAE30\uBCF8: sdk {space}-hello, liquid 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);
5884
5999
  program.command("build").description(
5885
- "\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) \u2014 liquid \uB808\uD3EC\uB294 \uAC80\uC99D\uB9CC"
5886
- ).option("--json", "\uAC80\uC99D \uACB0\uACFC\uB97C JSON\uC73C\uB85C \uCD9C\uB825 (liquid \uB808\uD3EC \uC804\uC6A9)").action((options) => buildCommand(options));
6000
+ "\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) \u2014 liquid \uB808\uD3EC\uB294 \uAC80\uC99D\uB9CC, \uD63C\uD569 \uB808\uD3EC\uB294 sdk \uBC88\uB4E4 + liquid \uAC80\uC99D"
6001
+ ).option("--json", "\uAC80\uC99D \uACB0\uACFC\xB7\uC0B0\uCD9C\uBB3C \uC694\uC57D\uC744 JSON\uC73C\uB85C \uCD9C\uB825 (liquid\xB7\uD63C\uD569 \uB808\uD3EC)").action((options) => buildCommand(options));
5887
6002
  program.command("docs").description("\uC124\uCE58\uB41C SDK \uBB38\uC11C \uBAA9\uB85D\xB7\uACBD\uB85C \uCD9C\uB825").action(docsCommand);
5888
6003
  program.command("doctor").description(
5889
6004
  "SDK \uBC84\uC804\xB7\uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uB4DC\uB9AC\uD504\uD2B8 \uC9C4\uB2E8 (\uC9C4\uB2E8 + SDK \uC18C\uC720 \uC2A4\uD0AC \uC790\uB3D9 \uB3D9\uAE30\uD654; package.json\xB7\uD6C5\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC74C)"
@@ -5891,6 +6006,9 @@ program.command("doctor").description(
5891
6006
  program.command("dev").description("\uC778\uC99D \uD504\uB85D\uC2DC \uD3EC\uD568 \uB85C\uCEEC \uAC1C\uBC1C \uC11C\uBC84 (liquid \uB808\uD3EC\uB294 liquidjs \uD504\uB9AC\uBDF0)").option("-p, --port <port>", "Dev server port", "5173").option(
5892
6007
  "--phase <phase>",
5893
6008
  "Target phase (dev, qa, real, sandbox). \uC0DD\uB7B5 \uC2DC .env\uC758 VITE_BSTAGE_PHASE, \uADF8\uAC83\uB3C4 \uC5C6\uC73C\uBA74 sandbox"
6009
+ ).option(
6010
+ "--kind <kind>",
6011
+ "\uD63C\uD569 \uB808\uD3EC(sdk + liquid)\uC5D0\uC11C \uB744\uC6B8 \uD504\uB9AC\uBDF0 (sdk: Vite \uAE30\uBCF8, liquid: liquidjs)"
5894
6012
  ).action((o) => runCommand(() => devCommand(o)));
5895
6013
  var ai = program.command("ai").description("\uC5D0\uC774\uC804\uD2B8\uC6A9 \uC2A4\uD0AC\xB7AGENTS.md \uC124\uCE58\xB7\uB3D9\uAE30\uD654\xB7\uC9C4\uB2E8");
5896
6014
  ai.command("install").description("\uC2A4\uD0AC(.claude/skills)\xB7AGENTS.md\xB7CLAUDE.md \uC124\uCE58 \uBC0F \uCD5C\uC2E0\uD654 + pre-commit \uC2DC\uD06C\uB9BF \uAC00\uB4DC").option("--dir <dir>", "\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC", ".claude/skills").option("--kind <kind>", "\uD504\uB85C\uC81D\uD2B8 \uC885\uB958(sdk|liquid) \u2014 \uC790\uB3D9 \uD310\uC815\uC774 \uC548 \uB420 \uB54C").action((o) => runCommand(() => aiInstallCommand(o)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bstage-sdk/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "bstage SDK CLI — build b.stage templates",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,14 +32,14 @@
32
32
  "liquidjs": "^10.24.0",
33
33
  "picocolors": "^1.1.1",
34
34
  "vite": "^7.0.0",
35
- "@bstage-sdk/ai-toolkit": "0.5.0",
36
- "@bstage-sdk/core": "0.5.0"
35
+ "@bstage-sdk/ai-toolkit": "0.6.1",
36
+ "@bstage-sdk/core": "0.6.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^25.5.0",
40
40
  "tsup": "^8.0.0",
41
41
  "typescript": "^5.5.0",
42
- "@bstage-sdk/design": "0.5.0"
42
+ "@bstage-sdk/design": "0.6.1"
43
43
  },
44
44
  "license": "MIT",
45
45
  "scripts": {