create-ait-app 0.0.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.
Files changed (35) hide show
  1. package/README.md +79 -0
  2. package/bin/index.js +4 -0
  3. package/package.json +33 -0
  4. package/src/main.js +534 -0
  5. package/template/README.md +26 -0
  6. package/template/__default/__samples/iaa/src/hooks/useInAppAds.tsx +122 -0
  7. package/template/__default/__samples/iaa/src/pages/InAppAdsPage.css +72 -0
  8. package/template/__default/__samples/iaa/src/pages/InAppAdsPage.tsx +79 -0
  9. package/template/__default/__samples/iap/public/icon-document.png +0 -0
  10. package/template/__default/__samples/iap/src/hooks/useInAppPurchase.ts +117 -0
  11. package/template/__default/__samples/iap/src/pages/InAppPurchasePage.css +115 -0
  12. package/template/__default/__samples/iap/src/pages/InAppPurchasePage.tsx +119 -0
  13. package/template/__default/src/App.css +104 -0
  14. package/template/__default/src/App.tsx +45 -0
  15. package/template/__default/src/index.css +27 -0
  16. package/template/__default/src/main.tsx +10 -0
  17. package/template/__tds/__samples/iaa/src/hooks/useInAppAds.tsx +132 -0
  18. package/template/__tds/__samples/iaa/src/pages/InAppAdsPage.tsx +92 -0
  19. package/template/__tds/__samples/iap/public/icon-document.png +0 -0
  20. package/template/__tds/__samples/iap/src/hooks/useInAppPurchase.ts +124 -0
  21. package/template/__tds/__samples/iap/src/pages/InAppPurchasePage.tsx +122 -0
  22. package/template/__tds/src/App.css +13 -0
  23. package/template/__tds/src/App.tsx +66 -0
  24. package/template/__tds/src/index.css +22 -0
  25. package/template/__tds/src/main.tsx +15 -0
  26. package/template/eslint.config.js +28 -0
  27. package/template/granite.config.ts +20 -0
  28. package/template/index.html +12 -0
  29. package/template/package.json +31 -0
  30. package/template/public/appsintoss-logo.png +0 -0
  31. package/template/src/vite-env.d.ts +6 -0
  32. package/template/tsconfig.app.json +22 -0
  33. package/template/tsconfig.json +7 -0
  34. package/template/tsconfig.node.json +20 -0
  35. package/template/vite.config.ts +6 -0
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # create-ait-app
2
+
3
+ 앱인토스(Apps in Toss) 프로젝트를 빠르게 시작할 수 있는 CLI 도구예요.
4
+ Vite 기반 React + TypeScript 프로젝트를 생성하고, TDS/인앱결제/인앱광고 등을 선택적으로 구성할 수 있어요.
5
+
6
+ ## 사용법
7
+
8
+ ```bash
9
+ npx create-ait-app my-app
10
+ ```
11
+
12
+ 실행하면 대화형 프롬프트로 아래 항목을 선택할 수 있어요.
13
+
14
+ 1. **패키지 매니저** — npm, yarn, pnpm 중 선택
15
+ 2. **TDS (Toss Design System)** — TDS 패키지 설치 여부
16
+ 3. **AI Skills** — AI 도구를 위한 SDK 문서 파일 추가 여부 (Cursor / Claude Code / Codex / 선택안함)
17
+ 4. **예제 코드** — 인앱결제, 인앱광고 샘플 코드 추가 (복수 선택 가능)
18
+
19
+ ## CLI 옵션
20
+
21
+ 프롬프트 없이 한 줄로 설정할 수도 있어요.
22
+
23
+ ```bash
24
+ create-ait-app my-app --inline --pm yarn --tds --skills --ai cursor --sample iap,iaa
25
+ ```
26
+
27
+ | 옵션 | 설명 |
28
+ | ----------------- | --------------------------------------------------------------- |
29
+ | `--inline` | 대화형 질문을 생략하고 옵션만으로 설정 (미지정 항목은 모두 `n`) |
30
+ | `--pm <name>` | 패키지 매니저 지정 (`npm`, `yarn`, `pnpm`) |
31
+ | `--tds` | TDS 패키지 설치 |
32
+ | `--skills` | AI Skills 파일 추가 |
33
+ | `--ai <name>` | AI 도구 지정 (`cursor`, `claude`, `codex`) |
34
+ | `--sample <name>` | 예제 코드 추가 (`iap`, `iaa` / 복수: `iap,iaa`) |
35
+ | `--help` | 도움말 출력 |
36
+
37
+ ## 생성되는 프로젝트 구조
38
+
39
+ - 기본 템플릿은 최소 구성(헤더, 개발자센터/커뮤니티 링크 등)만 포함합니다.
40
+ - **예제 코드**를 선택한 경우에만 `src/hooks/`, `src/pages/` 및 해당 파일들이 추가됩니다.
41
+
42
+ ```
43
+ my-app/
44
+ ├── src/
45
+ │ ├── App.tsx
46
+ │ ├── App.css
47
+ │ ├── main.tsx
48
+ │ ├── index.css
49
+ │ ├── vite-env.d.ts
50
+ │ ├── hooks/ # --sample iap/iaa 선택 시에만 추가
51
+ │ └── pages/ # --sample iap/iaa 선택 시에만 추가
52
+ ├── public/
53
+ ├── granite.config.ts
54
+ ├── package.json
55
+ ├── vite.config.ts
56
+ ├── tsconfig.json
57
+ └── README.md
58
+ ```
59
+
60
+ ## 개발 (이 CLI 도구 자체)
61
+
62
+ ```bash
63
+ # 의존성 설치
64
+ npm install
65
+
66
+ # 로컬에서 CLI 테스트
67
+ npm link
68
+ create-ait-app test-project
69
+
70
+ # 링크 해제
71
+ npm unlink -g create-ait-app
72
+ ```
73
+
74
+ ## 관련 링크
75
+
76
+ - [앱인토스 콘솔](https://apps-in-toss.toss.im/)
77
+ - [앱인토스 개발자센터](https://developers-apps-in-toss.toss.im/)
78
+ - [앱인토스 개발자 커뮤니티](https://techchat-apps-in-toss.toss.im/)
79
+ - [AI를 위한 LLMs 문서](https://developers-apps-in-toss.toss.im/development/llms.html)
package/bin/index.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../src/main.js');
4
+ main();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-ait-app",
3
+ "version": "0.0.1",
4
+ "description": "Create AIT App scaffolding tool",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "cli",
8
+ "scaffold",
9
+ "scaffolding",
10
+ "vite",
11
+ "react",
12
+ "typescript",
13
+ "create-app",
14
+ "apps-in-toss",
15
+ "ait"
16
+ ],
17
+ "bin": {
18
+ "create-ait-app": "./bin/index.js"
19
+ },
20
+ "scripts": {
21
+ "test": "node -c ./bin/index.js && node -c ./src/main.js",
22
+ "prepublishOnly": "npm run test"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "template"
28
+ ],
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^7.10.1"
31
+ },
32
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
33
+ }
package/src/main.js ADDED
@@ -0,0 +1,534 @@
1
+ const { execSync } = require("child_process");
2
+ const { select, confirm, input, checkbox } = require("@inquirer/prompts");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const https = require("https");
6
+ const http = require("http");
7
+ const zlib = require("zlib");
8
+
9
+ function toNpmPackageName(input) {
10
+ const raw = String(input || "").trim();
11
+ if (!raw) return "my-app";
12
+
13
+ // Allow scoped packages: @scope/name
14
+ if (raw.startsWith("@")) {
15
+ const slash = raw.indexOf("/");
16
+ if (slash > 1) {
17
+ const scope = raw.slice(0, slash).toLowerCase();
18
+ const name = raw.slice(slash + 1);
19
+ const normalizedName = toNpmPackageName(name);
20
+ return `${scope}/${normalizedName}`;
21
+ }
22
+ }
23
+
24
+ return raw
25
+ .toLowerCase()
26
+ .replace(/\s+/g, "-")
27
+ .replace(/[^a-z0-9._-]/g, "-")
28
+ .replace(/-+/g, "-")
29
+ .replace(/^[._-]+/, "")
30
+ .replace(/[._-]+$/, "") || "my-app";
31
+ }
32
+
33
+ function copyDir(src, dest) {
34
+ fs.mkdirSync(dest, { recursive: true });
35
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
36
+ if (entry.name.startsWith("__")) continue;
37
+
38
+ const srcPath = path.join(src, entry.name);
39
+ const destPath = path.join(dest, entry.name);
40
+ if (entry.isDirectory()) {
41
+ copyDir(srcPath, destPath);
42
+ } else {
43
+ fs.copyFileSync(srcPath, destPath);
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 샘플별 App.tsx 주입용 메타데이터.
50
+ * 새 샘플 추가 시: 1) template/../__samples/<id>/ 폴더 추가, 2) 여기에 항목 추가
51
+ */
52
+ const SAMPLE_CONFIG = {
53
+ iap: {
54
+ displayName: "인앱결제",
55
+ import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
56
+ route:
57
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
58
+ getButton: (useTds) =>
59
+ useTds
60
+ ? '<Button color="dark" variant="weak" onClick={() => setPage("iap")}>인앱결제 테스트하기</Button>'
61
+ : '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
62
+ },
63
+ iaa: {
64
+ displayName: "인앱광고",
65
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
66
+ route:
67
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
68
+ getButton: (useTds) =>
69
+ useTds
70
+ ? '<Button color="dark" variant="weak" onClick={() => setPage("iaa")}>인앱광고 테스트하기</Button>'
71
+ : '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
72
+ },
73
+ };
74
+
75
+ const SAMPLE_PRIMARY_COLOR = [
76
+ "#FF8A65",
77
+ "#FD9B3C",
78
+ "#E0B20C",
79
+ "#3FD599",
80
+ "#81C784",
81
+ "#4DB6AC",
82
+ "#4DD0E1",
83
+ "#64B5F6",
84
+ "#655DFF",
85
+ "#9575CD",
86
+ "#BA68C8",
87
+ "#FF91D5",
88
+ "#F06292",
89
+ "#D7B59E",
90
+ ];
91
+
92
+ function fetchText(url) {
93
+ return new Promise((resolve, reject) => {
94
+ const get = (targetUrl) => {
95
+ const client = targetUrl.startsWith("https") ? https : http;
96
+ const req = client.get(
97
+ targetUrl,
98
+ { headers: { "Accept-Encoding": "gzip, deflate" } },
99
+ (res) => {
100
+ if (
101
+ res.statusCode >= 300 &&
102
+ res.statusCode < 400 &&
103
+ res.headers.location
104
+ ) {
105
+ get(res.headers.location);
106
+ return;
107
+ }
108
+ if (res.statusCode !== 200) {
109
+ reject(new Error(`HTTP ${res.statusCode} for ${targetUrl}`));
110
+ return;
111
+ }
112
+
113
+ let stream = res;
114
+ const encoding = res.headers["content-encoding"];
115
+ if (encoding === "gzip") {
116
+ stream = res.pipe(zlib.createGunzip());
117
+ } else if (encoding === "deflate") {
118
+ stream = res.pipe(zlib.createInflate());
119
+ }
120
+
121
+ let data = "";
122
+ stream.setEncoding("utf-8");
123
+ stream.on("data", (chunk) => (data += chunk));
124
+ stream.on("end", () => resolve(data));
125
+ stream.on("error", reject);
126
+ },
127
+ );
128
+ req.on("error", reject);
129
+ };
130
+ get(url);
131
+ });
132
+ }
133
+
134
+ function parseArgs(argv) {
135
+ const args = { _: [], sample: [] };
136
+ for (let i = 0; i < argv.length; i++) {
137
+ if (argv[i] === "--pm" && argv[i + 1]) {
138
+ args.pm = argv[++i];
139
+ } else if (argv[i] === "--ai" && argv[i + 1]) {
140
+ args.ai = argv[++i];
141
+ } else if (
142
+ argv[i] === "--sample" &&
143
+ argv[i + 1] &&
144
+ !argv[i + 1].startsWith("--")
145
+ ) {
146
+ args.sample.push(...argv[++i].split(","));
147
+ } else if (argv[i].startsWith("--")) {
148
+ args[argv[i].slice(2)] = true;
149
+ } else {
150
+ args._.push(argv[i]);
151
+ }
152
+ }
153
+ return args;
154
+ }
155
+
156
+ function printHelp() {
157
+ console.log(`
158
+ 사용법: create-ait-app [project-name] [options]
159
+
160
+ options:
161
+ --inline 질문을 생략하고 옵션만으로 설정합니다 (옵션 미지정 시 모두 n)
162
+ --pm <name> 패키지 매니저를 지정합니다 (npm, yarn, pnpm)
163
+ --tds TDS(Toss Design System) 패키지를 설치합니다
164
+ --skills AI를 위한 skills 파일을 추가합니다
165
+ --ai <name> AI 도구를 지정합니다 (cursor, claude, codex)
166
+ --sample <name> 예제 코드를 추가합니다 (iap, iaa / 복수선택: iap,iaa)
167
+ --help 이 도움말을 출력합니다
168
+
169
+ links:
170
+ 앱인토스 콘솔 https://apps-in-toss.toss.im/
171
+ 앱인토스 개발자센터 https://developers-apps-in-toss.toss.im/
172
+ 앱인토스 개발자 커뮤니티 https://techchat-apps-in-toss.toss.im/
173
+ `);
174
+ }
175
+
176
+ async function main() {
177
+ const cliArgs = parseArgs(process.argv.slice(2));
178
+
179
+ if (cliArgs.help) {
180
+ printHelp();
181
+ return;
182
+ }
183
+
184
+ const isInline = cliArgs.inline;
185
+ const hasAnyOptionFlag =
186
+ cliArgs.tds || cliArgs.skills || cliArgs.sample.length > 0;
187
+
188
+ const projectName =
189
+ cliArgs._[0] ||
190
+ (await input({
191
+ message: "프로젝트 이름을 입력하세요:",
192
+ required: true,
193
+ }));
194
+
195
+ const targetDir = path.resolve(process.cwd(), projectName);
196
+ const packageName = toNpmPackageName(projectName);
197
+
198
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
199
+ console.error(
200
+ `\n❌ "${projectName}" 디렉토리가 이미 존재하고 비어있지 않습니다.`,
201
+ );
202
+ process.exit(1);
203
+ }
204
+
205
+ const validPms = ["npm", "yarn", "pnpm"];
206
+ let packageManager;
207
+
208
+ if (cliArgs.pm) {
209
+ if (!validPms.includes(cliArgs.pm)) {
210
+ console.error(
211
+ `\n❌ 지원하지 않는 패키지 매니저입니다: ${cliArgs.pm} (npm, yarn, pnpm 중 선택)`,
212
+ );
213
+ process.exit(1);
214
+ }
215
+ packageManager = cliArgs.pm;
216
+ } else {
217
+ packageManager = await select({
218
+ message: "사용할 패키지 매니저를 선택하세요:",
219
+ choices: [
220
+ { name: "npm", value: "npm" },
221
+ { name: "yarn", value: "yarn" },
222
+ { name: "pnpm", value: "pnpm" },
223
+ ],
224
+ });
225
+ }
226
+
227
+ // --- 3. TDS ---
228
+ let useTds;
229
+ if (cliArgs.tds) {
230
+ useTds = true;
231
+ } else if (isInline) {
232
+ useTds = false;
233
+ } else if (hasAnyOptionFlag) {
234
+ useTds = false;
235
+ } else {
236
+ useTds = await confirm({
237
+ message: "TDS(Toss Design System)를 사용할까요?",
238
+ default: false,
239
+ });
240
+ }
241
+
242
+ // --- 4. AI skills ---
243
+ let useSkills;
244
+ let aiTool;
245
+ if (cliArgs.skills) {
246
+ useSkills = true;
247
+ const validAiTools = ["cursor", "claude", "codex"];
248
+ aiTool = cliArgs.ai;
249
+ if (aiTool && !validAiTools.includes(aiTool)) {
250
+ console.error(
251
+ `\n❌ 지원하지 않는 AI 도구입니다: ${aiTool} (cursor, claude, codex 중 선택)`,
252
+ );
253
+ process.exit(1);
254
+ }
255
+ if (!aiTool) {
256
+ aiTool = await select({
257
+ message: "사용하는 AI 도구를 선택하세요:",
258
+ choices: [
259
+ { name: "Cursor", value: "cursor" },
260
+ { name: "Claude Code", value: "claude" },
261
+ { name: "Codex", value: "codex" },
262
+ ],
263
+ });
264
+ }
265
+ } else if (isInline || hasAnyOptionFlag) {
266
+ useSkills = false;
267
+ } else {
268
+ aiTool = await select({
269
+ message: "AI를 위한 skills를 추가할까요?",
270
+ choices: [
271
+ { name: "Cursor", value: "cursor" },
272
+ { name: "Claude Code", value: "claude" },
273
+ { name: "Codex", value: "codex" },
274
+ { name: "선택안함", value: "none" },
275
+ ],
276
+ });
277
+ useSkills = aiTool !== "none";
278
+ }
279
+
280
+ // --- 5. 예제 코드 (복수 선택 가능) ---
281
+ const validSamples = Object.keys(SAMPLE_CONFIG);
282
+ let sampleChoices = [];
283
+ if (cliArgs.sample.length > 0) {
284
+ const invalid = cliArgs.sample.filter((s) => !validSamples.includes(s));
285
+ if (invalid.length > 0) {
286
+ console.error(
287
+ `\n❌ 지원하지 않는 예제 코드입니다: ${invalid.join(", ")} (${validSamples.join(", ")} 중 선택)`,
288
+ );
289
+ process.exit(1);
290
+ }
291
+ sampleChoices = [...new Set(cliArgs.sample)];
292
+ } else if (isInline) {
293
+ sampleChoices = [];
294
+ } else if (hasAnyOptionFlag) {
295
+ sampleChoices = [];
296
+ } else {
297
+ const sampleChoiceList = validSamples.map((id) => ({
298
+ name: SAMPLE_CONFIG[id].displayName,
299
+ value: id,
300
+ }));
301
+ sampleChoices = await checkbox({
302
+ message: "예제 코드를 추가할까요? (복수 선택 가능)",
303
+ choices: sampleChoiceList,
304
+ });
305
+ }
306
+
307
+ console.log(`\n🚀 프로젝트를 생성합니다...\n`);
308
+
309
+ const templateDir = path.resolve(__dirname, "..", "template");
310
+
311
+ try {
312
+ copyDir(templateDir, targetDir);
313
+
314
+ if (useTds) {
315
+ copyDir(path.resolve(templateDir, "__tds"), targetDir);
316
+ } else {
317
+ copyDir(path.resolve(templateDir, "__default"), targetDir);
318
+ }
319
+
320
+ // package.json name 치환 (TDS 선택 시 React 18 사용 — TDS peer dependency)
321
+ const pkgPath = path.join(targetDir, "package.json");
322
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
323
+ pkg.name = packageName;
324
+ if (useTds) {
325
+ pkg.dependencies.react = "^18.0.0";
326
+ pkg.dependencies["react-dom"] = "^18.0.0";
327
+ if (pkg.devDependencies["@types/react"])
328
+ pkg.devDependencies["@types/react"] = "^18.0.0";
329
+ if (pkg.devDependencies["@types/react-dom"])
330
+ pkg.devDependencies["@types/react-dom"] = "^18.0.0";
331
+ }
332
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
333
+
334
+ // pnpm: shamefully-hoist 설정 (granite가 Vite 플러그인을 찾을 수 있도록)
335
+ if (packageManager === "pnpm") {
336
+ fs.writeFileSync(
337
+ path.join(targetDir, ".npmrc"),
338
+ "shamefully-hoist=true\n",
339
+ );
340
+ }
341
+
342
+ // granite.config.ts appName 치환
343
+ const configPath = path.join(targetDir, "granite.config.ts");
344
+ const configContent = fs.readFileSync(configPath, "utf-8");
345
+ fs.writeFileSync(
346
+ configPath,
347
+ configContent
348
+ .replace("{{APP_NAME}}", projectName)
349
+ .replace(
350
+ "{{PRIMARY_COLOR}}",
351
+ SAMPLE_PRIMARY_COLOR[
352
+ Math.floor(Math.random() * SAMPLE_PRIMARY_COLOR.length)
353
+ ],
354
+ ),
355
+ );
356
+
357
+ // README.md 프로젝트명 + 선택한 패키지 매니저 명령어 치환
358
+ const readmePath = path.join(targetDir, "README.md");
359
+ const pmDev =
360
+ packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
361
+ const pmBuild =
362
+ packageManager === "npm" ? "npm run build" : `${packageManager} build`;
363
+ const pmDeploy =
364
+ packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
365
+
366
+ let readmeContent = fs.readFileSync(readmePath, "utf-8");
367
+ readmeContent = readmeContent
368
+ .replace(/\{\{APP_NAME\}\}/g, projectName)
369
+ .replace(/\{\{PM_DEV\}\}/g, pmDev)
370
+ .replace(/\{\{PM_BUILD\}\}/g, pmBuild)
371
+ .replace(/\{\{PM_DEPLOY\}\}/g, pmDeploy);
372
+ fs.writeFileSync(readmePath, readmeContent);
373
+
374
+ // 예제 코드: 선택한 샘플만 template/../__samples/<id> 에서 src 로 복사 후 App.tsx 플레이스홀더 치환
375
+ const samplesDir = path.join(
376
+ path.resolve(templateDir, useTds ? "__tds" : "__default"),
377
+ "__samples",
378
+ );
379
+
380
+ for (const id of sampleChoices) {
381
+ const sampleRoot = path.join(samplesDir, id);
382
+
383
+ copyDir(sampleRoot, targetDir);
384
+ }
385
+
386
+ const srcDir = path.join(targetDir, "src");
387
+ const appPath = path.join(srcDir, "App.tsx");
388
+ let appContent = fs.existsSync(appPath)
389
+ ? fs.readFileSync(appPath, "utf-8")
390
+ : null;
391
+
392
+ if (appContent) {
393
+ const hasSamples = sampleChoices.length > 0;
394
+ const sampleImports = hasSamples
395
+ ? sampleChoices
396
+ .map((id) => SAMPLE_CONFIG[id]?.import)
397
+ .filter(Boolean)
398
+ .join("\n") + '\nimport { useState } from "react";'
399
+ : "";
400
+ const pageStateAndRoutes = hasSamples
401
+ ? " const [page, setPage] = useState<string | null>(null);\n\n" +
402
+ sampleChoices
403
+ .map((id) => SAMPLE_CONFIG[id]?.route)
404
+ .filter(Boolean)
405
+ .join("\n") +
406
+ "\n\n"
407
+ : "";
408
+ const sampleButtons = hasSamples
409
+ ? sampleChoices
410
+ .map((id) => SAMPLE_CONFIG[id]?.getButton(useTds))
411
+ .filter(Boolean)
412
+ .join("\n\n ")
413
+ : "";
414
+
415
+ appContent = appContent
416
+ .replace("{{SAMPLE_IMPORTS}}", sampleImports)
417
+ .replace("{{PAGE_STATE_AND_ROUTES}}", pageStateAndRoutes)
418
+ .replace("{{SAMPLE_BUTTONS}}", sampleButtons);
419
+ fs.writeFileSync(appPath, appContent);
420
+ }
421
+
422
+ // 의존성 설치
423
+ console.log(`📦 의존성을 설치합니다...\n`);
424
+
425
+ const installCommands = {
426
+ npm: "npm install",
427
+ yarn: "yarn",
428
+ pnpm: "pnpm install",
429
+ };
430
+ execSync(installCommands[packageManager], {
431
+ stdio: "inherit",
432
+ cwd: targetDir,
433
+ });
434
+
435
+ console.log(`\n📦 @apps-in-toss/web-framework 최신 버전을 설치합니다...\n`);
436
+
437
+ const addCmd = { npm: "npm install", yarn: "yarn add", pnpm: "pnpm add" };
438
+ execSync(`${addCmd[packageManager]} @apps-in-toss/web-framework@latest`, {
439
+ stdio: "inherit",
440
+ cwd: targetDir,
441
+ });
442
+
443
+ // TDS 설치
444
+ if (useTds) {
445
+ console.log(`\n📦 TDS 패키지를 설치합니다...\n`);
446
+ const tdsPackages =
447
+ "@toss/tds-mobile @toss/tds-mobile-ait @toss/tds-colors @emotion/react@^11 react@^18 react-dom@^18";
448
+ execSync(`${addCmd[packageManager]} ${tdsPackages}`, {
449
+ stdio: "inherit",
450
+ cwd: targetDir,
451
+ });
452
+ }
453
+
454
+ // AI skills
455
+ if (useSkills) {
456
+ console.log(`\n📄 AI skills 파일을 추가합니다... (${aiTool})\n`);
457
+
458
+ const aitDocs = await fetchText(
459
+ "https://developers-apps-in-toss.toss.im/llms.txt",
460
+ );
461
+ let tdsDocs;
462
+ if (useTds) {
463
+ tdsDocs = await fetchText(
464
+ "https://tossmini-docs.toss.im/tds-mobile/llms-full.txt",
465
+ );
466
+ }
467
+
468
+ if (aiTool === "cursor") {
469
+ const skillsDir = path.join(targetDir, ".cursor", "skills");
470
+ fs.mkdirSync(skillsDir, { recursive: true });
471
+ fs.writeFileSync(path.join(skillsDir, "apps-in-toss.md"), aitDocs);
472
+ console.log(" ✓ .cursor/skills/apps-in-toss.md 추가 완료");
473
+ if (tdsDocs) {
474
+ fs.writeFileSync(path.join(skillsDir, "tds-mobile.md"), tdsDocs);
475
+ console.log(" ✓ .cursor/skills/tds-mobile.md 추가 완료");
476
+ }
477
+ } else if (aiTool === "claude") {
478
+ const docsDir = path.join(targetDir, "docs", "skills");
479
+ fs.mkdirSync(docsDir, { recursive: true });
480
+
481
+ fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
482
+ console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
483
+
484
+ if (tdsDocs) {
485
+ fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
486
+ console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
487
+ }
488
+
489
+ let claudeMd = "@docs/skills/apps-in-toss.md\n";
490
+ if (tdsDocs) {
491
+ claudeMd += "@docs/skills/tds-mobile.md\n";
492
+ }
493
+ fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), claudeMd);
494
+ console.log(" ✓ CLAUDE.md 추가 완료");
495
+ } else if (aiTool === "codex") {
496
+ const docsDir = path.join(targetDir, "docs", "skills");
497
+ fs.mkdirSync(docsDir, { recursive: true });
498
+
499
+ fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
500
+ console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
501
+
502
+ if (tdsDocs) {
503
+ fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
504
+ console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
505
+ }
506
+
507
+ let agentsMd = "docs/skills/apps-in-toss.md 파일을 참고하세요.\n";
508
+ if (tdsDocs) {
509
+ agentsMd += "docs/skills/tds-mobile.md 파일을 참고하세요.\n";
510
+ }
511
+ fs.writeFileSync(path.join(targetDir, "AGENTS.md"), agentsMd);
512
+ console.log(" ✓ AGENTS.md 추가 완료");
513
+ }
514
+ }
515
+
516
+ // 코드 포맷팅
517
+ console.log(`\n📐 코드 포맷팅을 실행합니다...\n`);
518
+ const formatCmd =
519
+ packageManager === "npm" ? "npm run format" : `${packageManager} format`;
520
+ execSync(formatCmd, { stdio: "inherit", cwd: targetDir });
521
+
522
+ console.log(`
523
+ ✅ 프로젝트가 성공적으로 생성되었습니다!
524
+
525
+ cd ${projectName}
526
+ ${packageManager === "npm" ? "npm run dev" : `${packageManager} dev`}
527
+ `);
528
+ } catch (error) {
529
+ console.error("\n❌ 프로젝트 생성 중 오류가 발생했습니다:", error.message);
530
+ process.exit(1);
531
+ }
532
+ }
533
+
534
+ module.exports = { main };
@@ -0,0 +1,26 @@
1
+ # {{APP_NAME}}
2
+
3
+ Apps in Toss 프로젝트입니다.
4
+
5
+ ## 시작하기
6
+
7
+ ```bash
8
+ {{PM_DEV}}
9
+ ```
10
+
11
+ ## 배포하기
12
+
13
+ - 앱인토스 배포 API 키는 [앱인토스 콘솔](https://apps-in-toss.toss.im/) > 워크스페이스 > API 키 > 콘솔 API 키 에서 발급받을 수 있어요.
14
+
15
+ ```bash
16
+ {{PM_BUILD}}
17
+ {{PM_DEPLOY}}
18
+ ```
19
+
20
+ ## 유용한 링크
21
+
22
+ - [앱인토스 콘솔](https://apps-in-toss.toss.im/)
23
+ - [앱인토스 개발자센터](https://developers-apps-in-toss.toss.im/)
24
+ - [앱인토스 개발자 커뮤니티](https://techchat-apps-in-toss.toss.im/)
25
+
26
+ AI를 사용하시는 경우 [여기](https://developers-apps-in-toss.toss.im/development/llms.html)를 확인해보세요.