create-saas-starter-workspace 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1 (미출판)
3
+ ## 0.1.2 (미출판)
4
+
5
+ - 문서·doctor 안내의 점검 명령을 `pnpm run doctor`로 정정 — `doctor`는 pnpm 내장 명령이라 `pnpm doctor`는 우리 스크립트 대신 내장 명령(무출력)을 실행했다.
6
+
7
+ - doctor [웹]에 Sentry CLI 점검 추가(`sentry-cli info`의 Unauthorized 파싱 — exit 0이어도 미로그인 판별). setup이 Sentry 로그인까지 처리하게 된 변경과 짝.
8
+
9
+ ## 0.1.1 — 2026-06-12
4
10
 
5
11
  - doctor [공통]에 브릿지 계약 점검 추가: `ssb/`(원본)와 `web/lib/bridge/`·`mobile/src/bridge/` 사본의 byte-동일성을 단언한다. 템플릿 문서들이 약속하던 "CI checksum"(실재하지 않던 게이트)을 doctor 기준으로 전부 정정.
6
- - 워크스페이스 루트의 자기명칭도 프로젝트 이름으로 렌더: 루트 `package.json`(`<이름>-workspace`)과 `README.md`·`AGENTS.md` 제목. `pnpm doctor` 등 루트 명령의 출력에 학생 프로젝트명이 보인다.
12
+ - 워크스페이스 루트의 자기명칭도 프로젝트 이름으로 렌더: 루트 `package.json`(`<이름>-workspace`)과 `README.md`·`AGENTS.md` 제목. `pnpm run doctor` 등 루트 명령의 출력에 학생 프로젝트명이 보인다.
7
13
  - bin이 `dist/` 부재 시(개발 트리 직접 실행) `npm run stage` 안내와 함께 명확히 실패한다.
8
14
  - 패키지 내부 레이아웃 정리: 템플릿 SSOT를 패키지 내 `template/`(소스)로 이동, stage 산출물은 `templates/` → `dist/`(표준 src/dist 관례). 소비자(npx) 영향 없음.
9
15
 
