create-saas-starter-workspace 0.1.4 → 0.1.7

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,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7 — 2026-06-14
4
+
5
+ - connect-repos 실패 안내를 코드스페이스 현실에 맞게 정정: 코드스페이스 기본 토큰은 레포 생성 권한이 없어, 복구는 `unset GITHUB_TOKEN GH_TOKEN` → `gh auth login --web -s repo` → 재실행이 필요하다(이전 안내엔 unset 단계가 빠져 있었다). 실측으로 확인된 흐름.
6
+
7
+ ## 0.1.6 — 2026-06-13
8
+
9
+ - **소유 모델: 세 레이어를 학생 GitHub 계정에 자동 백업.** 새 `scripts/connect-repos.mjs`가 루트·`web/`·`mobile/`을 각각 private 원격 `<name>-workspace`·`<name>-web`·`<name>-mobile`에 연결한다(`gh repo create --source --push`). 멱등·비차단이며 부트스트랩(setup.sh/ps1·코드스페이스)이 gh 로그인 직후 한 번 호출한다. 소유(git 연결)와 배포(`/go-live`·`/kickoff`의 클라우드 프로비저닝)를 분리 — 코드스페이스 휘발성 대비 + go-live 이전 작업의 백업.
10
+ - doctor에 GitHub 백업 점검 추가(레이어별 origin 연결 여부, 읽기 전용). 미연결 ✗ → `node scripts/connect-repos.mjs`.
11
+ - mobile 네이밍을 레포와 일치시켜 대칭화: Expo slug·package.json·문서 제목을 `<name>-app` → `<name>-mobile`. (레포 3종이 `-workspace`/`-web`/`-mobile`로 정렬된다.)
12
+ - `/go-live`는 더 이상 GitHub repo를 만들지 않는다 — 이미 있는 `<name>-web` 위에 클라우드만 붙인다. 워크스페이스 `AGENTS.md` 불변식을 3-레포(루트=컨텍스트 레이어 자기 repo, web/mobile 독립) 모델로 정정.
13
+ - mobile `AGENTS.md`의 실재하지 않던 "CI checksum" 참조를 `pnpm run doctor` 브릿지 점검으로 정정.
14
+
15
+ ## 0.1.5 — 2026-06-13
16
+
17
+ - 스캐폴드가 git을 처음부터 초기화한다: 루트(컨텍스트 레이어)·`web/`·`mobile/` 각각 독립 repo + 첫 커밋. 루트 `.gitignore` 화이트리스트가 전제하던 구조의 완성 — 1일차부터 undo 안전망이 생기고, 부모 폴더가 git repo인 환경(Codespaces)에서 git 명령이 바깥 강의 레포로 새는 사고를 차단한다. `/go-live`·`/kickoff`는 기존 repo에 리모트만 붙이면 된다. git 부재·identity 미설정은 조용히 건너뛴다(doctor가 git 부재를 짚는다).
18
+
19
+ ## 0.1.4 — 2026-06-12
20
+
21
+ - 내용 변경 없는 재출판(버전만 상승).
22
+
3
23
  ## 0.1.3 — 2026-06-12
4
24
 
5
25
  - 문서·doctor 안내의 점검 명령을 `pnpm run doctor`로 정정 — `doctor`는 pnpm 내장 명령이라 `pnpm doctor`는 우리 스크립트 대신 내장 명령(무출력)을 실행했다.
@@ -11,9 +11,9 @@ dev/prod 클라우드 환경을 뚫어 첫 배포를 진행한다. 최신 외부
11
11
 
12
12
  ## 1. 이름·GitHub repo
13
13
 
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와 일치.)
14
+ 이름은 워크스페이스 루트 `workspace.json`의 `name`(생성 시 입력한 프로젝트명)이다. 리소스 기준 이름: GitHub repo = `<name>-web`, Vercel 프로젝트 = `<name>`, Supabase = `<name>-dev`/`-prod`, Sentry 프로젝트 = `<name>`. **폴더명(`web`)을 리소스 이름으로 쓰지 않는다** — 특히 `vercel link`는 폴더명 `web`을 기본 프로젝트명으로 제안하므로 그대로 수락하지 말고 `<name>`으로 바꾼다. (앱 단계의 mobile repo `<name>-mobile`도 셋업이 이미 생성했다 — package.json·Expo slug와 일치.)
15
15
 
