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.
- package/README.md +38 -38
- package/package.json +5 -8
- package/src/cli.js +52 -0
- package/src/main.js +95 -444
- package/src/sample-configs.js +140 -0
- package/src/sample-inject.js +65 -0
- package/src/scaffold.js +111 -0
- package/src/skills.js +66 -0
- package/src/templates.js +88 -0
- package/src/utils/copy-dir.js +19 -0
- package/src/utils/fetch-text.js +47 -0
- package/src/utils/package-name.js +26 -0
- package/templates/js/eslint.config.js +14 -0
- package/templates/js/index.html +12 -0
- package/templates/js/package.json +20 -0
- package/templates/js/samples/iaa/src/lib/inAppAds.js +119 -0
- package/templates/js/samples/iaa/src/pages/InAppAdsPage.js +83 -0
- package/templates/js/samples/iap/src/lib/inAppPurchase.js +105 -0
- package/templates/js/samples/iap/src/pages/InAppPurchasePage.js +102 -0
- package/templates/js/src/app.js +58 -0
- package/templates/js/src/main.js +2 -0
- package/templates/js/vite.config.js +3 -0
- package/templates/react/README.md +26 -0
- package/templates/react/eslint.config.js +30 -0
- package/templates/react/granite.config.ts +20 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +27 -0
- package/templates/react/public/appsintoss-logo.png +0 -0
- package/templates/react/samples/iaa/src/hooks/useInAppAds.js +102 -0
- package/templates/react/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/react/samples/iaa/src/pages/InAppAdsPage.jsx +75 -0
- package/templates/react/samples/iap/public/icon-document.png +0 -0
- package/templates/react/samples/iap/src/hooks/useInAppPurchase.js +95 -0
- package/templates/react/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/react/samples/iap/src/pages/InAppPurchasePage.jsx +115 -0
- package/templates/react/src/App.css +104 -0
- package/templates/react/src/App.jsx +45 -0
- package/templates/react/src/index.css +27 -0
- package/templates/react/src/main.jsx +10 -0
- package/templates/react/vite.config.js +6 -0
- package/templates/react-ts/README.md +26 -0
- package/templates/react-ts/eslint.config.js +28 -0
- package/templates/react-ts/granite.config.ts +20 -0
- package/templates/react-ts/index.html +12 -0
- package/templates/react-ts/public/appsintoss-logo.png +0 -0
- package/templates/react-ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/react-ts/samples/iap/public/icon-document.png +0 -0
- package/templates/react-ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/react-ts/src/App.css +104 -0
- package/templates/react-ts/src/index.css +27 -0
- package/templates/react-ts/src/vite-env.d.ts +6 -0
- package/templates/react-ts/tsconfig.app.json +22 -0
- package/templates/react-ts/tsconfig.json +7 -0
- package/templates/react-ts/tsconfig.node.json +20 -0
- package/templates/react-ts/vite.config.ts +6 -0
- package/templates/react-ts-tds/README.md +26 -0
- package/templates/react-ts-tds/granite.config.ts +20 -0
- package/templates/react-ts-tds/package.json +35 -0
- package/templates/react-ts-tds/public/appsintoss-logo.png +0 -0
- package/templates/ts/README.md +26 -0
- package/templates/ts/eslint.config.js +15 -0
- package/templates/ts/granite.config.ts +20 -0
- package/templates/ts/index.html +12 -0
- package/templates/ts/package.json +22 -0
- package/templates/ts/public/appsintoss-logo.png +0 -0
- package/templates/ts/samples/iaa/src/lib/inAppAds.ts +132 -0
- package/templates/ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/ts/samples/iaa/src/pages/InAppAdsPage.ts +85 -0
- package/templates/ts/samples/iap/public/icon-document.png +0 -0
- package/templates/ts/samples/iap/src/lib/inAppPurchase.ts +114 -0
- package/templates/ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/ts/samples/iap/src/pages/InAppPurchasePage.ts +105 -0
- package/templates/ts/src/App.css +104 -0
- package/templates/ts/src/app.ts +60 -0
- package/templates/ts/src/index.css +27 -0
- package/templates/ts/src/main.ts +2 -0
- package/templates/ts/src/vite-env.d.ts +1 -0
- package/templates/ts/tsconfig.app.json +22 -0
- package/templates/ts/tsconfig.json +7 -0
- package/templates/ts/tsconfig.node.json +20 -0
- package/templates/ts/vite.config.ts +3 -0
- /package/{template → templates/js}/README.md +0 -0
- /package/{template → templates/js}/granite.config.ts +0 -0
- /package/{template → templates/js}/public/appsintoss-logo.png +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iaa/src/pages/InAppAdsPage.css +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iap/public/icon-document.png +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iap/src/pages/InAppPurchasePage.css +0 -0
- /package/{template/__default → templates/js}/src/App.css +0 -0
- /package/{template/__default → templates/js}/src/index.css +0 -0
- /package/{template → templates/react-ts}/package.json +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
- /package/{template/__default → templates/react-ts}/src/App.tsx +0 -0
- /package/{template/__default → templates/react-ts}/src/main.tsx +0 -0
- /package/{template → templates/react-ts-tds}/eslint.config.js +0 -0
- /package/{template → templates/react-ts-tds}/index.html +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/public/icon-document.png +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/App.css +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/App.tsx +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/index.css +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/main.tsx +0 -0
- /package/{template → templates/react-ts-tds}/src/vite-env.d.ts +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.app.json +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.json +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.node.json +0 -0
- /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
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
269
|
-
let useTds;
|
|
96
|
+
let useTds = false;
|
|
270
97
|
if (cliArgs.tds) {
|
|
271
|
-
useTds =
|
|
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:
|
|
101
|
+
message:
|
|
102
|
+
"TDS(Toss Design System)를 사용할까요? (앱인토스에 필수 아님, 기본값: 사용 안 함)",
|
|
279
103
|
default: false,
|
|
280
104
|
});
|
|
281
105
|
}
|
|
282
106
|
|
|
283
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
349
|
-
|
|
350
|
-
|
|
179
|
+
console.log(
|
|
180
|
+
`\n🚀 프로젝트를 생성합니다... (templates/${templateId}${useTds ? ", TDS" : ""})\n`,
|
|
181
|
+
);
|
|
351
182
|
|
|
352
183
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 = {
|
|
220
|
+
module.exports = {
|
|
221
|
+
main,
|
|
222
|
+
TEMPLATE_IDS,
|
|
223
|
+
TEMPLATE_REGISTRY,
|
|
224
|
+
TEMPLATE_CHOICES,
|
|
225
|
+
resolveTemplateFolder,
|
|
226
|
+
};
|