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
@@ -0,0 +1,140 @@
1
+ /**
2
+ * React + TypeScript 샘플 주입 메타데이터 (react-ts)
3
+ */
4
+ const REACT_SAMPLE_CONFIG = {
5
+ iap: {
6
+ displayName: "인앱결제",
7
+ import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
8
+ route:
9
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
10
+ getButton: () =>
11
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
12
+ },
13
+ iaa: {
14
+ displayName: "인앱광고",
15
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
16
+ route:
17
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
18
+ getButton: () =>
19
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
20
+ },
21
+ };
22
+
23
+ /**
24
+ * React + JavaScript 샘플 주입 메타데이터 (react)
25
+ */
26
+ const REACT_JS_SAMPLE_CONFIG = {
27
+ iap: {
28
+ displayName: "인앱결제",
29
+ import:
30
+ 'import { InAppPurchasePage } from "./pages/InAppPurchasePage.jsx";',
31
+ route:
32
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
33
+ getButton: () =>
34
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
35
+ },
36
+ iaa: {
37
+ displayName: "인앱광고",
38
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage.jsx";',
39
+ route:
40
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
41
+ getButton: () =>
42
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
43
+ },
44
+ };
45
+
46
+ /**
47
+ * React + TypeScript + TDS 샘플 주입 메타데이터 (react-ts-tds)
48
+ */
49
+ const REACT_TDS_SAMPLE_CONFIG = {
50
+ iap: {
51
+ displayName: "인앱결제",
52
+ import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
53
+ route:
54
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
55
+ getButton: () =>
56
+ '<Button color="dark" variant="weak" onClick={() => setPage("iap")}>인앱결제 테스트하기</Button>',
57
+ },
58
+ iaa: {
59
+ displayName: "인앱광고",
60
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
61
+ route:
62
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
63
+ getButton: () =>
64
+ '<Button color="dark" variant="weak" onClick={() => setPage("iaa")}>인앱광고 테스트하기</Button>',
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Vanilla JavaScript 샘플 주입 메타데이터 (js)
70
+ */
71
+ const JS_SAMPLE_CONFIG = {
72
+ iap: {
73
+ displayName: "인앱결제",
74
+ import:
75
+ 'import { mountInAppPurchasePage } from "./pages/InAppPurchasePage.js";',
76
+ route: ` if (currentPage === "iap") {
77
+ mountInAppPurchasePage(() => {
78
+ currentPage = null;
79
+ render();
80
+ });
81
+ return;
82
+ }`,
83
+ getButton: () =>
84
+ '<button type="button" class="app-button app-button-ghost" data-page="iap">인앱결제 테스트하기</button>',
85
+ },
86
+ iaa: {
87
+ displayName: "인앱광고",
88
+ import: 'import { mountInAppAdsPage } from "./pages/InAppAdsPage.js";',
89
+ route: ` if (currentPage === "iaa") {
90
+ mountInAppAdsPage(() => {
91
+ currentPage = null;
92
+ render();
93
+ });
94
+ return;
95
+ }`,
96
+ getButton: () =>
97
+ '<button type="button" class="app-button app-button-ghost" data-page="iaa">인앱광고 테스트하기</button>',
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Vanilla TypeScript 샘플 주입 메타데이터 (ts)
103
+ */
104
+ const TS_SAMPLE_CONFIG = {
105
+ iap: {
106
+ displayName: "인앱결제",
107
+ import:
108
+ 'import { mountInAppPurchasePage } from "./pages/InAppPurchasePage.ts";',
109
+ route: ` if (currentPage === "iap") {
110
+ mountInAppPurchasePage(() => {
111
+ currentPage = null;
112
+ render();
113
+ });
114
+ return;
115
+ }`,
116
+ getButton: () =>
117
+ '<button type="button" class="app-button app-button-ghost" data-page="iap">인앱결제 테스트하기</button>',
118
+ },
119
+ iaa: {
120
+ displayName: "인앱광고",
121
+ import: 'import { mountInAppAdsPage } from "./pages/InAppAdsPage.ts";',
122
+ route: ` if (currentPage === "iaa") {
123
+ mountInAppAdsPage(() => {
124
+ currentPage = null;
125
+ render();
126
+ });
127
+ return;
128
+ }`,
129
+ getButton: () =>
130
+ '<button type="button" class="app-button app-button-ghost" data-page="iaa">인앱광고 테스트하기</button>',
131
+ },
132
+ };
133
+
134
+ module.exports = {
135
+ REACT_SAMPLE_CONFIG,
136
+ REACT_JS_SAMPLE_CONFIG,
137
+ REACT_TDS_SAMPLE_CONFIG,
138
+ JS_SAMPLE_CONFIG,
139
+ TS_SAMPLE_CONFIG,
140
+ };
@@ -0,0 +1,65 @@
1
+ function injectReactSamples(
2
+ appContent,
3
+ sampleChoices,
4
+ sampleConfig,
5
+ isTypeScript,
6
+ ) {
7
+ const hasSamples = sampleChoices.length > 0;
8
+ const sampleImports = hasSamples
9
+ ? sampleChoices
10
+ .map((id) => sampleConfig[id]?.import)
11
+ .filter(Boolean)
12
+ .join("\n") + '\nimport { useState } from "react";'
13
+ : "";
14
+ const pageState = isTypeScript
15
+ ? "useState<string | null>(null)"
16
+ : "useState(null)";
17
+ const pageStateAndRoutes = hasSamples
18
+ ? ` const [page, setPage] = ${pageState};\n\n` +
19
+ sampleChoices
20
+ .map((id) => sampleConfig[id]?.route)
21
+ .filter(Boolean)
22
+ .join("\n") +
23
+ "\n\n"
24
+ : "";
25
+ const sampleButtons = hasSamples
26
+ ? sampleChoices
27
+ .map((id) => sampleConfig[id]?.getButton())
28
+ .filter(Boolean)
29
+ .join("\n\n ")
30
+ : "";
31
+
32
+ return appContent
33
+ .replace("{{SAMPLE_IMPORTS}}", sampleImports)
34
+ .replace("{{PAGE_STATE_AND_ROUTES}}", pageStateAndRoutes)
35
+ .replace("{{SAMPLE_BUTTONS}}", sampleButtons);
36
+ }
37
+
38
+ function injectVanillaSamples(appContent, sampleChoices, sampleConfig) {
39
+ const hasSamples = sampleChoices.length > 0;
40
+ const sampleImports = hasSamples
41
+ ? sampleChoices
42
+ .map((id) => sampleConfig[id]?.import)
43
+ .filter(Boolean)
44
+ .join("\n")
45
+ : "";
46
+ const sampleRoutes = hasSamples
47
+ ? sampleChoices
48
+ .map((id) => sampleConfig[id]?.route)
49
+ .filter(Boolean)
50
+ .join("\n")
51
+ : "";
52
+ const sampleButtons = hasSamples
53
+ ? sampleChoices
54
+ .map((id) => sampleConfig[id]?.getButton())
55
+ .filter(Boolean)
56
+ .join("\n\n ")
57
+ : "";
58
+
59
+ return appContent
60
+ .replace("{{SAMPLE_IMPORTS}}", sampleImports)
61
+ .replace("{{SAMPLE_ROUTES}}", sampleRoutes)
62
+ .replace("{{SAMPLE_BUTTONS}}", sampleButtons);
63
+ }
64
+
65
+ module.exports = { injectReactSamples, injectVanillaSamples };
@@ -0,0 +1,111 @@
1
+ const { execSync } = require("child_process");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { copyDir } = require("./utils/copy-dir");
5
+ const { injectReactSamples, injectVanillaSamples } = require("./sample-inject");
6
+ const { SAMPLE_PRIMARY_COLOR } = require("./templates");
7
+
8
+ function scaffoldProject({
9
+ templateDir,
10
+ targetDir,
11
+ template,
12
+ sampleConfig,
13
+ sampleChoices,
14
+ projectName,
15
+ packageName,
16
+ packageManager,
17
+ }) {
18
+ copyDir(templateDir, targetDir, { exclude: ["samples"] });
19
+
20
+ const pkgPath = path.join(targetDir, "package.json");
21
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
22
+ pkg.name = packageName;
23
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
24
+
25
+ const configPath = path.join(targetDir, "granite.config.ts");
26
+ const configContent = fs.readFileSync(configPath, "utf-8");
27
+ fs.writeFileSync(
28
+ configPath,
29
+ configContent
30
+ .replace("{{APP_NAME}}", projectName)
31
+ .replace(
32
+ "{{PRIMARY_COLOR}}",
33
+ SAMPLE_PRIMARY_COLOR[
34
+ Math.floor(Math.random() * SAMPLE_PRIMARY_COLOR.length)
35
+ ],
36
+ ),
37
+ );
38
+
39
+ const readmePath = path.join(targetDir, "README.md");
40
+ const pmDev =
41
+ packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
42
+ const pmBuild =
43
+ packageManager === "npm" ? "npm run build" : `${packageManager} build`;
44
+ const pmDeploy =
45
+ packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
46
+
47
+ let readmeContent = fs.readFileSync(readmePath, "utf-8");
48
+ readmeContent = readmeContent
49
+ .replace(/\{\{APP_NAME\}\}/g, projectName)
50
+ .replace(/\{\{PM_DEV\}\}/g, pmDev)
51
+ .replace(/\{\{PM_BUILD\}\}/g, pmBuild)
52
+ .replace(/\{\{PM_DEPLOY\}\}/g, pmDeploy);
53
+ fs.writeFileSync(readmePath, readmeContent);
54
+
55
+ const samplesDir = path.join(templateDir, "samples");
56
+ for (const id of sampleChoices) {
57
+ const sampleRoot = path.join(samplesDir, id);
58
+ if (fs.existsSync(sampleRoot)) {
59
+ copyDir(sampleRoot, targetDir);
60
+ }
61
+ }
62
+
63
+ const appPath = path.join(targetDir, template.appFile);
64
+ if (fs.existsSync(appPath)) {
65
+ let appContent = fs.readFileSync(appPath, "utf-8");
66
+ appContent = template.isVanilla
67
+ ? injectVanillaSamples(appContent, sampleChoices, sampleConfig)
68
+ : injectReactSamples(
69
+ appContent,
70
+ sampleChoices,
71
+ sampleConfig,
72
+ template.isTypeScript,
73
+ );
74
+ fs.writeFileSync(appPath, appContent);
75
+ }
76
+ }
77
+
78
+ function installDependencies(targetDir, packageManager) {
79
+ console.log(`📦 의존성을 설치합니다...\n`);
80
+
81
+ const installCommands = {
82
+ npm: "npm install",
83
+ yarn: "yarn",
84
+ pnpm: "pnpm install",
85
+ };
86
+ execSync(installCommands[packageManager], {
87
+ stdio: "inherit",
88
+ cwd: targetDir,
89
+ });
90
+
91
+ console.log(`\n📦 @apps-in-toss/web-framework 최신 버전을 설치합니다...\n`);
92
+
93
+ const addCmd = { npm: "npm install", yarn: "yarn add", pnpm: "pnpm add" };
94
+ execSync(`${addCmd[packageManager]} @apps-in-toss/web-framework@latest`, {
95
+ stdio: "inherit",
96
+ cwd: targetDir,
97
+ });
98
+ }
99
+
100
+ function formatProject(targetDir, packageManager) {
101
+ console.log(`\n📐 코드 포맷팅을 실행합니다...\n`);
102
+ const formatCmd =
103
+ packageManager === "npm" ? "npm run format" : `${packageManager} format`;
104
+ execSync(formatCmd, { stdio: "inherit", cwd: targetDir });
105
+ }
106
+
107
+ module.exports = {
108
+ scaffoldProject,
109
+ installDependencies,
110
+ formatProject,
111
+ };
package/src/skills.js ADDED
@@ -0,0 +1,66 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { fetchText } = require("./utils/fetch-text");
4
+
5
+ async function writeAiSkills({ targetDir, aiTool, useTds }) {
6
+ console.log(`\n📄 AI skills 파일을 추가합니다... (${aiTool})\n`);
7
+
8
+ const aitDocs = await fetchText(
9
+ "https://developers-apps-in-toss.toss.im/llms.txt",
10
+ );
11
+ let tdsDocs;
12
+ if (useTds) {
13
+ tdsDocs = await fetchText(
14
+ "https://tossmini-docs.toss.im/tds-mobile/llms-full.txt",
15
+ );
16
+ }
17
+
18
+ if (aiTool === "cursor") {
19
+ const skillsDir = path.join(targetDir, ".cursor", "skills");
20
+ fs.mkdirSync(skillsDir, { recursive: true });
21
+ fs.writeFileSync(path.join(skillsDir, "apps-in-toss.md"), aitDocs);
22
+ console.log(" ✓ .cursor/skills/apps-in-toss.md 추가 완료");
23
+ if (tdsDocs) {
24
+ fs.writeFileSync(path.join(skillsDir, "tds-mobile.md"), tdsDocs);
25
+ console.log(" ✓ .cursor/skills/tds-mobile.md 추가 완료");
26
+ }
27
+ } else if (aiTool === "claude") {
28
+ const docsDir = path.join(targetDir, "docs", "skills");
29
+ fs.mkdirSync(docsDir, { recursive: true });
30
+
31
+ fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
32
+ console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
33
+
34
+ if (tdsDocs) {
35
+ fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
36
+ console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
37
+ }
38
+
39
+ let claudeMd = "@docs/skills/apps-in-toss.md\n";
40
+ if (tdsDocs) {
41
+ claudeMd += "@docs/skills/tds-mobile.md\n";
42
+ }
43
+ fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), claudeMd);
44
+ console.log(" ✓ CLAUDE.md 추가 완료");
45
+ } else if (aiTool === "codex") {
46
+ const docsDir = path.join(targetDir, "docs", "skills");
47
+ fs.mkdirSync(docsDir, { recursive: true });
48
+
49
+ fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
50
+ console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
51
+
52
+ if (tdsDocs) {
53
+ fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
54
+ console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
55
+ }
56
+
57
+ let agentsMd = "docs/skills/apps-in-toss.md 파일을 참고하세요.\n";
58
+ if (tdsDocs) {
59
+ agentsMd += "docs/skills/tds-mobile.md 파일을 참고하세요.\n";
60
+ }
61
+ fs.writeFileSync(path.join(targetDir, "AGENTS.md"), agentsMd);
62
+ console.log(" ✓ AGENTS.md 추가 완료");
63
+ }
64
+ }
65
+
66
+ module.exports = { writeAiSkills };
@@ -0,0 +1,88 @@
1
+ const path = require("path");
2
+ const {
3
+ REACT_SAMPLE_CONFIG,
4
+ REACT_JS_SAMPLE_CONFIG,
5
+ REACT_TDS_SAMPLE_CONFIG,
6
+ JS_SAMPLE_CONFIG,
7
+ TS_SAMPLE_CONFIG,
8
+ } = require("./sample-configs");
9
+
10
+ const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
11
+
12
+ const TEMPLATE_IDS = ["react-ts", "react", "js", "ts"];
13
+
14
+ const TEMPLATE_REGISTRY = {
15
+ "react-ts": {
16
+ appFile: "src/App.tsx",
17
+ isVanilla: false,
18
+ isTypeScript: true,
19
+ useTds: false,
20
+ sampleConfig: REACT_SAMPLE_CONFIG,
21
+ },
22
+ react: {
23
+ appFile: "src/App.jsx",
24
+ isVanilla: false,
25
+ isTypeScript: false,
26
+ useTds: false,
27
+ sampleConfig: REACT_JS_SAMPLE_CONFIG,
28
+ },
29
+ "react-ts-tds": {
30
+ appFile: "src/App.tsx",
31
+ isVanilla: false,
32
+ isTypeScript: true,
33
+ useTds: true,
34
+ sampleConfig: REACT_TDS_SAMPLE_CONFIG,
35
+ },
36
+ js: {
37
+ appFile: "src/app.js",
38
+ isVanilla: true,
39
+ isTypeScript: false,
40
+ useTds: false,
41
+ sampleConfig: JS_SAMPLE_CONFIG,
42
+ },
43
+ ts: {
44
+ appFile: "src/app.ts",
45
+ isVanilla: true,
46
+ isTypeScript: true,
47
+ useTds: false,
48
+ sampleConfig: TS_SAMPLE_CONFIG,
49
+ },
50
+ };
51
+
52
+ const SAMPLE_PRIMARY_COLOR = [
53
+ "#FF8A65",
54
+ "#FD9B3C",
55
+ "#E0B20C",
56
+ "#3FD599",
57
+ "#81C784",
58
+ "#4DB6AC",
59
+ "#4DD0E1",
60
+ "#64B5F6",
61
+ "#655DFF",
62
+ "#9575CD",
63
+ "#BA68C8",
64
+ "#FF91D5",
65
+ "#F06292",
66
+ "#D7B59E",
67
+ ];
68
+
69
+ const TEMPLATE_CHOICES = [
70
+ { name: "react-ts — React + TypeScript (기본)", value: "react-ts" },
71
+ { name: "react — React + JavaScript", value: "react" },
72
+ { name: "js — Vanilla JavaScript", value: "js" },
73
+ { name: "ts — Vanilla TypeScript", value: "ts" },
74
+ ];
75
+
76
+ function resolveTemplateFolder(baseTemplateId, useTds) {
77
+ if (baseTemplateId === "react-ts" && useTds) return "react-ts-tds";
78
+ return baseTemplateId;
79
+ }
80
+
81
+ module.exports = {
82
+ TEMPLATES_DIR,
83
+ TEMPLATE_IDS,
84
+ TEMPLATE_REGISTRY,
85
+ TEMPLATE_CHOICES,
86
+ SAMPLE_PRIMARY_COLOR,
87
+ resolveTemplateFolder,
88
+ };
@@ -0,0 +1,19 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function copyDir(src, dest, { exclude = [] } = {}) {
5
+ fs.mkdirSync(dest, { recursive: true });
6
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
7
+ if (exclude.includes(entry.name)) continue;
8
+
9
+ const srcPath = path.join(src, entry.name);
10
+ const destPath = path.join(dest, entry.name);
11
+ if (entry.isDirectory()) {
12
+ copyDir(srcPath, destPath, { exclude });
13
+ } else {
14
+ fs.copyFileSync(srcPath, destPath);
15
+ }
16
+ }
17
+ }
18
+
19
+ module.exports = { copyDir };
@@ -0,0 +1,47 @@
1
+ const https = require("https");
2
+ const http = require("http");
3
+ const zlib = require("zlib");
4
+
5
+ function fetchText(url) {
6
+ return new Promise((resolve, reject) => {
7
+ const get = (targetUrl) => {
8
+ const client = targetUrl.startsWith("https") ? https : http;
9
+ const req = client.get(
10
+ targetUrl,
11
+ { headers: { "Accept-Encoding": "gzip, deflate" } },
12
+ (res) => {
13
+ if (
14
+ res.statusCode >= 300 &&
15
+ res.statusCode < 400 &&
16
+ res.headers.location
17
+ ) {
18
+ get(res.headers.location);
19
+ return;
20
+ }
21
+ if (res.statusCode !== 200) {
22
+ reject(new Error(`HTTP ${res.statusCode} for ${targetUrl}`));
23
+ return;
24
+ }
25
+
26
+ let stream = res;
27
+ const encoding = res.headers["content-encoding"];
28
+ if (encoding === "gzip") {
29
+ stream = res.pipe(zlib.createGunzip());
30
+ } else if (encoding === "deflate") {
31
+ stream = res.pipe(zlib.createInflate());
32
+ }
33
+
34
+ let data = "";
35
+ stream.setEncoding("utf-8");
36
+ stream.on("data", (chunk) => (data += chunk));
37
+ stream.on("end", () => resolve(data));
38
+ stream.on("error", reject);
39
+ },
40
+ );
41
+ req.on("error", reject);
42
+ };
43
+ get(url);
44
+ });
45
+ }
46
+
47
+ module.exports = { fetchText };
@@ -0,0 +1,26 @@
1
+ function toNpmPackageName(input) {
2
+ const raw = String(input || "").trim();
3
+ if (!raw) return "my-app";
4
+
5
+ if (raw.startsWith("@")) {
6
+ const slash = raw.indexOf("/");
7
+ if (slash > 1) {
8
+ const scope = raw.slice(0, slash).toLowerCase();
9
+ const name = raw.slice(slash + 1);
10
+ const normalizedName = toNpmPackageName(name);
11
+ return `${scope}/${normalizedName}`;
12
+ }
13
+ }
14
+
15
+ return (
16
+ raw
17
+ .toLowerCase()
18
+ .replace(/\s+/g, "-")
19
+ .replace(/[^a-z0-9._-]/g, "-")
20
+ .replace(/-+/g, "-")
21
+ .replace(/^[._-]+/, "")
22
+ .replace(/[._-]+$/, "") || "my-app"
23
+ );
24
+ }
25
+
26
+ module.exports = { toNpmPackageName };
@@ -0,0 +1,14 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+
4
+ export default [
5
+ { ignores: ["dist"] },
6
+ {
7
+ files: ["**/*.{js,mjs}"],
8
+ ...js.configs.recommended,
9
+ languageOptions: {
10
+ ecmaVersion: 2020,
11
+ globals: globals.browser,
12
+ },
13
+ },
14
+ ];
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AIT App</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.js"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "granite dev",
8
+ "build": "ait build",
9
+ "deploy": "ait deploy",
10
+ "lint": "eslint .",
11
+ "format": "prettier --write ."
12
+ },
13
+ "devDependencies": {
14
+ "@eslint/js": "^9.21.0",
15
+ "eslint": "^9.21.0",
16
+ "globals": "^15.15.0",
17
+ "prettier": "^3.4.2",
18
+ "vite": "^6.2.0"
19
+ }
20
+ }