16
- private repo를 생성한다. secret scanning push protection은 private free에서 미지원이고 GitHub Advanced Security 유료 기능이다. 9-6에서 점검만 skip한다.
16
+ **GitHub repo는 이미 있다.** 셋업의 `connect-repos`가 `<name>-web` private repo를 만들어 `web/`의 origin으로 연결·push해 두었다 — 새로 `gh repo create`·`git init` 하지 않는다. 없다면(doctor가 ✗로 짚는다) 워크스페이스 루트에서 `node scripts/connect-repos.mjs`로 먼저 만든 뒤 진행한다. go-live는 이 repo 위에 클라우드를 붙이고 첫 배포까지 잇는 일이다. secret scanning push protection은 private free에서 미지원이고 GitHub Advanced Security 유료 기능이다. 9-6에서 점검만 skip한다.
17
17
 
18
18
  ## 2. Supabase dev + prod
19
19
 
@@ -10,14 +10,16 @@
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 run doctor`). 진단과 안내까지만 하고 자동 수리는 하지 않는다. Owner가 ✗ 해결을 부탁하면 doctor의 → 안내를 따라 수행하고 다시 doctor로 검증한다.
13
+ - `scripts/doctor.mjs` — 환경·로그인·백업 점검(`pnpm run doctor`). 진단과 안내까지만 하고 자동 수리는 하지 않는다. Owner가 ✗ 해결을 부탁하면 doctor의 → 안내를 따라 수행하고 다시 doctor로 검증한다.
14
+ - `scripts/connect-repos.mjs` — 세 레이어(루트·`web/`·`mobile/`)를 Owner의 GitHub 계정에 private 원격으로 연결·백업(`<name>-workspace`·`<name>-web`·`<name>-mobile`). 셋업이 한 번 호출하며, 멱등·비차단이다. doctor가 ✗(미연결)를 짚으면 이 스크립트를 다시 돌린다.
14
15
  - `workspace.json` — 생성기가 기록한 메타(프로젝트명·선택한 코딩 에이전트). doctor가 읽는다.
15
16
 
16
17
  ## 불변식
17
18
 
18
19
  - 각 서비스는 독립 repo다. 서로 import하지 않고, 공유는 복사된 계약(`ssb/`)으로만 한다.
19
20
  - web은 단독으로 완결된다. mobile은 그 위에 얹는 선택적 레이어이고, web은 mobile에 의존하지 않는다.
20
- - 클라우드 프로비저닝·git·배포 명령은 해당 서비스 폴더 안에서 실행한다. `/go-live`는 `web/`에서(web이 배포 단위), 빌드·출시는 `mobile/`에서. 워크스페이스 루트를 repo로 묶거나 `web/`·`mobile/`을 부모 git에 add하지 않는다.
21
+ - 레이어가 각자 독립 git repo다: 루트(컨텍스트 레이어)·`web/`·`mobile/`. 루트 repo는 `.gitignore` 화이트리스트로 컨텍스트 레이어(에이전트 도구·계약·문서)만 추적하고 `web/`·`mobile/`은 추적하지 않는다(모노레포로 묶지 않는다 — 부모 git에 add하지 않는다). 셋은 셋업의 `connect-repos`가 Owner GitHub 계정의 private 원격 `<name>-workspace`·`<name>-web`·`<name>-mobile`에 각각 연결한다.
22
+ - 소유(git 연결)와 배포(클라우드)는 분리한다. 코드 백업은 셋 다 위 원격으로, 클라우드 프로비저닝·배포는 해당 서비스 폴더에서: `/go-live`는 `web/`에서(web이 배포 단위), 앱 빌드·출시는 `mobile/`에서.
21
23
  - 에이전트 도구(스킬·`.claude/` 설정)는 워크스페이스 루트에만 둔다. `web/`·`mobile/`은 도구가 없는 순수 코드이고, 각자 자기 `AGENTS.md`(그 repo를 직접 손보는 사람용)만 둔다.
22
24
  - 한 서비스에만 속한 지식은 그 서비스 repo가 진다. 서비스를 가로지르는 맥락만 여기가 진다.
23
25
 
@@ -1,4 +1,4 @@
1
- # my-space-app
1
+ # my-space-mobile
2
2
 
3
3
  배포된 prod 웹을 https로 WebView에 띄우는 얇은 셸이다(React Native + Expo). 모든 화면·기능·로그인은 웹(Next.js)의 것이고, 셸은 화면을 만들지 않는다. 웹뷰가 막거나 못 하는 네이티브 공백만 메운다: 오프라인 화면, 첫 로드와 크래시 재마운트 한정의 로딩·에러 오버레이, Android 하드웨어 뒤로가기, 외부 링크의 시스템 브라우저 라우팅, safe-area inset 주입, 카메라·마이크·파일 업로드 권한 배선. SSOT는 언제나 웹이다.
4
4
 
@@ -34,7 +34,7 @@ docs/web-adapter/ # 웹 측 브릿지 어댑터(/kickoff이 웹 repo에
34
34
 
35
35
  어기면 빌드·심사·보안이 깨진다. 상세는 가이드 스킬에 있고, 여기엔 규칙만 둔다.
36
36
 
37
- 1. **ssb 브릿지가 계약의 SSOT다.** `src/bridge/contract.ts`·`reader.ts`는 웹과 바이트 단위로 동일하게 동기화되고, CI checksum이 어긋남을 막는다. 계약은 추가만 허용한다(타입·옵셔널 필드만 추가, 제거·이름변경·필수화 금지). reader는 tolerant하다(모르는·깨진 메시지는 `UNKNOWN`, 예외를 던지지 않음). 토큰은 브릿지로 보내지 않는다. → `bridge-guide`
37
+ 1. **ssb 브릿지가 계약의 SSOT다.** `src/bridge/contract.ts`·`reader.ts`는 웹과 바이트 단위로 동일하게 동기화되고, `pnpm run doctor`의 브릿지 점검이 어긋남을 막는다. 계약은 추가만 허용한다(타입·옵셔널 필드만 추가, 제거·이름변경·필수화 금지). reader는 tolerant하다(모르는·깨진 메시지는 `UNKNOWN`, 예외를 던지지 않음). 토큰은 브릿지로 보내지 않는다. → `bridge-guide`
38
38
 
39
39
  2. **세션 핸드오프는 opt-in이고 v1엔 호출자가 없다.** 기본 로그인은 웹이 직접 처리하고 셸은 관여하지 않는다.
40
40
  - 흐름: 네이티브 로그인 → POST `/auth/app-bridge` + short-TTL 1회용 nonce → Set-Cookie 303. URL에 `?token=`을 넣지 않는다(유출).
@@ -1,4 +1,4 @@
1
- # my-space-app
1
+ # my-space-mobile
2
2
 
3
3
  이미 배포된 웹 서비스를 iOS·Android 스토어 앱으로 감싸는 스타터입니다. 결정은 Owner가 하고, 코드·빌드·배포는 AI 코딩 에이전트(권장: Claude Code)가 맡습니다.
4
4
 
@@ -68,8 +68,8 @@ export default ({ config }: ConfigContext): ExpoConfig => {
68
68
  // Expo 기본값 위에 우리 설정을 덮어쓴다. app.config.ts가 유일한 설정 소스다(app.json 없음).
69
69
  ...config,
70
70
  name: appName,
71
- // slug: Expo 프로젝트 식별 슬러그. 보통 레포명과 맞춥니다(영소문자+하이픈).
72
- slug: "my-space-app",
71
+ // slug: Expo 프로젝트 식별 슬러그. 레포명(`<name>-mobile`)과 맞춥니다(영소문자+하이픈).
72
+ slug: "my-space-mobile",
73
73
  version: "1.0.0", // 마케팅 버전(CFBundleShortVersionString / versionName).
74
74
  // 재제출할 때 네이티브가 바뀌면 이 값을 올려야 합니다(DESIGN 결정 9: autoIncrement는
75
75
  // buildNumber만 올림 — marketing version은 수동, 안 올리면 ITMS-90186).
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "my-space-app",
2
+ "name": "my-space-mobile",
3
3
  "version": "1.0.0",
4
4
  "main": "index.ts",
5
5
  "dependencies": {
@@ -0,0 +1,93 @@
1
+ // connect-repos — 워크스페이스의 세 로컬 git 레포를 내 GitHub 계정의 private 원격에 연결·백업한다.
2
+ //
3
+ // 레이어 = 레포: 루트(컨텍스트) → `<name>-workspace`, web/ → `<name>-web`, mobile/ → `<name>-mobile`.
4
+ // 소유(여기서 하는 git 연결)와 배포(/go-live·/kickoff의 클라우드 프로비저닝)는 분리돼 있다 —
5
+ // 이 스크립트는 클라우드를 건드리지 않고 "내 코드를 내 GitHub에 백업"만 한다.
6
+ //
7
+ // 호출: 부트스트랩(setup.sh/ps1·코드스페이스)이 gh 로그인 직후 한 번. 직접 실행도 안전하다:
8
+ // node scripts/connect-repos.mjs
9
+ //
10
+ // 원칙:
11
+ // - 멱등: origin이 이미 있으면 건너뛴다. 몇 번을 돌려도 안전하다.
12
+ // - 비차단: 무슨 일이 있어도 exit 0. 준비 상태의 판정은 doctor가 한다.
13
+ // - gh 미설치·미인증·권한부족이면 조용히 멈추고 다음 행동만 안내한다.
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const WIN = process.platform === "win32";
21
+
22
+ function workspaceName() {
23
+ try {
24
+ const meta = JSON.parse(readFileSync(path.join(ROOT, "workspace.json"), "utf8"));
25
+ if (meta.name) return meta.name;
26
+ } catch {
27
+ /* 메타가 없으면 폴더명으로 대체 */
28
+ }
29
+ return path.basename(ROOT);
30
+ }
31
+
32
+ // Windows에서 gh·git은 .cmd 셔임이라 shell 해석이 필요하다. 값에 공백이 없어(레포명은 검증됨)
33
+ // shell:true + 배열 인자가 안전하다.
34
+ const sh = (cmd, args, cwd) => spawnSync(cmd, args, { cwd, encoding: "utf8", shell: WIN });
35
+
36
+ const name = workspaceName();
37
+ const REPOS = [
38
+ { layer: "workspace", dir: ".", repo: `${name}-workspace` },
39
+ { layer: "web", dir: "web", repo: `${name}-web` },
40
+ { layer: "mobile", dir: "mobile", repo: `${name}-mobile` },
41
+ ];
42
+
43
+ console.log("\n▸ GitHub 백업 — 내 계정에 private 레포 3개로 연결");
44
+
45
+ if (sh("gh", ["--version"]).status !== 0) {
46
+ console.log(" ⚠ gh(GitHub CLI)가 없어 건너뜁니다. 설치 후 다시: node scripts/connect-repos.mjs");
47
+ process.exit(0);
48
+ }
49
+ if (sh("gh", ["auth", "status"]).status !== 0) {
50
+ console.log(" ⚠ GitHub 로그인이 안 되어 건너뜁니다.");
51
+ console.log(" 먼저 gh auth login --web --git-protocol https 로 로그인한 뒤 다시: node scripts/connect-repos.mjs");
52
+ process.exit(0);
53
+ }
54
+
55
+ let created = 0;
56
+ let skipped = 0;
57
+ let failed = 0;
58
+ for (const { layer, dir, repo } of REPOS) {
59
+ const cwd = path.join(ROOT, dir);
60
+ if (!existsSync(path.join(cwd, ".git"))) {
61
+ console.log(` ⚠ ${layer}: git 레포가 아니라 건너뜁니다.`);
62
+ failed++;
63
+ continue;
64
+ }
65
+ if (sh("git", ["remote", "get-url", "origin"], cwd).status === 0) {
66
+ console.log(` ✓ ${layer}: 이미 연결됨 (${repo})`);
67
+ skipped++;
68
+ continue;
69
+ }
70
+ // gh repo create: private 레포 생성 + origin 연결 + push 를 한 번에.
71
+ const r = sh("gh", ["repo", "create", repo, "--private", "--source", ".", "--remote", "origin", "--push"], cwd);
72
+ if (r.status === 0) {
73
+ console.log(` ✓ ${layer}: 생성·백업 완료 (${repo})`);
74
+ created++;
75
+ } else {
76
+ const msg = (r.stderr || r.stdout || "").trim().split("\n").filter(Boolean).pop() ?? "원인 미상";
77
+ console.log(` ⚠ ${layer}: 연결 실패 (${repo}) — ${msg}`);
78
+ failed++;
79
+ }
80
+ }
81
+
82
+ if (failed > 0) {
83
+ console.log("");
84
+ console.log(" 남은 ⚠는 보통 둘 중 하나입니다:");
85
+ console.log(" 1) 같은 이름의 레포가 이미 있음 → 지우거나 다른 프로젝트명으로 다시 만드세요.");
86
+ console.log(" 2) 레포 생성 권한 부족 — 코드스페이스라면 아래를 순서대로 실행하세요:");
87
+ console.log(" unset GITHUB_TOKEN GH_TOKEN");
88
+ console.log(" gh auth login --git-protocol https --web -s repo (나오는 주소를 Cmd/Ctrl+클릭해 코드로 승인)");
89
+ console.log(" node scripts/connect-repos.mjs");
90
+ }
91
+ console.log(` (연결 ${created} · 기존 ${skipped} · 남음 ${failed})`);
92
+ console.log("");
93
+ process.exit(0);
@@ -280,6 +280,28 @@ function checkBridgeSync() {
280
280
  };
281
281
  }
282
282
 
283
+ // GitHub 백업 — 각 레이어의 로컬 git이 내 GitHub 계정의 원격(origin)에 연결됐는가.
284
+ // 연결은 connect-repos.mjs가 하고, 여기서는 "연결됐나"만 본다(읽기 전용·오프라인).
285
+ function checkRemote(layer, sub) {
286
+ const cwd = sub ? path.join(ROOT, sub) : ROOT;
287
+ if (!existsSync(path.join(cwd, ".git")))
288
+ return {
289
+ status: "fail",
290
+ label: `GitHub 백업 (${layer})`,
291
+ detail: "git 레포가 아닙니다 (생성이 비정상).",
292
+ fix: "워크스페이스를 지우고 강의의 설치 명령을 다시 실행하세요.",
293
+ };
294
+ const r = run("git remote get-url origin", { cwd, timeout: 8000 });
295
+ if (r.ok && r.stdout)
296
+ return { status: "ok", label: `GitHub 백업 (${layer}) — ${r.stdout.split("/").pop().replace(/\.git$/, "")}` };
297
+ return {
298
+ status: "fail",
299
+ label: `GitHub 백업 (${layer})`,
300
+ detail: "내 GitHub 계정에 아직 연결되지 않았습니다 (작업이 백업되지 않습니다).",
301
+ fix: "node scripts/connect-repos.mjs (gh 로그인 후)",
302
+ };
303
+ }
304
+
283
305
  // ---------- 실행 ----------
284
306
 
285
307
  const meta = readWorkspaceMeta();
@@ -289,9 +311,9 @@ console.log(bold(`🩺 doctor — ${meta.name ?? path.basename(ROOT)} 환경 점
289
311
  console.log(dim(" 각 항목을 점검합니다. 네트워크 확인이 포함되어 몇십 초 걸릴 수 있습니다."));
