create-ait-app 0.0.2 → 0.0.3

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 (112) hide show
  1. package/README.md +38 -38
  2. package/package.json +5 -8
  3. package/src/cli.js +52 -0
  4. package/src/main.js +95 -444
  5. package/src/sample-configs.js +140 -0
  6. package/src/sample-inject.js +65 -0
  7. package/src/scaffold.js +111 -0
  8. package/src/skills.js +66 -0
  9. package/src/templates.js +88 -0
  10. package/src/utils/copy-dir.js +19 -0
  11. package/src/utils/fetch-text.js +47 -0
  12. package/src/utils/package-name.js +26 -0
  13. package/templates/js/eslint.config.js +14 -0
  14. package/templates/js/index.html +12 -0
  15. package/templates/js/package.json +20 -0
  16. package/templates/js/samples/iaa/src/lib/inAppAds.js +119 -0
  17. package/templates/js/samples/iaa/src/pages/InAppAdsPage.js +83 -0
  18. package/templates/js/samples/iap/src/lib/inAppPurchase.js +105 -0
  19. package/templates/js/samples/iap/src/pages/InAppPurchasePage.js +102 -0
  20. package/templates/js/src/app.js +58 -0
  21. package/templates/js/src/main.js +2 -0
  22. package/templates/js/vite.config.js +3 -0
  23. package/templates/react/README.md +26 -0
  24. package/templates/react/eslint.config.js +30 -0
  25. package/templates/react/granite.config.ts +20 -0
  26. package/templates/react/index.html +12 -0
  27. package/templates/react/package.json +27 -0
  28. package/templates/react/public/appsintoss-logo.png +0 -0
  29. package/templates/react/samples/iaa/src/hooks/useInAppAds.js +102 -0
  30. package/templates/react/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  31. package/templates/react/samples/iaa/src/pages/InAppAdsPage.jsx +75 -0
  32. package/templates/react/samples/iap/public/icon-document.png +0 -0
  33. package/templates/react/samples/iap/src/hooks/useInAppPurchase.js +95 -0
  34. package/templates/react/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  35. package/templates/react/samples/iap/src/pages/InAppPurchasePage.jsx +115 -0
  36. package/templates/react/src/App.css +104 -0
  37. package/templates/react/src/App.jsx +45 -0
  38. package/templates/react/src/index.css +27 -0
  39. package/templates/react/src/main.jsx +10 -0
  40. package/templates/react/vite.config.js +6 -0
  41. package/templates/react-ts/README.md +26 -0
  42. package/templates/react-ts/eslint.config.js +28 -0
  43. package/templates/react-ts/granite.config.ts +20 -0
  44. package/templates/react-ts/index.html +12 -0
  45. package/templates/react-ts/public/appsintoss-logo.png +0 -0
  46. package/templates/react-ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  47. package/templates/react-ts/samples/iap/public/icon-document.png +0 -0
  48. package/templates/react-ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  49. package/templates/react-ts/src/App.css +104 -0
  50. package/templates/react-ts/src/index.css +27 -0
  51. package/templates/react-ts/src/vite-env.d.ts +6 -0
  52. package/templates/react-ts/tsconfig.app.json +22 -0
  53. package/templates/react-ts/tsconfig.json +7 -0
  54. package/templates/react-ts/tsconfig.node.json +20 -0
  55. package/templates/react-ts/vite.config.ts +6 -0
  56. package/templates/react-ts-tds/README.md +26 -0
  57. package/templates/react-ts-tds/granite.config.ts +20 -0
  58. package/templates/react-ts-tds/package.json +35 -0
  59. package/templates/react-ts-tds/public/appsintoss-logo.png +0 -0
  60. package/templates/ts/README.md +26 -0
  61. package/templates/ts/eslint.config.js +15 -0
  62. package/templates/ts/granite.config.ts +20 -0
  63. package/templates/ts/index.html +12 -0
  64. package/templates/ts/package.json +22 -0
  65. package/templates/ts/public/appsintoss-logo.png +0 -0
  66. package/templates/ts/samples/iaa/src/lib/inAppAds.ts +132 -0
  67. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  68. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.ts +85 -0
  69. package/templates/ts/samples/iap/public/icon-document.png +0 -0
  70. package/templates/ts/samples/iap/src/lib/inAppPurchase.ts +114 -0
  71. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  72. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.ts +105 -0
  73. package/templates/ts/src/App.css +104 -0
  74. package/templates/ts/src/app.ts +60 -0
  75. package/templates/ts/src/index.css +27 -0
  76. package/templates/ts/src/main.ts +2 -0
  77. package/templates/ts/src/vite-env.d.ts +1 -0
  78. package/templates/ts/tsconfig.app.json +22 -0
  79. package/templates/ts/tsconfig.json +7 -0
  80. package/templates/ts/tsconfig.node.json +20 -0
  81. package/templates/ts/vite.config.ts +3 -0
  82. /package/{template → templates/js}/README.md +0 -0
  83. /package/{template → templates/js}/granite.config.ts +0 -0
  84. /package/{template → templates/js}/public/appsintoss-logo.png +0 -0
  85. /package/{template/__default/__samples → templates/js/samples}/iaa/src/pages/InAppAdsPage.css +0 -0
  86. /package/{template/__default/__samples → templates/js/samples}/iap/public/icon-document.png +0 -0
  87. /package/{template/__default/__samples → templates/js/samples}/iap/src/pages/InAppPurchasePage.css +0 -0
  88. /package/{template/__default → templates/js}/src/App.css +0 -0
  89. /package/{template/__default → templates/js}/src/index.css +0 -0
  90. /package/{template → templates/react-ts}/package.json +0 -0
  91. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  92. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  93. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  94. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  95. /package/{template/__default → templates/react-ts}/src/App.tsx +0 -0
  96. /package/{template/__default → templates/react-ts}/src/main.tsx +0 -0
  97. /package/{template → templates/react-ts-tds}/eslint.config.js +0 -0
  98. /package/{template → templates/react-ts-tds}/index.html +0 -0
  99. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  100. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  101. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/public/icon-document.png +0 -0
  102. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  103. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  104. /package/{template/__tds → templates/react-ts-tds}/src/App.css +0 -0
  105. /package/{template/__tds → templates/react-ts-tds}/src/App.tsx +0 -0
  106. /package/{template/__tds → templates/react-ts-tds}/src/index.css +0 -0
  107. /package/{template/__tds → templates/react-ts-tds}/src/main.tsx +0 -0
  108. /package/{template → templates/react-ts-tds}/src/vite-env.d.ts +0 -0
  109. /package/{template → templates/react-ts-tds}/tsconfig.app.json +0 -0
  110. /package/{template → templates/react-ts-tds}/tsconfig.json +0 -0
  111. /package/{template → templates/react-ts-tds}/tsconfig.node.json +0 -0
  112. /package/{template → templates/react-ts-tds}/vite.config.ts +0 -0