package/bin/index.mjs CHANGED
@@ -184,7 +184,7 @@ async function main() {
184
184
  if (installFailed.length > 0) {
185
185
  console.log(`\n⚠ 의존성 설치가 끝나지 않았습니다 (${installFailed.join(", ")}). 이렇게 마무리하세요:`);
186
186
  for (const sub of installFailed) console.log(` cd ${name}/${sub} && pnpm install --frozen-lockfile`);
187
- console.log(` 그다음 cd ${name} && pnpm doctor 로 다시 확인합니다.`);
187
+ console.log(` 그다음 cd ${name} && pnpm run doctor 로 다시 확인합니다.`);
188
188
  }
189
189
  console.log(`\n다음 단계:`);
190
190
  console.log(` 1. cd ${name}`);
@@ -6,7 +6,7 @@ user-invocable: false
6
6
 
7
7
  # 앱↔웹 대화 규격 (ssb 계약)
8
8
 
9
- 앱과 웹은 정해진 형식의 메시지로만 대화한다. 그 형식 정의는 양쪽이 똑같은 파일 하나(`src/bridge/contract.ts` + `reader.ts`)로 공유한다. 어긋나면 워크스페이스 루트의 `pnpm doctor`가 잡아낸다.
9
+ 앱과 웹은 정해진 형식의 메시지로만 대화한다. 그 형식 정의는 양쪽이 똑같은 파일 하나(`src/bridge/contract.ts` + `reader.ts`)로 공유한다. 어긋나면 워크스페이스 루트의 `pnpm run doctor`가 잡아낸다.
10
10
 
11
11
  ## 봉투 & 전달 방식
12
12
 
@@ -52,7 +52,7 @@ user-invocable: false
52
52
  4. inbound라면 `router.ts`의 `handleInbound` switch에 case를 추가한다. UNKNOWN은 그대로 무시되니 안전하다.
53
53
  5. 새 능력이면 `capabilities.ts`의 `APP_CAPABILITIES`에 capability 문자열을 추가한다. 웹이 HELLO로 감지한다.
54
54
  6. 네이티브 변경이므로 앱을 재빌드한다(OTA 아님 — `eas-deploy-guide`).
55
- 7. 동기화한다. 같은 변경을 `ssb/`(원본)와 `web/lib/bridge/`·`mobile/src/bridge/` 세 곳에 바이트 단위로 동일하게 반영하고, 워크스페이스 루트에서 `pnpm doctor`로 동일성을 확인한다.
55
+ 7. 동기화한다. 같은 변경을 `ssb/`(원본)와 `web/lib/bridge/`·`mobile/src/bridge/` 세 곳에 바이트 단위로 동일하게 반영하고, 워크스페이스 루트에서 `pnpm run doctor`로 동일성을 확인한다.
56
56
 
57
57
  > 새 기능은 앱에 먼저 들어가고(HELLO capability), 웹이 나중에 감지해 쓴다. 순서가 어긋나도 웹 코드가 capability 게이트로 막혀 있어 안전하다.
58
58
 
@@ -11,7 +11,9 @@ dev/prod 클라우드 환경을 뚫어 첫 배포를 진행한다. 최신 외부
11
11
 
12
12
  ## 1. 이름·GitHub repo
13
13
 
14
- 이름을 먼저 결정한다. 이후 모든 리소스가 이 이름을 기반으로 한다. private repo 생성한다. secret scanning push protection은 private free에서 미지원이고 GitHub Advanced Security 유료 기능이다. 9-6에서 점검만 skip한다.
14
+ 이름을 먼저 결정한다. 기본값 = 워크스페이스 루트 `workspace.json`의 `name`(생성 시 입력한 프로젝트명). 이후 모든 리소스가 이 이름을 기반으로 한다: GitHub repo = `<name>`, Vercel 프로젝트 = `<name>`, Supabase = `<name>-dev`/`-prod`, Sentry 프로젝트 = `<name>`. **폴더명(`web`)을 리소스 이름으로 쓰지 않는다** — 특히 `vercel link`는 폴더명 `web`을 기본 프로젝트명으로 제안하므로 그대로 수락하지 말고 `<name>`으로 바꾼다. (앱 단계의 mobile repo는 `<name>-app` — package.json·Expo slug와 일치.)
15
+
16
+ private repo를 생성한다. secret scanning push protection은 private free에서 미지원이고 GitHub Advanced Security 유료 기능이다. 9-6에서 점검만 skip한다.
15
17
 
16
18
  ## 2. Supabase dev + prod
17
19
 
@@ -44,7 +44,7 @@ Owner confirm. 비용을 알린 뒤 계속할지 확정받고서야 다음 단
44
44
 
45
45
  ## 4. 웹 어댑터 (web/)
46
46
 
47
- 계약(`contract.ts`·`reader.ts`)은 이미 `web/lib/bridge/`에 바이트 단위로 동일하게 설치돼 있고(`pnpm doctor`가 동일성을 점검한다) 다시 복사하지 않는다. 웹이 앱과 실제로 대화하게 만드는 얇은 옵트인 레이어만 필요할 때 추가한다. 규약·보안 불변식은 인라인하지 않고 `bridge-guide`를 따른다.
47
+ 계약(`contract.ts`·`reader.ts`)은 이미 `web/lib/bridge/`에 바이트 단위로 동일하게 설치돼 있고(`pnpm run doctor`가 동일성을 점검한다) 다시 복사하지 않는다. 웹이 앱과 실제로 대화하게 만드는 얇은 옵트인 레이어만 필요할 때 추가한다. 규약·보안 불변식은 인라인하지 않고 `bridge-guide`를 따른다.
48
48
 
49
49
  세션 핸드오프는 fast-follow다. v1은 호출자가 없다. 기본 로그인은 웹뷰 안의 웹 로그인이 그대로 동작하고, 핸드오프 라우트(POST `/auth/app-bridge` + nonce)는 네이티브 소셜 로그인을 켤 때만 살아난다. 토큰은 브릿지·URL로 보내지 않는다(1회용 nonce만 보낸다).
50
50
 
@@ -25,7 +25,7 @@ Owner의 행동이 필요한 부분은 AskUserQuestion으로 해결한다.
25
25
 
26
26
  ## 의존성
27
27
 
28
- 워크스페이스 루트의 `package.json`은 `pnpm doctor`(환경 점검) 진입점만 가진다 — 루트에 의존성을 설치하지 않는다. `web/`과 `mobile/`은 각자 독립 프로젝트이므로 의존성도 각자 설치한다.
28
+ 워크스페이스 루트의 `package.json`은 `pnpm run doctor`(환경 점검) 진입점만 가진다 — 루트에 의존성을 설치하지 않는다. `web/`과 `mobile/`은 각자 독립 프로젝트이므로 의존성도 각자 설치한다.
29
29
 
30
30
  - `web/`에서 `pnpm install` 후 `pnpm audit`.
31
31
  - `mobile/`에서도 동일하게 한다.
@@ -10,7 +10,7 @@
10
10
  - `mobile/` — 배포된 web을 WebView로 감싸는 네이티브 셸 (Expo). 자기 git → EAS → 스토어로 배포한다. 웹뷰가 못 메우는 네이티브 공백(푸시·홈 화면 아이콘·오프라인 화면)만 채운다.
11
11
  - `ssb/` — web ↔ mobile postMessage 계약의 SSOT. `web/lib/bridge/`와 `mobile/src/bridge/`에 바이트 단위로 동일하게 복사된다.
12
12
  - `.claude/` — 에이전트 도구(스킬·설정). 워크스페이스 루트에만 둔다.
13
- - `scripts/doctor.mjs` — 환경·로그인 점검(`pnpm doctor`). 진단과 안내까지만 하고 자동 수리는 하지 않는다. Owner가 ✗ 해결을 부탁하면 doctor의 → 안내를 따라 수행하고 다시 doctor로 검증한다.
13
+ - `scripts/doctor.mjs` — 환경·로그인 점검(`pnpm run doctor`). 진단과 안내까지만 하고 자동 수리는 하지 않는다. Owner가 ✗ 해결을 부탁하면 doctor의 → 안내를 따라 수행하고 다시 doctor로 검증한다.
14
14
  - `workspace.json` — 생성기가 기록한 메타(프로젝트명·선택한 코딩 에이전트). doctor가 읽는다.
15
15
 
16
16
  ## 불변식
@@ -11,7 +11,7 @@
11
11
  ├── web/ 웹 SaaS (Next.js · Supabase · Vercel). 화면·기능·데이터의 본체.
12
12
  ├── mobile/ 그 웹을 감싸는 스토어 앱 (Expo · EAS).
13
13
  ├── ssb/ web ↔ mobile 통신 계약. 손댈 일은 거의 없습니다.
14
- ├── scripts/ 환경 점검 도구(`pnpm doctor`).
14
+ ├── scripts/ 환경 점검 도구(`pnpm run doctor`).
15
15
  └── .claude/ 에이전트 도구(스킬·설정).
16
16
  ```
17
17
 
@@ -10,7 +10,7 @@
10
10
  2. 처음 열면 에이전트가 로그인(구독 계정)을 안내합니다. 한 번만 하면 됩니다.
11
11
  3. 그다음부터는 아래 명령을 입력하거나, 평소 말로 부탁하면 됩니다.
12
12
 
13
- > 환경이 멀쩡한지 궁금할 땐 언제든 터미널에서 `pnpm doctor`를 실행하세요. 도구·로그인 상태를 ✓/✗로 보여주고, ✗마다 해결 방법을 알려줍니다.
13
+ > 환경이 멀쩡한지 궁금할 땐 언제든 터미널에서 `pnpm run doctor`를 실행하세요. 도구·로그인 상태를 ✓/✗로 보여주고, ✗마다 해결 방법을 알려줍니다.
14
14
 
15
15
  ## 2. 명령 순서
16
16
 
@@ -37,7 +37,7 @@
37
37
  ├── web/ 당신의 웹 서비스 (Next.js → Vercel). 모든 화면·기능·데이터의 본체.
38
38
  ├── mobile/ 그 웹을 감싸는 스토어 앱 (Expo → 앱·플레이스토어). 푸시·홈 화면 아이콘처럼 앱만의 것.
39
39
  ├── ssb/ web ↔ mobile이 주고받는 통신 계약. 손대지 않아도 됩니다.
40
- ├── scripts/ 환경 점검 도구(`pnpm doctor`).
40
+ ├── scripts/ 환경 점검 도구(`pnpm run doctor`).
41
41
  └── .claude/ 위 명령(스킬)이 있는 곳.
42
42
  ```
43
43
 
@@ -21,7 +21,7 @@
21
21
  | `src/bridge/contract.ts` | `lib/bridge/contract.ts` |
22
22
  | `src/bridge/reader.ts` | `lib/bridge/reader.ts` |
23
23
 
24
- 두 사본은 바이트 단위로 동일해야 한다. 어긋나면 양쪽이 메시지 형태를 다르게 해석해 브릿지가 오작동하므로, 워크스페이스 루트의 `pnpm doctor`가 동일성을 점검한다.
24
+ 두 사본은 바이트 단위로 동일해야 한다. 어긋나면 양쪽이 메시지 형태를 다르게 해석해 브릿지가 오작동하므로, 워크스페이스 루트의 `pnpm run doctor`가 동일성을 점검한다.
25
25
 
26
26
  ```bash
27
27
  cmp -s mobile/src/bridge/contract.ts web/lib/bridge/contract.ts || exit 1
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * App ↔ Web bridge contract — SINGLE SOURCE OF TRUTH.
5
5
  *
6
6
  * This file is copied byte-for-byte into the web (web/lib/bridge/) and the app
7
- * (mobile/src/bridge/). All copies MUST stay identical; `pnpm doctor` at the
7
+ * (mobile/src/bridge/). All copies MUST stay identical; `pnpm run doctor` at the
8
8
  * workspace root checks this.
9
9
  *
10
10
  * Invariants (never break these):
@@ -1,6 +1,6 @@
1
1
  // doctor — 워크스페이스 생성 이후의 환경·로그인 상태를 결정적으로 점검한다.
2
2
  //
3
- // 실행: 워크스페이스 루트에서 `pnpm doctor` (또는 `node scripts/doctor.mjs`).
3
+ // 실행: 워크스페이스 루트에서 `pnpm run doctor` (또는 `node scripts/doctor.mjs`).
4
4
  // 의존성 0 — node 내장 모듈만 쓴다. 루트에는 node_modules가 없어도 돈다.
5
5
  //
6
6
  // 원칙:
@@ -191,7 +191,7 @@ function checkVercel() {
191
191
  const r = run("vercel whoami", { timeout: 20000 });
192
192
  if (r.ok) return { status: "ok", label: `Vercel CLI — 로그인됨 (${r.stdout.split("\n").pop()})` };
193
193
  if (r.timedOut || r.networkIssue)
194
- return { status: "warn", label: "Vercel CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm doctor 재실행" };
194
+ return { status: "warn", label: "Vercel CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm run doctor 재실행" };
195
195
  return { status: "fail", label: "Vercel CLI", detail: "로그인이 안 되어 있습니다.", fix: "vercel login" };
196
196
  }
197
197
 
@@ -207,7 +207,7 @@ function checkSupabase() {
207
207
  const r = run(`"${bin}" projects list`, { cwd: path.join(ROOT, "web"), timeout: 25000 });
208
208
  if (r.ok) return { status: "ok", label: "Supabase CLI — 로그인됨" };
209
209
  if (r.timedOut || r.networkIssue)
210
- return { status: "warn", label: "Supabase CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm doctor 재실행" };
210
+ return { status: "warn", label: "Supabase CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm run doctor 재실행" };
211
211
  return {
212
212
  status: "fail",
213
213
  label: "Supabase CLI",
@@ -227,7 +227,7 @@ function checkEas() {
227
227
  const r = run("eas whoami", { timeout: 20000 });
228
228
  if (r.ok) return { status: "ok", label: `EAS CLI — 로그인됨 (${r.stdout.split("\n").pop()})` };
229
229
  if (r.timedOut || r.networkIssue)
230
- return { status: "warn", label: "EAS CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm doctor 재실행" };
230
+ return { status: "warn", label: "EAS CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm run doctor 재실행" };
231
231
  return {
232
232
  status: "fail",
233
233
  label: "EAS CLI",
@@ -236,6 +236,22 @@ function checkEas() {
236
236
  };
237
237
  }
238
238
 
239
+ function checkSentry() {
240
+ if (!installed("sentry-cli"))
241
+ return { status: "fail", label: "Sentry CLI", detail: "설치되어 있지 않습니다.", fix: "npm install -g @sentry/cli" };
242
+ // sentry-cli info는 미로그인이어도 exit 0 — 출력의 Unauthorized로 판별한다.
243
+ const r = run("sentry-cli info", { timeout: 15000 });
244
+ if (r.ok && !/unauthorized/i.test(r.stdout + r.stderr)) return { status: "ok", label: "Sentry CLI — 로그인됨" };
245
+ if (r.timedOut || r.networkIssue)
246
+ return { status: "warn", label: "Sentry CLI", detail: "네트워크 문제로 확인하지 못했습니다.", fix: "인터넷 연결 후 pnpm run doctor 재실행" };
247
+ return {
248
+ status: "fail",
249
+ label: "Sentry CLI",
250
+ detail: "로그인이 안 되어 있습니다.",
251
+ fix: "sentry-cli login --global → 열린 페이지에서 [Create New Token] → 토큰 복사 → 터미널의 Enter your token:에 붙여넣기",
252
+ };
253
+ }
254
+
239
255
  // 브릿지 계약 동기 — ssb/(원본)와 web/mobile 사본의 byte-동일성.
240
256
  // 어긋난 채 배포되면 앱·웹이 메시지를 다르게 해석해 조용히 오작동한다.
241
257
  function checkBridgeSync() {
@@ -274,7 +290,7 @@ console.log(dim(" 각 항목을 점검합니다. 네트워크 확인이 포함
274
290
 
275
291
  const sections = [
276
292
  { title: "공통", blocking: true, checks: [checkNode, checkPnpm, checkGit, () => checkAgent(meta), checkGh, checkVercel, checkBridgeSync] },
277
- { title: "웹", blocking: true, checks: [() => checkDeps("web"), checkSupabase] },
293
+ { title: "웹", blocking: true, checks: [() => checkDeps("web"), checkSupabase, checkSentry] },
278
294
  { title: "앱", blocking: false, note: "'앱으로 확장' 챕터 전까지는 ✗여도 정상입니다.", checks: [() => checkDeps("mobile"), checkEas] },
279
295
  ];
280
296
 
@@ -303,13 +319,15 @@ for (const section of sections) {
303
319
 
304
320
  console.log("");
305
321
  if (blockingFails === 0) {
306
- console.log(green(bold("✅ 개발 준비 완료")) + (warns ? dim(` (⚠ ${warns}개는 안내를 참고해 직접 확인하세요)`) : ""));
322
+ // [앱]까지 전부 ✓면 사실도 인정한다 "웹만 완료"로 읽히지 않게.
323
+ const verdict = appFails === 0 ? "✅ 웹·앱 개발 준비 모두 완료" : "✅ 웹 개발 준비 완료";
324
+ console.log(green(bold(verdict)) + (warns ? dim(` (⚠ ${warns}개는 안내를 참고해 직접 확인하세요)`) : ""));
307
325
  } else {
308
- console.log(red(bold(`✗ ${blockingFails}개 항목이 남았습니다.`)) + " 위의 → 안내를 따라 해결한 뒤 다시 실행하세요: " + bold("pnpm doctor"));
326
+ console.log(red(bold(`✗ ${blockingFails}개 항목이 남았습니다.`)) + " 위의 → 안내를 따라 해결한 뒤 다시 실행하세요: " + bold("pnpm run doctor"));
309
327
  console.log("");
310
328
  console.log("📋 직접 하기 어렵다면, 코딩 에이전트에 아래 문장을 그대로 붙여넣으세요:");
311
- console.log(bold(' "pnpm doctor를 실행해서 ✗ 항목을 확인하고, 각 항목의 → 안내를 따라 해결해줘.'));
312
- console.log(bold(' 브라우저 로그인이 필요한 단계는 멈추고 나에게 알려줘. 끝나면 pnpm doctor로 다시 검증해줘."'));
329
+ console.log(bold(' "pnpm run doctor를 실행해서 ✗ 항목을 확인하고, 각 항목의 → 안내를 따라 해결해줘.'));
330
+ console.log(bold(' 브라우저 로그인이 필요한 단계는 멈추고 나에게 알려줘. 끝나면 pnpm run doctor로 다시 검증해줘."'));
313
331
  }
314
332
  if (appFails > 0 && blockingFails === 0) {
315
333
  console.log(dim(`📦 [앱] ✗ ${appFails}개는 '앱으로 확장' 챕터에서 해결합니다. 지금은 무시해도 됩니다.`));
@@ -1,6 +1,6 @@
1
1
  # ssb — 앱↔웹 브릿지 계약 (SSOT)
2
2
 
3
- 이 폴더가 앱↔웹 통신 계약의 원본이다. 메시지는 `ns: "ssb"` 네임스페이스를 달아 페이지의 다른 postMessage 트래픽과 섞이지 않는다. 이 원본은 app(`mobile/src/bridge/`)과 web(`web/lib/bridge/`)에 바이트 단위로 동일하게 복사되어 있고, 워크스페이스 루트의 `pnpm doctor`가 세 사본의 동일성을 점검한다.
3
+ 이 폴더가 앱↔웹 통신 계약의 원본이다. 메시지는 `ns: "ssb"` 네임스페이스를 달아 페이지의 다른 postMessage 트래픽과 섞이지 않는다. 이 원본은 app(`mobile/src/bridge/`)과 web(`web/lib/bridge/`)에 바이트 단위로 동일하게 복사되어 있고, 워크스페이스 루트의 `pnpm run doctor`가 세 사본의 동일성을 점검한다.
4
4
 
5
5
  불변식 (상세는 `contract.ts` 헤더):
6
6
 
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * App ↔ Web bridge contract — SINGLE SOURCE OF TRUTH.
5
5
  *
6
6
  * This file is copied byte-for-byte into the web (web/lib/bridge/) and the app
7
- * (mobile/src/bridge/). All copies MUST stay identical; `pnpm doctor` at the
7
+ * (mobile/src/bridge/). All copies MUST stay identical; `pnpm run doctor` at the
8
8
  * workspace root checks this.
9
9
  *
10
10
  * Invariants (never break these):
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * App ↔ Web bridge contract — SINGLE SOURCE OF TRUTH.
5
5
  *
6
6
  * This file is copied byte-for-byte into the web (web/lib/bridge/) and the app
7
- * (mobile/src/bridge/). All copies MUST stay identical; `pnpm doctor` at the
7
+ * (mobile/src/bridge/). All copies MUST stay identical; `pnpm run doctor` at the
8
8
  * workspace root checks this.
9
9
  *
10
10
  * Invariants (never break these):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-saas-starter-workspace",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "강의용 SaaS 워크스페이스(web + mobile + 에이전트 도구)를 한 번에 만드는 생성기. 생성 → 의존성 설치 → doctor 점검까지 한 흐름.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",