290
312
 
291
313
  const sections = [
292
- { title: "공통", blocking: true, checks: [checkNode, checkPnpm, checkGit, () => checkAgent(meta), checkGh, checkVercel, checkBridgeSync] },
293
- { title: "웹", blocking: true, checks: [() => checkDeps("web"), checkSupabase, checkSentry] },
294
- { title: "앱", blocking: false, note: "'앱으로 확장' 챕터 전까지는 ✗여도 정상입니다.", checks: [() => checkDeps("mobile"), checkEas] },
314
+ { title: "공통", blocking: true, checks: [checkNode, checkPnpm, checkGit, () => checkAgent(meta), checkGh, checkVercel, checkBridgeSync, () => checkRemote("workspace", null)] },
315
+ { title: "웹", blocking: true, checks: [() => checkDeps("web"), () => checkRemote("web", "web"), checkSupabase, checkSentry] },
316
+ { title: "앱", blocking: false, note: "'앱으로 확장' 챕터 전까지는 ✗여도 정상입니다.", checks: [() => checkDeps("mobile"), () => checkRemote("mobile", "mobile"), checkEas] },
295
317
  ];
296
318
 
297
319
  let blockingFails = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-saas-starter-workspace",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "강의용 SaaS 워크스페이스(web + mobile + 에이전트 도구)를 한 번에 만드는 생성기. 생성 → 의존성 설치 → doctor 점검까지 한 흐름.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