package/src/main.js CHANGED
@@ -1,213 +1,21 @@
1
- const { execSync } = require("child_process");
2
1
  const { select, confirm, input, checkbox } = require("@inquirer/prompts");
3
- const path = require("path");
4
2
  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
- /**
135
- * npm create / yarn create / pnpm create 로 실행된 경우 호출한 패키지 매니저를 추론합니다.
136
- */
137
- function detectPackageManagerFromInvokingTool() {
138
- const ua = process.env.npm_config_user_agent || "";
139
- const execpath = (process.env.npm_execpath || "").replace(/\\/g, "/");
140
-
141
- // pnpm: execpath 또는 UA (경로에 /pnpm/ segment 또는 실행 파일名 pnpm)
142
- if (
143
- /pnpm\//i.test(ua) ||
144
- /\/pnpm\//i.test(execpath) ||
145
- /(^|\/)pnpm(\.cjs|\.mjs|\.exe|\.cmd)?$/i.test(execpath)
146
- ) {
147
- return "pnpm";
148
- }
149
-
150
- // Yarn — Corepack yarn.js, Berry .yarn/releases, 글로벌/툴 shim(…/bin/yarn) 등
151
- if (
152
- /\.yarn\/releases\/yarn-/i.test(execpath) ||
153
- /\/corepack\/v\d+\/yarn\//i.test(execpath) ||
154
- /\/node_modules\/yarn\//i.test(execpath) ||
155
- /\/yarn\/\d+\.\d+\.\d+\//i.test(execpath) ||
156
- /\/yarn\/bin\/yarn\.js$/i.test(execpath) ||
157
- /\/yarn\.js$/i.test(execpath) ||
158
- /(^|\/)yarn(\.cmd|\.exe)?$/i.test(execpath) ||
159
- /yarn\//i.test(ua)
160
- ) {
161
- return "yarn";
162
- }
163
-
164
- if (/npm\//i.test(ua)) return "npm";
165
-
166
- return null;
167
- }
168
-
169
- function parseArgs(argv) {
170
- const args = { _: [], sample: [] };
171
- for (let i = 0; i < argv.length; i++) {
172
- if (argv[i] === "--pm" && argv[i + 1]) {
173
- args.pm = argv[++i];
174
- } else if (argv[i] === "--ai" && argv[i + 1]) {
175
- args.ai = argv[++i];
176
- } else if (
177
- argv[i] === "--sample" &&
178
- argv[i + 1] &&
179
- !argv[i + 1].startsWith("--")
180
- ) {
181
- args.sample.push(...argv[++i].split(","));
182
- } else if (argv[i].startsWith("--")) {
183
- args[argv[i].slice(2)] = true;
184
- } else {
185
- args._.push(argv[i]);
186
- }
187
- }
188
- return args;
189
- }
190
-
191
- function printHelp() {
192
- console.log(`
193
- 사용법: create-ait-app [project-name] [options]
194
-
195
- options:
196
- --inline 질문을 생략하고 옵션만으로 설정합니다 (옵션 미지정 시 모두 n)
197
- --pm <name> 패키지 매니저를 지정합니다 (npm, yarn, pnpm).
198
- npm/yarn/pnpm create 로 실행한 경우 해당 매니저를 씁니다
199
- --tds TDS(Toss Design System) 패키지를 설치합니다
200
- --skills AI를 위한 skills 파일을 추가합니다
201
- --ai <name> AI 도구를 지정합니다 (cursor, claude, codex)
202
- --sample <name> 예제 코드를 추가합니다 (iap, iaa / 복수선택: iap,iaa)
203
- --help 이 도움말을 출력합니다
204
-
205
- links:
206
- 앱인토스 콘솔 https://apps-in-toss.toss.im/
207
- 앱인토스 개발자센터 https://developers-apps-in-toss.toss.im/
208
- 앱인토스 개발자 커뮤니티 https://techchat-apps-in-toss.toss.im/
209
- `);
210
- }
3
+ const path = require("path");
4
+ const { parseArgs, printHelp } = require("./cli");
5
+ const {
6
+ TEMPLATES_DIR,
7
+ TEMPLATE_IDS,
8
+ TEMPLATE_REGISTRY,
9
+ TEMPLATE_CHOICES,
10
+ resolveTemplateFolder,
11
+ } = require("./templates");
12
+ const { toNpmPackageName } = require("./utils/package-name");
13
+ const {
14
+ scaffoldProject,
15
+ installDependencies,
16
+ formatProject,
17
+ } = require("./scaffold");
18
+ const { writeAiSkills } = require("./skills");
211
19
 
212
20
  async function main() {
213
21
  const cliArgs = parseArgs(process.argv.slice(2));
@@ -219,7 +27,7 @@ async function main() {
219
27
 
220
28
  const isInline = cliArgs.inline;
221
29
  const hasAnyOptionFlag =
222
- cliArgs.tds || cliArgs.skills || cliArgs.sample.length > 0;
30
+ cliArgs.template || cliArgs.tds || cliArgs.skills || cliArgs.sample.length > 0;
223
31
 
224
32
  const projectName =
225
33
  cliArgs._[0] ||
@@ -250,37 +58,63 @@ async function main() {
250
58
  }
251
59
  packageManager = cliArgs.pm;
252
60
  } else {
253
- const detected = detectPackageManagerFromInvokingTool();
254
- if (detected && validPms.includes(detected)) {
255
- packageManager = detected;
256
- } else {
257
- packageManager = await select({
258
- message: "사용할 패키지 매니저를 선택하세요:",
259
- choices: [
260
- { name: "npm", value: "npm" },
261
- { name: "yarn", value: "yarn" },
262
- { name: "pnpm", value: "pnpm" },
263
- ],
264
- });
61
+ packageManager = await select({
62
+ message: "사용할 패키지 매니저를 선택하세요:",
63
+ choices: [
64
+ { name: "npm", value: "npm" },
65
+ { name: "yarn", value: "yarn" },
66
+ { name: "pnpm", value: "pnpm" },
67
+ ],
68
+ });
69
+ }
70
+
71
+ let baseTemplateId = null;
72
+
73
+ if (cliArgs.template) {
74
+ if (cliArgs.template === "react-ts-tds") {
75
+ console.error(
76
+ "\n❌ react-ts-tds는 더 이상 템플릿 이름으로 사용할 수 없습니다. --template react-ts --tds를 사용해 주세요.",
77
+ );
78
+ process.exit(1);
79
+ }
80
+ if (!TEMPLATE_IDS.includes(cliArgs.template)) {
81
+ console.error(
82
+ `\n❌ 지원하지 않는 템플릿입니다: ${cliArgs.template} (${TEMPLATE_IDS.join(", ")} 중 선택)`,
83
+ );
84
+ process.exit(1);
265
85
  }
86
+ baseTemplateId = cliArgs.template;
87
+ } else if (isInline || hasAnyOptionFlag) {
88
+ baseTemplateId = "react-ts";
89
+ } else {
90
+ baseTemplateId = await select({
91
+ message: "사용할 템플릿을 선택하세요:",
92
+ choices: TEMPLATE_CHOICES,
93
+ });
266
94
  }
267
95
 
268
- // --- 3. TDS ---
269
- let useTds;
96
+ let useTds = false;
270
97
  if (cliArgs.tds) {
271
- useTds = true;
272
- } else if (isInline) {
273
- useTds = false;
274
- } else if (hasAnyOptionFlag) {
275
- useTds = false;
276
- } else {
98
+ useTds = baseTemplateId === "react-ts";
99
+ } else if (!isInline && !hasAnyOptionFlag && baseTemplateId === "react-ts") {
277
100
  useTds = await confirm({
278
- message: "TDS(Toss Design System)를 사용할까요?",
101
+ message:
102
+ "TDS(Toss Design System)를 사용할까요? (앱인토스에 필수 아님, 기본값: 사용 안 함)",
279
103
  default: false,
280
104
  });
281
105
  }
282
106
 
283
- // --- 4. AI skills ---
107
+ const templateId = resolveTemplateFolder(baseTemplateId, useTds);
108
+ const template = TEMPLATE_REGISTRY[templateId];
109
+ const templateDir = path.join(TEMPLATES_DIR, templateId);
110
+
111
+ if (!fs.existsSync(templateDir)) {
112
+ console.error(`\n❌ 템플릿을 찾을 수 없습니다: templates/${templateId}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ const sampleConfig = template.sampleConfig;
117
+
284
118
  let useSkills;
285
119
  let aiTool;
286
120
  if (cliArgs.skills) {
@@ -318,8 +152,7 @@ async function main() {
318
152
  useSkills = aiTool !== "none";
319
153
  }
320
154
 
321
- // --- 5. 예제 코드 (복수 선택 가능) ---
322
- const validSamples = Object.keys(SAMPLE_CONFIG);
155
+ const validSamples = Object.keys(sampleConfig);
323
156
  let sampleChoices = [];
324
157
  if (cliArgs.sample.length > 0) {
325
158
  const invalid = cliArgs.sample.filter((s) => !validSamples.includes(s));
@@ -330,13 +163,11 @@ async function main() {
330
163
  process.exit(1);
331
164
  }
332
165
  sampleChoices = [...new Set(cliArgs.sample)];
333
- } else if (isInline) {
334
- sampleChoices = [];
335
- } else if (hasAnyOptionFlag) {
166
+ } else if (isInline || hasAnyOptionFlag) {
336
167
  sampleChoices = [];
337
168
  } else {
338
169
  const sampleChoiceList = validSamples.map((id) => ({
339
- name: SAMPLE_CONFIG[id].displayName,
170
+ name: sampleConfig[id].displayName,
340
171
  value: id,
341
172
  }));
342
173
  sampleChoices = await checkbox({
@@ -345,224 +176,38 @@ async function main() {
345
176
  });
346
177
  }
347
178
 
348
- console.log(`\n🚀 프로젝트를 생성합니다...\n`);
349
-
350
- const templateDir = path.resolve(__dirname, "..", "template");
179
+ console.log(
180
+ `\n🚀 프로젝트를 생성합니다... (templates/${templateId}${useTds ? ", TDS" : ""})\n`,
181
+ );
351
182
 
352
183
  try {
353
- copyDir(templateDir, targetDir);
354
-
355
- if (useTds) {
356
- copyDir(path.resolve(templateDir, "__tds"), targetDir);
357
- } else {
358
- copyDir(path.resolve(templateDir, "__default"), targetDir);
359
- }
360
-
361
- // package.json name 치환 (TDS 선택 시 React 18 사용 — TDS peer dependency)
362
- const pkgPath = path.join(targetDir, "package.json");
363
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
364
- pkg.name = packageName;
365
- if (useTds) {
366
- pkg.dependencies.react = "^18.0.0";
367
- pkg.dependencies["react-dom"] = "^18.0.0";
368
- if (pkg.devDependencies["@types/react"])
369
- pkg.devDependencies["@types/react"] = "^18.0.0";
370
- if (pkg.devDependencies["@types/react-dom"])
371
- pkg.devDependencies["@types/react-dom"] = "^18.0.0";
372
- }
373
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
374
-
375
- // pnpm: shamefully-hoist 설정 (granite가 Vite 플러그인을 찾을 수 있도록)
376
- if (packageManager === "pnpm") {
377
- fs.writeFileSync(
378
- path.join(targetDir, ".npmrc"),
379
- "shamefully-hoist=true\n",
380
- );
381
- }
382
-
383
- // granite.config.ts appName 치환
384
- const configPath = path.join(targetDir, "granite.config.ts");
385
- const configContent = fs.readFileSync(configPath, "utf-8");
386
- fs.writeFileSync(
387
- configPath,
388
- configContent
389
- .replace("{{APP_NAME}}", projectName)
390
- .replace(
391
- "{{PRIMARY_COLOR}}",
392
- SAMPLE_PRIMARY_COLOR[
393
- Math.floor(Math.random() * SAMPLE_PRIMARY_COLOR.length)
394
- ],
395
- ),
396
- );
397
-
398
- // README.md 프로젝트명 + 선택한 패키지 매니저 명령어 치환
399
- const readmePath = path.join(targetDir, "README.md");
400
- const pmDev =
401
- packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
402
- const pmBuild =
403
- packageManager === "npm" ? "npm run build" : `${packageManager} build`;
404
- const pmDeploy =
405
- packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
406
-
407
- let readmeContent = fs.readFileSync(readmePath, "utf-8");
408
- readmeContent = readmeContent
409
- .replace(/\{\{APP_NAME\}\}/g, projectName)
410
- .replace(/\{\{PM_DEV\}\}/g, pmDev)
411
- .replace(/\{\{PM_BUILD\}\}/g, pmBuild)
412
- .replace(/\{\{PM_DEPLOY\}\}/g, pmDeploy);
413
- fs.writeFileSync(readmePath, readmeContent);
414
-
415
- // 예제 코드: 선택한 샘플만 template/../__samples/<id> 에서 src 로 복사 후 App.tsx 플레이스홀더 치환
416
- const samplesDir = path.join(
417
- path.resolve(templateDir, useTds ? "__tds" : "__default"),
418
- "__samples",
419
- );
420
-
421
- for (const id of sampleChoices) {
422
- const sampleRoot = path.join(samplesDir, id);
423
-
424
- copyDir(sampleRoot, targetDir);
425
- }
426
-
427
- const srcDir = path.join(targetDir, "src");
428
- const appPath = path.join(srcDir, "App.tsx");
429
- let appContent = fs.existsSync(appPath)
430
- ? fs.readFileSync(appPath, "utf-8")
431
- : null;
432
-
433
- if (appContent) {
434
- const hasSamples = sampleChoices.length > 0;
435
- const sampleImports = hasSamples
436
- ? sampleChoices
437
- .map((id) => SAMPLE_CONFIG[id]?.import)
438
- .filter(Boolean)
439
- .join("\n") + '\nimport { useState } from "react";'
440
- : "";
441
- const pageStateAndRoutes = hasSamples
442
- ? " const [page, setPage] = useState<string | null>(null);\n\n" +
443
- sampleChoices
444
- .map((id) => SAMPLE_CONFIG[id]?.route)
445
- .filter(Boolean)
446
- .join("\n") +
447
- "\n\n"
448
- : "";
449
- const sampleButtons = hasSamples
450
- ? sampleChoices
451
- .map((id) => SAMPLE_CONFIG[id]?.getButton(useTds))
452
- .filter(Boolean)
453
- .join("\n\n ")
454
- : "";
455
-
456
- appContent = appContent
457
- .replace("{{SAMPLE_IMPORTS}}", sampleImports)
458
- .replace("{{PAGE_STATE_AND_ROUTES}}", pageStateAndRoutes)
459
- .replace("{{SAMPLE_BUTTONS}}", sampleButtons);
460
- fs.writeFileSync(appPath, appContent);
461
- }
462
-
463
- // 의존성 설치
464
- console.log(`📦 의존성을 설치합니다...\n`);
465
-
466
- const installCommands = {
467
- npm: "npm install",
468
- yarn: "yarn",
469
- pnpm: "pnpm install",
470
- };
471
- execSync(installCommands[packageManager], {
472
- stdio: "inherit",
473
- cwd: targetDir,
474
- });
475
-
476
- console.log(`\n📦 @apps-in-toss/web-framework 최신 버전을 설치합니다...\n`);
477
-
478
- const addCmd = { npm: "npm install", yarn: "yarn add", pnpm: "pnpm add" };
479
- execSync(`${addCmd[packageManager]} @apps-in-toss/web-framework@latest`, {
480
- stdio: "inherit",
481
- cwd: targetDir,
184
+ scaffoldProject({
185
+ templateDir,
186
+ targetDir,
187
+ template,
188
+ sampleConfig,
189
+ sampleChoices,
190
+ projectName,
191
+ packageName,
192
+ packageManager,
482
193
  });
483
194
 
484
- // TDS 설치
485
- if (useTds) {
486
- console.log(`\n📦 TDS 패키지를 설치합니다...\n`);
487
- const tdsPackages =
488
- "@toss/tds-mobile @toss/tds-mobile-ait @toss/tds-colors @emotion/react@^11 react@^18 react-dom@^18";
489
- execSync(`${addCmd[packageManager]} ${tdsPackages}`, {
490
- stdio: "inherit",
491
- cwd: targetDir,
492
- });
493
- }
195
+ installDependencies(targetDir, packageManager);
494
196
 
495
- // AI skills
496
197
  if (useSkills) {
497
- console.log(`\n📄 AI skills 파일을 추가합니다... (${aiTool})\n`);
498
-
499
- const aitDocs = await fetchText(
500
- "https://developers-apps-in-toss.toss.im/llms.txt",
501
- );
502
- let tdsDocs;
503
- if (useTds) {
504
- tdsDocs = await fetchText(
505
- "https://tossmini-docs.toss.im/tds-mobile/llms-full.txt",
506
- );
507
- }
508
-
509
- if (aiTool === "cursor") {
510
- const skillsDir = path.join(targetDir, ".cursor", "skills");
511
- fs.mkdirSync(skillsDir, { recursive: true });
512
- fs.writeFileSync(path.join(skillsDir, "apps-in-toss.md"), aitDocs);
513
- console.log(" ✓ .cursor/skills/apps-in-toss.md 추가 완료");
514
- if (tdsDocs) {
515
- fs.writeFileSync(path.join(skillsDir, "tds-mobile.md"), tdsDocs);
516
- console.log(" ✓ .cursor/skills/tds-mobile.md 추가 완료");
517
- }
518
- } else if (aiTool === "claude") {
519
- const docsDir = path.join(targetDir, "docs", "skills");
520
- fs.mkdirSync(docsDir, { recursive: true });
521
-
522
- fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
523
- console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
524
-
525
- if (tdsDocs) {
526
- fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
527
- console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
528
- }
529
-
530
- let claudeMd = "@docs/skills/apps-in-toss.md\n";
531
- if (tdsDocs) {
532
- claudeMd += "@docs/skills/tds-mobile.md\n";
533
- }
534
- fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), claudeMd);
535
- console.log(" ✓ CLAUDE.md 추가 완료");
536
- } else if (aiTool === "codex") {
537
- const docsDir = path.join(targetDir, "docs", "skills");
538
- fs.mkdirSync(docsDir, { recursive: true });
539
-
540
- fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
541
- console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
542
-
543
- if (tdsDocs) {
544
- fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
545
- console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
546
- }
547
-
548
- let agentsMd = "docs/skills/apps-in-toss.md 파일을 참고하세요.\n";
549
- if (tdsDocs) {
550
- agentsMd += "docs/skills/tds-mobile.md 파일을 참고하세요.\n";
551
- }
552
- fs.writeFileSync(path.join(targetDir, "AGENTS.md"), agentsMd);
553
- console.log(" ✓ AGENTS.md 추가 완료");
554
- }
198
+ await writeAiSkills({
199
+ targetDir,
200
+ aiTool,
201
+ useTds: template.useTds,
202
+ });
555
203
  }
556
204
 
557
- // 코드 포맷팅
558
- console.log(`\n📐 코드 포맷팅을 실행합니다...\n`);
559
- const formatCmd =
560
- packageManager === "npm" ? "npm run format" : `${packageManager} format`;
561
- execSync(formatCmd, { stdio: "inherit", cwd: targetDir });
205
+ formatProject(targetDir, packageManager);
562
206
 
563
207
  console.log(`
564
208
  ✅ 프로젝트가 성공적으로 생성되었습니다!
565
209
 
210
+ 템플릿: templates/${templateId}
566
211
  cd ${projectName}
567
212
  ${packageManager === "npm" ? "npm run dev" : `${packageManager} dev`}
568
213
  `);
@@ -572,4 +217,10 @@ async function main() {
572
217
  }
573
218
  }
574
219
 
575
- module.exports = { main };
220
+ module.exports = {
221
+ main,
222
+ TEMPLATE_IDS,
223
+ TEMPLATE_REGISTRY,
224
+ TEMPLATE_CHOICES,
225
+ resolveTemplateFolder,
226
+ };