package/src/scaffold.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  // 치환 후 트리 전체를 스캔해 남은 토큰이 없음을 단언한다 — SSOT에서 토큰이 새 파일로
6
6
  // 번지면 stage·테스트가 그린인 채로 수강생 산출물에 누수되는 사고(드리프트)를 게이트로 막는다.
7
7
  import { promises as fs } from "node:fs";
8
+ import { spawnSync } from "node:child_process";
8
9
  import path from "node:path";
9
10
 
10
11
  export const TOKEN = "my-space";
@@ -90,6 +91,20 @@ async function assertNoLeftoverToken(dir, base = dir, found = []) {
90
91
  return found;
91
92
  }
92
93
 
94
+ // 루트(컨텍스트 레이어)·web/·mobile/(각자 독립 repo)에 git을 처음부터 만든다 — 루트 .gitignore의
95
+ // 화이트리스트가 전제하는 구조를 스캐폴드가 실제로 완성한다. 1일차부터 undo 안전망이 생기고,
96
+ // 부모 폴더가 git repo인 환경(Codespaces)에서 git 명령이 바깥 레포로 새는 사고를 차단한다.
97
+ // go-live/kickoff는 여기에 리모트만 붙인다. git 부재·identity 미설정 등 어떤 실패도 스캐폴드를
98
+ // 막지 않는다(git 부재는 doctor가 짚고, identity가 없으면 staged 상태로 두어 히스토리를 오염시키지 않는다).
99
+ function initGitRepos(dest) {
100
+ const git = (args, cwd) => spawnSync("git", args, { cwd, stdio: "ignore" }).status === 0;
101
+ for (const rel of ["", "web", "mobile"]) {
102
+ const cwd = path.join(dest, rel);
103
+ if (!git(["init", "-b", "main"], cwd) && !git(["init"], cwd)) continue;
104
+ if (git(["add", "-A"], cwd)) git(["commit", "-q", "-m", "init: saas-starter-workspace 템플릿"], cwd);
105
+ }
106
+ }
107
+
93
108
  export async function scaffold({ templateDir, dest, name, agent, createdWith }) {
94
109
  // 실패 시 절반 생성된 폴더를 남기지 않는다 — fresh 디렉토리만 만들고, 실패하면 통째로 지운다.
95
110
  await fs.mkdir(dest, { recursive: false });
@@ -105,6 +120,7 @@ export async function scaffold({ templateDir, dest, name, agent, createdWith })
105
120
  2,
106
121
  ) + "\n",
107
122
  );
123
+ initGitRepos(dest);
108
124
  } catch (e) {
109
125
  await fs.rm(dest, { recursive: true, force: true });
110
126
  throw e;