sonamu 0.10.13 → 0.11.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 (233) hide show
  1. package/dist/ai/agents/agent.d.ts.map +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
  4. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -1
  5. package/dist/api/base-frame.d.ts.map +1 -1
  6. package/dist/api/decorators.d.ts.map +1 -1
  7. package/dist/api/sonamu.d.ts.map +1 -1
  8. package/dist/api/sonamu.js +3 -2
  9. package/dist/api/validator.d.ts.map +1 -1
  10. package/dist/auth/audit-log/events.d.ts.map +1 -1
  11. package/dist/auth/index.d.ts +1 -1
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +3 -3
  14. package/dist/auth/knex-adapter.d.ts.map +1 -1
  15. package/dist/auth/plugins/wrappers/admin.d.ts +4 -4
  16. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  17. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -1
  18. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -1
  19. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -1
  20. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -1
  21. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -1
  22. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  23. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
  24. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  25. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  26. package/dist/bin/build-config.d.ts +10 -0
  27. package/dist/bin/build-config.d.ts.map +1 -1
  28. package/dist/bin/build-config.js +130 -6
  29. package/dist/bin/fixture.d.ts +7 -4
  30. package/dist/bin/fixture.d.ts.map +1 -1
  31. package/dist/bin/fixture.js +161 -13
  32. package/dist/bin/test-command.d.ts +7 -0
  33. package/dist/bin/test-command.d.ts.map +1 -1
  34. package/dist/bin/test-command.js +18 -4
  35. package/dist/bin/typecheck-web.d.ts +2 -0
  36. package/dist/bin/typecheck-web.d.ts.map +1 -0
  37. package/dist/bin/typecheck-web.js +36 -0
  38. package/dist/cache/decorator.d.ts.map +1 -1
  39. package/dist/cache/drivers.d.ts.map +1 -1
  40. package/dist/cache-control/cache-control.d.ts.map +1 -1
  41. package/dist/compress/compress.d.ts.map +1 -1
  42. package/dist/cone/cone-generator.js +1 -1
  43. package/dist/database/base-model.d.ts.map +1 -1
  44. package/dist/database/base-model.types.d.ts +0 -1
  45. package/dist/database/base-model.types.d.ts.map +1 -1
  46. package/dist/database/db.d.ts.map +1 -1
  47. package/dist/database/puri-subset.types.d.ts +0 -1
  48. package/dist/database/puri-subset.types.d.ts.map +1 -1
  49. package/dist/database/puri-wrapper.d.ts +0 -1
  50. package/dist/database/puri-wrapper.d.ts.map +1 -1
  51. package/dist/database/puri.d.ts +0 -1
  52. package/dist/database/puri.d.ts.map +1 -1
  53. package/dist/database/puri.types.d.ts +0 -1
  54. package/dist/database/puri.types.d.ts.map +1 -1
  55. package/dist/database/transaction-context.d.ts.map +1 -1
  56. package/dist/database/upsert-builder.d.ts.map +1 -1
  57. package/dist/dict/en.d.ts +58 -58
  58. package/dist/dict/en.d.ts.map +1 -1
  59. package/dist/dict/ko.d.ts +58 -58
  60. package/dist/dict/ko.d.ts.map +1 -1
  61. package/dist/dict/rc-keys.d.ts.map +1 -1
  62. package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
  63. package/dist/dict/sonamu-dictionary.js +2 -2
  64. package/dist/dict/utils.d.ts.map +1 -1
  65. package/dist/entity/entity-manager.d.ts +115 -113
  66. package/dist/entity/entity-manager.d.ts.map +1 -1
  67. package/dist/entity/entity-manager.js +25 -19
  68. package/dist/entity/entity-template-cone.d.ts +4 -4
  69. package/dist/entity/entity.d.ts +6 -1
  70. package/dist/entity/entity.d.ts.map +1 -1
  71. package/dist/entity/entity.js +8 -3
  72. package/dist/env.d.ts.map +1 -1
  73. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  74. package/dist/filter/types.d.ts.map +1 -1
  75. package/dist/index.js +6 -6
  76. package/dist/logger/configure.d.ts +10 -1
  77. package/dist/logger/configure.d.ts.map +1 -1
  78. package/dist/logger/configure.js +30 -5
  79. package/dist/migration/migration-set.d.ts.map +1 -1
  80. package/dist/migration/migration-set.js +17 -6
  81. package/dist/migration/migrator.d.ts +12 -1
  82. package/dist/migration/migrator.d.ts.map +1 -1
  83. package/dist/migration/migrator.js +77 -30
  84. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  85. package/dist/migration/slack-confirm.d.ts.map +1 -1
  86. package/dist/naite/messaging-types.d.ts +0 -1
  87. package/dist/naite/messaging-types.d.ts.map +1 -1
  88. package/dist/naite/naite-reporter.d.ts +0 -1
  89. package/dist/naite/naite-reporter.d.ts.map +1 -1
  90. package/dist/naite/naite.d.ts.map +1 -1
  91. package/dist/storage/base-file.d.ts.map +1 -1
  92. package/dist/storage/buffered-file.d.ts.map +1 -1
  93. package/dist/storage/drivers.d.ts.map +1 -1
  94. package/dist/storage/s3-driver.d.ts.map +1 -1
  95. package/dist/storage/storage-manager.d.ts.map +1 -1
  96. package/dist/storage/uploaded-file.d.ts.map +1 -1
  97. package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
  98. package/dist/stream/ws-audience.d.ts.map +1 -1
  99. package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
  100. package/dist/stream/ws-delivery.d.ts.map +1 -1
  101. package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
  102. package/dist/stream/ws-presence-store.d.ts.map +1 -1
  103. package/dist/stream/ws-registry.d.ts.map +1 -1
  104. package/dist/stream/ws-telemetry-memory.d.ts.map +1 -1
  105. package/dist/stream/ws-telemetry.d.ts.map +1 -1
  106. package/dist/stream/ws.d.ts.map +1 -1
  107. package/dist/syncer/api-parser.js +2 -2
  108. package/dist/syncer/file-patterns.d.ts +17 -17
  109. package/dist/syncer/filesystem-dependencies.js +1 -1
  110. package/dist/syncer/syncer.d.ts.map +1 -1
  111. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  112. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  113. package/dist/template/implementations/entity.template.d.ts +2 -2
  114. package/dist/template/implementations/entity.template.d.ts.map +1 -1
  115. package/dist/template/implementations/entry-server.template.d.ts +2 -2
  116. package/dist/template/implementations/entry-server.template.d.ts.map +1 -1
  117. package/dist/template/implementations/generated.template.d.ts +2 -2
  118. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  119. package/dist/template/implementations/generated_http.template.d.ts +2 -2
  120. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  121. package/dist/template/implementations/generated_sso.template.d.ts +2 -2
  122. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  123. package/dist/template/implementations/http_validators.template.d.ts +2 -2
  124. package/dist/template/implementations/http_validators.template.d.ts.map +1 -1
  125. package/dist/template/implementations/init_types.template.d.ts +2 -2
  126. package/dist/template/implementations/init_types.template.d.ts.map +1 -1
  127. package/dist/template/implementations/model.template.d.ts +2 -2
  128. package/dist/template/implementations/model.template.d.ts.map +1 -1
  129. package/dist/template/implementations/model_test.template.d.ts +2 -2
  130. package/dist/template/implementations/model_test.template.d.ts.map +1 -1
  131. package/dist/template/implementations/queries.template.d.ts +2 -2
  132. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.d.ts +2 -2
  134. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  135. package/dist/template/implementations/sd.template.js +1 -1
  136. package/dist/template/implementations/services.template.d.ts +2 -2
  137. package/dist/template/implementations/services.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_form.template.d.ts +3 -3
  139. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  140. package/dist/template/implementations/view_id_all_select.template.d.ts +2 -2
  141. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -1
  142. package/dist/template/implementations/view_list.template.d.ts +2 -2
  143. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  144. package/dist/template/implementations/view_search_input.template.d.ts +2 -2
  145. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  146. package/dist/template/template-manager.d.ts.map +1 -1
  147. package/dist/template/template.d.ts.map +1 -1
  148. package/dist/template/zod-converter.d.ts.map +1 -1
  149. package/dist/testing/_relation-graph.d.ts.map +1 -1
  150. package/dist/testing/data-explorer.d.ts +2 -2
  151. package/dist/testing/data-explorer.d.ts.map +1 -1
  152. package/dist/testing/dev-vitest-manager.d.ts.map +1 -1
  153. package/dist/testing/faker-mappings.d.ts.map +1 -1
  154. package/dist/testing/fixture-generator.d.ts.map +1 -1
  155. package/dist/testing/fixture-generator.js +1 -1
  156. package/dist/testing/fixture-loader.d.ts.map +1 -1
  157. package/dist/testing/fixture-manager.d.ts +4 -0
  158. package/dist/testing/fixture-manager.d.ts.map +1 -1
  159. package/dist/testing/fixture-manager.js +99 -25
  160. package/dist/testing/naite-vitest-reporter.d.ts.map +1 -1
  161. package/dist/testing/parallel-db-manager.d.ts.map +1 -1
  162. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts +10 -0
  163. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts.map +1 -0
  164. package/dist/tooling/__tests__/helpers/sonamu-test-root.js +42 -0
  165. package/dist/tooling/cdd-service.d.ts +87 -0
  166. package/dist/tooling/cdd-service.d.ts.map +1 -0
  167. package/dist/tooling/cdd-service.js +215 -0
  168. package/dist/tooling/cli-tooling.d.ts +11 -0
  169. package/dist/tooling/cli-tooling.d.ts.map +1 -0
  170. package/dist/tooling/cli-tooling.js +901 -0
  171. package/dist/tooling/fixture-service.d.ts +29 -0
  172. package/dist/tooling/fixture-service.d.ts.map +1 -0
  173. package/dist/tooling/fixture-service.js +53 -0
  174. package/dist/tooling/index.d.ts +2 -0
  175. package/dist/tooling/index.d.ts.map +1 -0
  176. package/dist/tooling/index.js +2 -0
  177. package/dist/tooling/test-run-client.d.ts +27 -0
  178. package/dist/tooling/test-run-client.d.ts.map +1 -0
  179. package/dist/tooling/test-run-client.js +120 -0
  180. package/dist/tsdown.api.config.js +48 -0
  181. package/dist/types/types.d.ts +527 -527
  182. package/dist/types/types.d.ts.map +1 -1
  183. package/dist/ui/ai-client.d.ts.map +1 -1
  184. package/dist/utils/formatter.d.ts +6 -0
  185. package/dist/utils/formatter.d.ts.map +1 -1
  186. package/dist/utils/formatter.js +42 -11
  187. package/dist/utils/model.d.ts +1 -1
  188. package/dist/vector/chunking.d.ts.map +1 -1
  189. package/dist/vector/embedding.d.ts.map +1 -1
  190. package/package.json +13 -12
  191. package/src/api/sonamu.ts +2 -1
  192. package/src/auth/index.ts +1 -1
  193. package/src/bin/__tests__/zod-compiler-build.test.ts +67 -0
  194. package/src/bin/build-config.ts +151 -5
  195. package/src/bin/fixture.ts +204 -21
  196. package/src/bin/test-command.ts +27 -4
  197. package/src/bin/typecheck-web.ts +56 -0
  198. package/src/dict/sonamu-dictionary.ts +1 -1
  199. package/src/entity/__tests__/entity-manager-autoload.contract.test.ts +55 -0
  200. package/src/entity/__tests__/entity-manager-explicit-root-registration.contract.test.ts +107 -0
  201. package/src/entity/entity-manager.ts +31 -19
  202. package/src/entity/entity.ts +7 -2
  203. package/src/logger/__tests__/configure.test.ts +33 -0
  204. package/src/logger/configure.ts +45 -3
  205. package/src/migration/__tests__/migrator.test.ts +51 -0
  206. package/src/migration/migration-set.ts +22 -5
  207. package/src/migration/migrator.ts +103 -34
  208. package/src/syncer/api-parser.ts +1 -1
  209. package/src/testing/fixture-manager.ts +124 -32
  210. package/src/tooling/__tests__/cdd-rule-bootstrap.contract.test.ts +118 -0
  211. package/src/tooling/__tests__/cli-tooling-auth.contract.test.ts +72 -0
  212. package/src/tooling/__tests__/cli-tooling-cone-locale.contract.test.ts +150 -0
  213. package/src/tooling/__tests__/cli-tooling-entity-patch.contract.test.ts +131 -0
  214. package/src/tooling/__tests__/cli-tooling-fixture-transfer.contract.test.ts +255 -0
  215. package/src/tooling/__tests__/cli-tooling-i18n-update.contract.test.ts +104 -0
  216. package/src/tooling/__tests__/cli-tooling-scaffold.contract.test.ts +86 -0
  217. package/src/tooling/__tests__/cli-tooling-stub-practice.contract.test.ts +147 -0
  218. package/src/tooling/__tests__/default-cdd-adapter.contract.test.ts +73 -0
  219. package/src/tooling/__tests__/helpers/sonamu-test-root.ts +42 -0
  220. package/src/tooling/__tests__/test-run-client-config.contract.test.ts +154 -0
  221. package/src/tooling/cdd-service.ts +256 -0
  222. package/src/tooling/cli-tooling.ts +937 -0
  223. package/src/tooling/fixture-service.ts +72 -0
  224. package/src/tooling/index.ts +1 -0
  225. package/src/tooling/test-run-client.ts +196 -0
  226. package/src/utils/__tests__/formatter-bin.test.ts +99 -0
  227. package/src/utils/formatter.ts +74 -10
  228. package/tsdown.api.config.ts +8 -10
  229. package/bin/cli.js +0 -75
  230. package/dist/bin/cli.d.ts +0 -2
  231. package/dist/bin/cli.d.ts.map +0 -1
  232. package/dist/bin/cli.js +0 -956
  233. package/src/bin/cli.ts +0 -1166
package/src/bin/cli.ts DELETED
@@ -1,1166 +0,0 @@
1
- import assert from "assert";
2
- import { spawn, spawnSync } from "child_process";
3
- import { mkdir, open, readFile, readdir, rm, writeFile } from "fs/promises";
4
- import { createRequire } from "module";
5
- import path from "path";
6
- import process from "process";
7
-
8
- import chalk from "chalk";
9
- import knex from "knex";
10
- import { type Knex } from "knex";
11
- import { tsicli } from "tsicli";
12
-
13
- import { Sonamu } from "../api/sonamu";
14
- import { addCompanionsToEntities, generateBetterAuthEntities } from "../auth/auth-generator";
15
- import { isValidPluginId, SUPPORTED_PLUGIN_IDS } from "../auth/plugins/entity-definitions";
16
- import { type BetterAuthPluginId } from "../auth/plugins/entity-definitions";
17
- import { type SonamuDBConfig } from "../database/db";
18
- import { EntityManager } from "../entity/entity-manager";
19
- import { getSonamuEnvironment } from "../env";
20
- import { Migrator } from "../migration/migrator";
21
- import { FixtureManager } from "../testing/fixture-manager";
22
- import {
23
- execWithLinePrefix,
24
- printBuildSummary,
25
- printTaskFailed,
26
- printTaskHeader,
27
- printTaskStart,
28
- printTaskSuccess,
29
- } from "../utils/console-util";
30
- import { exists } from "../utils/fs-utils";
31
- import { findApiRootPath, findAppRootPath } from "../utils/utils";
32
- import { API_ARTIFACTS, WEB_ARTIFACTS } from "./build-config";
33
- import { type BuildArtifact } from "./build-config";
34
- import {
35
- assertApiZodCompilerBuildOutput,
36
- createApiZodCompilerBuildWrapper,
37
- HTTP_VALIDATOR_REGISTRY_BUILD_PATH,
38
- HTTP_VALIDATOR_REGISTRY_SOURCE_PATH,
39
- } from "./build-config";
40
- import { loadBuildCompilerPolicy } from "./compiler-policy";
41
- import { fixtureExploreCommand, fixtureFetchCommand, fixtureGenCommand } from "./fixture";
42
- import { getMigrateRunTargets } from "./migrate-targets";
43
- import { testCommand } from "./test-command";
44
-
45
- let migrator: Migrator;
46
-
47
- function runMysql(
48
- args: string[],
49
- stdio: [number | "ignore", number | "inherit", "inherit"] = ["ignore", "inherit", "inherit"],
50
- ) {
51
- const result = spawnSync("mysql", args, { stdio });
52
- if (result.error) {
53
- throw result.error;
54
- }
55
- if (result.status !== 0) {
56
- throw new Error(`mysql 명령이 종료 코드 ${result.status ?? "unknown"}로 실패했습니다.`);
57
- }
58
- }
59
-
60
- async function writeMysqlDump(args: string[], outputPath: string) {
61
- const output = await open(outputPath, "w");
62
- try {
63
- const result = spawnSync("mysqldump", args, { stdio: ["ignore", output.fd, "inherit"] });
64
- if (result.error) {
65
- throw result.error;
66
- }
67
- if (result.status !== 0) {
68
- throw new Error(`mysqldump 명령이 종료 코드 ${result.status ?? "unknown"}로 실패했습니다.`);
69
- }
70
- } finally {
71
- await output.close();
72
- }
73
- }
74
-
75
- async function runMysqlWithInput(args: string[], inputPath: string) {
76
- const input = await open(inputPath, "r");
77
- try {
78
- runMysql(args, [input.fd, "inherit", "inherit"]);
79
- } finally {
80
- await input.close();
81
- }
82
- }
83
-
84
- function openInCode(pathToOpen: string) {
85
- const result = spawnSync("code", [pathToOpen], { stdio: ["ignore", "inherit", "inherit"] });
86
- if (result.error) {
87
- throw result.error;
88
- }
89
- if (result.status !== 0) {
90
- throw new Error(`code 명령이 종료 코드 ${result.status ?? "unknown"}로 실패했습니다.`);
91
- }
92
- }
93
-
94
- function mysqlConnectionArgs(connection: Knex.ConnectionConfig) {
95
- return [
96
- `-h${String(connection.host)}`,
97
- `-u${String(connection.user)}`,
98
- `-p${String(connection.password)}`,
99
- ];
100
- }
101
-
102
- function copyToClipboard(value: string) {
103
- const result = spawnSync("pbcopy", [], { input: value, stdio: ["pipe", "inherit", "inherit"] });
104
- if (result.error) {
105
- throw result.error;
106
- }
107
- if (result.status !== 0) {
108
- throw new Error(`pbcopy 명령이 종료 코드 ${result.status ?? "unknown"}로 실패했습니다.`);
109
- }
110
- }
111
-
112
- /**
113
- * CLI 옵션을 파싱하는 헬퍼 함수
114
- */
115
- function parseCliOptions(argv: string[] = process.argv) {
116
- const flags = new Set<string>();
117
- const options: Record<string, string> = {};
118
-
119
- for (let i = 0; i < argv.length; i++) {
120
- const arg = argv[i];
121
- if (!arg.startsWith("--")) continue;
122
-
123
- // --option=value 형식
124
- if (arg.includes("=")) {
125
- const [key, value] = arg.slice(2).split("=");
126
- options[key] = value;
127
- continue;
128
- }
129
-
130
- // --option value 형식인지 확인
131
- const nextArg = argv[i + 1];
132
- if (nextArg && !nextArg.startsWith("--") && !nextArg.startsWith("-")) {
133
- options[arg.slice(2)] = nextArg;
134
- i++; // 다음 arg는 값이므로 스킵
135
- } else {
136
- // --flag 형식
137
- flags.add(arg.slice(2));
138
- }
139
- }
140
-
141
- return { flags, options };
142
- }
143
-
144
- async function bootstrap() {
145
- const notToInit = ["dev", "build", "start", "skills", "test"].includes(process.argv[2] ?? "");
146
- if (!notToInit) {
147
- await Sonamu.init(false, false);
148
- }
149
-
150
- try {
151
- // tsicli는 정확한 명령어 매칭만 지원하므로, --로 시작하는 옵션과 그 값을 필터링합니다.
152
- // 옵션 파싱은 각 runner 함수에서 원본 process.argv를 사용하여 수행합니다.
153
- // "--"(bare double dash)는 passthrough 구분자이므로, 그 뒤의 모든 인자도 제외합니다.
154
- const filteredArgv: string[] = [];
155
- let skipNext = false;
156
- let afterDoubleDash = false;
157
- for (let i = 0; i < process.argv.length; i++) {
158
- const arg = process.argv[i];
159
- if (arg === "--") {
160
- afterDoubleDash = true;
161
- continue;
162
- }
163
- if (afterDoubleDash) continue;
164
- if (skipNext) {
165
- skipNext = false;
166
- continue;
167
- }
168
- if (arg.startsWith("--")) {
169
- // --option=value 형식은 이 arg만 스킵
170
- if (arg.includes("=")) {
171
- continue;
172
- }
173
- // --option value 형식인지 확인: 다음 arg가 --로 시작하지 않으면 값이 있는 것
174
- const nextArg = process.argv[i + 1];
175
- if (nextArg && !nextArg.startsWith("--") && !nextArg.startsWith("-")) {
176
- skipNext = true;
177
- }
178
- continue;
179
- }
180
- filteredArgv.push(arg);
181
- }
182
-
183
- // build/dev 명령어가 서브커맨드 없이 호출될 때 "all"을 기본값으로 추가합니다.
184
- // 예: `sonamu build` → `sonamu build all`, `sonamu dev` → `sonamu dev all`
185
- const cmd = filteredArgv[2];
186
- if ((cmd === "build" || cmd === "dev") && filteredArgv.length === 3) {
187
- filteredArgv.push("all");
188
- }
189
-
190
- // test 커맨드는 가변 인자(파일명, --pattern 등)를 받으므로 tsicli를 우회합니다.
191
- if (cmd === "test") {
192
- return testCommand();
193
- }
194
-
195
- await tsicli(filteredArgv, {
196
- types: {
197
- "#entityId": {
198
- type: "autocomplete",
199
- name: "#entityId",
200
- message: "Please input #entityId",
201
- choices: EntityManager.getAllParentIds().map((entityId) => ({
202
- title: entityId,
203
- value: entityId,
204
- })),
205
- },
206
- "#recordIds": "number[]",
207
- "#name": "string",
208
- "#targets": {
209
- type: "multiselect",
210
- name: "#targets",
211
- message: "Please input #targets",
212
- choices: [
213
- { title: "Development", value: "development" },
214
- { title: "Staging", value: "staging" },
215
- { title: "Production", value: "production" },
216
- { title: "Fixture", value: "fixture" },
217
- { title: "Test", value: "test" },
218
- ],
219
- },
220
- },
221
- args: [
222
- ["fixture", "init"],
223
- ["fixture", "import", "#entityId", "#recordIds"],
224
- ["fixture", "sync"],
225
- ["fixture", "gen"],
226
- ["fixture", "fetch"],
227
- ["fixture", "explore"],
228
- ["migrate", "run"],
229
- ["migrate", "apply", "#targets"],
230
- ["migrate", "generate"],
231
- ["migrate", "status"],
232
- ["stub", "practice", "#name"],
233
- ["stub", "entity", "#name"],
234
- ["scaffold", "model", "#entityId"],
235
- ["scaffold", "model_test", "#entityId"],
236
- ["scaffold", "view_list", "#entityId"],
237
- ["scaffold", "view_form", "#entityId"],
238
- ["cone", "gen", "#entityId"],
239
- ["sync"],
240
- ["build", "all"],
241
- ["build", "api"],
242
- ["build", "web"],
243
- ["dev", "all"],
244
- ["dev", "api"],
245
- ["dev", "web"],
246
- ["start"],
247
- ["skills", "sync"],
248
- ["test"],
249
- ["auth", "generate"],
250
- ["auth", "add-companions"],
251
- ],
252
- runners: {
253
- migrate_status,
254
- migrate_run,
255
- migrate_apply,
256
- migrate_generate,
257
- fixture_init,
258
- fixture_import,
259
- fixture_sync,
260
- fixture_gen,
261
- fixture_fetch,
262
- fixture_explore,
263
- stub_practice,
264
- stub_entity,
265
- scaffold_model,
266
- scaffold_model_test,
267
- scaffold_view_list,
268
- scaffold_view_form,
269
- cone_gen,
270
- sync,
271
- build_all,
272
- build_api,
273
- build_web,
274
- dev_all,
275
- dev_api,
276
- dev_web,
277
- start,
278
- skills_sync: skillsMigrationNotice,
279
- test: testCommand,
280
- auth_generate,
281
- "auth_add-companions": auth_add_companions,
282
- },
283
- });
284
- } finally {
285
- await Sonamu.destroy();
286
- }
287
- }
288
-
289
- bootstrap().finally(async () => {
290
- await FixtureManager.destroy();
291
- });
292
-
293
- /**
294
- * pnpm sync 하면 실행되는 함수입니다.
295
- * 프로젝트를 싱크합니다.
296
- *
297
- * `--force` 옵션이 주어지면 lock을 무시하고 풀-싱크를 수행합니다.
298
- * git post-merge hook이나 CI에서 매 pull 후 자동 실행할 수 있도록 노출.
299
- */
300
- async function sync() {
301
- const { flags } = parseCliOptions();
302
- if (flags.has("force")) {
303
- await Sonamu.syncer.forceSync();
304
- } else {
305
- await Sonamu.syncer.sync();
306
- }
307
- }
308
-
309
- /**
310
- * 이전 생성 프로젝트의 postinstall이 업그레이드를 막지 않도록 구 명령만 안전하게 종료합니다.
311
- */
312
- async function skillsMigrationNotice() {
313
- console.log(chalk.yellow("'sonamu skills sync' is no longer supported."));
314
- console.log(
315
- chalk.cyan("Install Sonamu skills with 'npx skills@latest add cartanova-ai/skills'."),
316
- );
317
- }
318
-
319
- /**
320
- * API 개발 서버를 실행하는 공통 로직입니다.
321
- * dev_all과 dev_api에서 공유합니다.
322
- *
323
- * TypeScript를 바로 실행할 수 있도록 @sonamu-kit/ts-loader를,
324
- * HMR을 지원하기 위해 @sonamu-kit/hmr-hook을 import하며,
325
- * 소스맵 지원을 위해 --enable-source-maps 플래그를 포함하여 실행합니다.
326
- *
327
- * 이때 @sonamu-kit/ts-loader와 @sonamu-kit/hmr-hook는 sonamu가 자체적으로 가지고 있는 dependency입니다.
328
- * 또한 실행에 사용하는 @sonamu-kit/hmr-runner도 마찬가지로 sonamu가 자체적으로 가지고 있는 dependency입니다.
329
- * 따라서 사용자 프로젝트에서는 이 세 패키지를 직접 설치할 필요가 없습니다.
330
- */
331
- function spawnApiDevServer(options?: { extraEnv?: Record<string, string> }) {
332
- const apiRoot = findApiRootPath();
333
- const entryPoint = "src/index.ts";
334
-
335
- // 이 sonamu 패키지가 dependencies로 가지고 있는 @sonamu-kit/hmr-runner의 bin/run.js를 사용합니다.
336
- // 이 경로(/bin/run.js)는 @sonamu-kit/hmr-runner의 package.json의 bin 필드에 명시되어 있는 그것과 같습니다.
337
- const hotRunnerBinPath = createRequire(import.meta.url).resolve(
338
- "@sonamu-kit/hmr-runner/bin/run.js",
339
- );
340
-
341
- const serverProcess = spawn(
342
- process.execPath, // node
343
- [
344
- hotRunnerBinPath, // 이렇게 해서 hot-runner를 실행하구요
345
- "--clear-screen=false", // 이하 hot-runner에게 넘겨줄 인자들입니다.
346
- "--node-args=--import=sonamu/ts-loader-register", // TypeScript 서포트를 위한 로더,
347
- "--node-args=--import=sonamu/hmr-hook-register", // HMR을 지원하기 위한 hook,
348
- "--node-args=--enable-source-maps", // 그리고 소스맵 지원을 위한 플래그입니다.
349
- "--on-key=r:restart:Restart server", // r 누르면 서버 재시작하게 해줘요.
350
- "--on-key=c:clear:Clear screen", // c 누르면 터미널 화면을 지워줘요.
351
- `--on-key=f:shell(rm ${path.join(apiRoot, "sonamu.lock")}):restart:Force restart`, // f 누르면 lock 제거 후 재시작 → 새 프로세스가 부트스트랩에서 풀-싱크. force sync CLI를 shell로 부르면 살아있는 서버의 watcher 폭풍이 :restart와 충돌해 상태 이상.
352
-
353
- "--on-key=enter:shell(echo hi):Key binding test", // enter를 key로 쓸 수 있음을 보이기 위한 테스트입니다.
354
- "--on-key=ctrl+f ctrl+f:shell(git pull && pnpm install && pnpm --filter sonamu build && echo 'Sonamu is now up-to-date!'):restart:Pull & install & build & restart", // modifier와의 조합, 그리고 두 개의 chord를 사용할 수 있음을 보이기 위한 테스트입니다.
355
- entryPoint, // 마지막으로 실제 실행할 스크립트의 경로를 넘겨줍니다.
356
- ],
357
- {
358
- cwd: apiRoot,
359
- stdio: "inherit",
360
- env: {
361
- ...process.env,
362
- NODE_ENV: process.env.NODE_ENV ?? "development",
363
- HOT: "yes", // 얘가 있어야 HMR이 활성화됩니다.
364
- API_ROOT_PATH: apiRoot, // 이 경로가 hmr-hook의 루트 디렉토리가 됩니다.
365
- ...options?.extraEnv,
366
- },
367
- },
368
- );
369
-
370
- // 종료 처리
371
- const cleanup = () => {
372
- console.log(chalk.yellow("\n\n👋 Shutting down..."));
373
- serverProcess.kill("SIGTERM");
374
- process.exit(0);
375
- };
376
-
377
- process.on("SIGINT", cleanup);
378
- process.on("SIGTERM", cleanup);
379
-
380
- serverProcess.on("exit", (code) => {
381
- if (code !== 0) {
382
- console.error(chalk.red(`Server exited with code ${code}`));
383
- process.exit(code || 1);
384
- }
385
- });
386
- }
387
-
388
- /**
389
- * pnpm dev / pnpm dev all 하면 실행되는 함수입니다.
390
- * 프로젝트에 대해 HMR 지원하는 개발 서버를 띄워줍니다.
391
- *
392
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
393
- */
394
- function dev_all() {
395
- const require = createRequire(import.meta.url);
396
- const { version } = require("../../package.json");
397
- console.log(`🌲 Sonamu v${version}\n`);
398
- spawnApiDevServer();
399
- }
400
-
401
- /**
402
- * pnpm dev api 하면 실행되는 함수입니다.
403
- * API 전용 개발 서버를 띄웁니다.
404
- * dev_all과 거의 동일하되, 통합 웹 서버를 비활성화합니다.
405
- *
406
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
407
- */
408
- function dev_api() {
409
- console.log(chalk.yellow.bold("Starting Sonamu API-only dev server...\n"));
410
- spawnApiDevServer({
411
- extraEnv: { SONAMU_DISABLE_INTEGRATED_WEB: "yes" },
412
- });
413
- }
414
-
415
- /**
416
- * pnpm dev web 하면 실행되는 함수입니다.
417
- * Vite 개발 서버를 단독으로 실행합니다.
418
- * -- 뒤의 인자는 Vite에 그대로 전달됩니다.
419
- *
420
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
421
- */
422
- async function dev_web() {
423
- const appRoot = findAppRootPath();
424
- const webPath = path.join(appRoot, "web");
425
-
426
- if (!(await exists(webPath))) {
427
- console.error(`web 디렉토리를 찾을 수 없습니다: ${webPath}`);
428
- process.exit(1);
429
- }
430
-
431
- // -- 뒤의 인자 추출
432
- const doubleDashIndex = process.argv.indexOf("--");
433
- const passthroughArgs = doubleDashIndex !== -1 ? process.argv.slice(doubleDashIndex + 1) : [];
434
-
435
- const viteArgs = ["exec", "vite", ...passthroughArgs];
436
-
437
- console.log(chalk.yellow.bold("Starting Vite dev server...\n"));
438
-
439
- const viteProcess = spawn("pnpm", viteArgs, {
440
- cwd: webPath,
441
- stdio: "inherit",
442
- });
443
-
444
- viteProcess.on("exit", (code) => {
445
- process.exit(code ?? 0);
446
- });
447
-
448
- // SIGINT/SIGTERM 시 Vite 프로세스를 gracefully 종료합니다.
449
- for (const signal of ["SIGINT", "SIGTERM"] as const) {
450
- process.on(signal, () => {
451
- viteProcess.kill(signal);
452
- });
453
- }
454
- }
455
-
456
- /**
457
- * API 빌드 설정 파일 경로를 결정합니다.
458
- * 프로젝트 루트에 `tsdown.config.ts`가 있으면 그것을, 없으면 sonamu 기본 설정을 사용합니다.
459
- */
460
- async function resolveApiBuildConfigPath(apiRootPath: string): Promise<string> {
461
- const localConfigPath = path.join(apiRootPath, "tsdown.config.ts");
462
-
463
- try {
464
- if (await exists(localConfigPath)) {
465
- console.log(chalk.dim("Using tsdown.config.ts from project root..."));
466
- return localConfigPath;
467
- }
468
-
469
- console.log(chalk.dim("Using default tsdown API config from sonamu package..."));
470
- return path.join(import.meta.dirname, "..", "..", "tsdown.api.config.ts");
471
- } catch (error) {
472
- console.error(chalk.red("Setting up API build config failed."), error);
473
- process.exit(1);
474
- }
475
- }
476
-
477
- /**
478
- * sonamu build / sonamu build all 하면 실행되는 함수입니다.
479
- * build_api + build_web의 합성입니다. Web 디렉토리가 없으면 Web 빌드를 스킵합니다.
480
- */
481
- async function build_all() {
482
- await build_api();
483
- await build_web({ skipIfMissing: true });
484
- }
485
-
486
- /**
487
- * pnpm build api 하면 실행되는 함수입니다.
488
- * API 프로젝트만 빌드합니다.
489
- *
490
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
491
- */
492
- async function build_api() {
493
- const apiRootPath = findApiRootPath();
494
- const baseConfigPath = await resolveApiBuildConfigPath(apiRootPath);
495
- const policy = await loadBuildCompilerPolicy(apiRootPath);
496
- const registryPath = path.join(apiRootPath, HTTP_VALIDATOR_REGISTRY_SOURCE_PATH);
497
- const configFilePath = await createApiZodCompilerBuildWrapper({
498
- apiRootPath,
499
- baseConfigPath,
500
- policy,
501
- registryPath,
502
- });
503
- const temporaryConfigPath = configFilePath === baseConfigPath ? undefined : configFilePath;
504
-
505
- const apiStartedAt = Date.now();
506
- try {
507
- for (const artifact of API_ARTIFACTS) {
508
- const cwd = apiRootPath;
509
- printTaskHeader(artifact.name, artifact.description, cwd);
510
-
511
- await runBuildSteps(artifact, { cwd, buildCommandArgs: { configFilePath } });
512
- }
513
- if (policy.api === "aot") {
514
- const outputPath = path.join(apiRootPath, HTTP_VALIDATOR_REGISTRY_BUILD_PATH);
515
- if (!(await exists(outputPath))) {
516
- throw new Error(`Built HTTP validator registry not found: ${outputPath}`);
517
- }
518
- assertApiZodCompilerBuildOutput([
519
- {
520
- code: await readFile(outputPath, "utf8"),
521
- path: outputPath,
522
- },
523
- ]);
524
- }
525
- printBuildSummary("API", true, Date.now() - apiStartedAt);
526
- } catch (e) {
527
- printBuildSummary("API", false, Date.now() - apiStartedAt);
528
- throw e;
529
- } finally {
530
- if (temporaryConfigPath !== undefined) {
531
- await rm(temporaryConfigPath, { force: true });
532
- }
533
- }
534
- }
535
-
536
- /**
537
- * pnpm build web 하면 실행되는 함수입니다.
538
- * Web 프로젝트만 빌드합니다.
539
- *
540
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
541
- */
542
- async function build_web({ skipIfMissing = false } = {}) {
543
- const appRoot = findAppRootPath();
544
- const webPath = path.join(appRoot, "web");
545
-
546
- if (!(await exists(webPath))) {
547
- if (skipIfMissing) {
548
- console.log(chalk.gray("Web 디렉토리가 없으므로 Web 빌드를 건너뜁니다."));
549
- return;
550
- }
551
- console.error(`web 디렉토리를 찾을 수 없습니다: ${webPath}`);
552
- process.exit(1);
553
- }
554
-
555
- const webStartedAt = Date.now();
556
- try {
557
- for (const artifact of WEB_ARTIFACTS) {
558
- const cwd = path.join(appRoot, artifact.projectPath);
559
- printTaskHeader(artifact.name, artifact.description, cwd);
560
-
561
- await runBuildSteps(artifact, { cwd, buildCommandArgs: {} });
562
- }
563
- printBuildSummary("Web", true, Date.now() - webStartedAt);
564
- } catch (e) {
565
- printBuildSummary("Web", false, Date.now() - webStartedAt);
566
- console.error(e);
567
- process.exit(1);
568
- }
569
- }
570
-
571
- /**
572
- * pre-build, build, post-build 단계를 순차적으로 실행합니다.
573
- */
574
- async function runBuildSteps<T>(
575
- artifact: BuildArtifact<T>,
576
- options: { cwd: string; buildCommandArgs: T },
577
- ) {
578
- const steps = [
579
- { name: "pre-build", cmd: artifact.preBuildCommand?.() },
580
- { name: "build", cmd: artifact.buildCommand(options.buildCommandArgs) },
581
- { name: "post-build", cmd: artifact.postBuildCommand?.() },
582
- ].filter((step) => step.cmd);
583
-
584
- for (let i = 0; i < steps.length; i++) {
585
- const step = steps[i];
586
- const isLast = i === steps.length - 1;
587
-
588
- try {
589
- assert(step.cmd);
590
- printTaskStart(step.name, step.cmd, isLast);
591
- await execWithLinePrefix(step.cmd, { cwd: options.cwd });
592
- printTaskSuccess(step.name, isLast);
593
- } catch (e) {
594
- printTaskFailed(step.name, isLast);
595
- throw new Error(`${step.name} failed`, { cause: e });
596
- }
597
- }
598
- }
599
-
600
- /**
601
- * pnpm start 하면 실행되는 함수입니다.
602
- * 빌드된 프로젝트를 실행합니다.
603
- *
604
- * 빌드된 결과물(dist 디렉토리의 index.js 엔트리포인트)이 없다면 실행을 중단합니다.
605
- * 소스맵 지원과 dotenv 지원을 포함하여 실행합니다.
606
- *
607
- * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
608
- */
609
- async function start() {
610
- const require = createRequire(import.meta.url);
611
- const { version } = require("../../package.json");
612
- console.log(`🌲 Sonamu v${version}\n`);
613
-
614
- const apiRoot = findApiRootPath();
615
- const entryPoint = "dist/index.js";
616
-
617
- if (!(await exists(entryPoint))) {
618
- console.log(chalk.red(`${entryPoint} not found. Please build your project first.`));
619
- console.log(chalk.blue("Run: pnpm sonamu build"));
620
- return;
621
- }
622
-
623
- const serverProcess = spawn(
624
- process.execPath,
625
- ["--enable-source-maps", "-r", "dotenv/config", entryPoint],
626
- {
627
- cwd: apiRoot,
628
- stdio: "inherit",
629
- },
630
- );
631
-
632
- process.on("SIGINT", () => {
633
- serverProcess.kill("SIGTERM");
634
- process.exit(0);
635
- });
636
- }
637
-
638
- async function setupMigrator() {
639
- // migrator
640
- migrator = new Migrator();
641
- }
642
-
643
- async function setupFixtureManager() {
644
- FixtureManager.init();
645
- }
646
-
647
- async function migrate_apply(targets: (keyof SonamuDBConfig)[]) {
648
- await setupMigrator();
649
- await migrator.apply(targets);
650
- }
651
-
652
- async function migrate_run() {
653
- await setupMigrator();
654
- await migrator.apply(getMigrateRunTargets());
655
- }
656
-
657
- async function migrate_generate() {
658
- await setupMigrator();
659
-
660
- const { conns } = await migrator.getStatus();
661
- const hasStatus0 = conns.some((conn) => conn.status === 0);
662
- if (!hasStatus0) {
663
- console.log(
664
- chalk.red(
665
- "마이그레이션 파일을 생성하려면 기존 마이그레이션이 최소 하나의 DB에 모두 적용되어 있어야 합니다.",
666
- ),
667
- );
668
- for (const conn of conns) {
669
- if (conn.pending.length > 0) {
670
- console.log(chalk.yellow(` ${conn.name}: pending ${conn.pending.length}개`));
671
- }
672
- }
673
- process.exit(1);
674
- }
675
-
676
- const count = await migrator.generatePreparedCodes();
677
- if (count > 0) {
678
- console.log(chalk.green(`${count}개의 마이그레이션 파일이 생성되었습니다.`));
679
- }
680
- }
681
-
682
- async function migrate_status() {
683
- await setupMigrator();
684
-
685
- const status = await migrator.getStatus();
686
- // status;
687
- console.log(status);
688
- }
689
-
690
- async function fixture_init() {
691
- const srcConfig = Sonamu.dbConfig[getSonamuEnvironment()];
692
- const targets = /* SAFETY: CLI 파서와 빌드 도구 입력 계약이 이 값의 타입을 보장한다. */ [
693
- {
694
- label: "(REMOTE) Fixture DB",
695
- config: Sonamu.dbConfig.fixture,
696
- },
697
- {
698
- label: "(LOCAL) Testing DB",
699
- config: Sonamu.dbConfig.test,
700
- toSkip: (() => {
701
- const remoteConn =
702
- /* SAFETY: CLI 파서와 빌드 도구 입력 계약이 이 값의 타입을 보장한다. */ Sonamu.dbConfig
703
- .fixture.connection as Knex.ConnectionConfig;
704
- const localConn =
705
- /* SAFETY: CLI 파서와 빌드 도구 입력 계약이 이 값의 타입을 보장한다. */ Sonamu.dbConfig
706
- .test.connection as Knex.ConnectionConfig;
707
- return remoteConn.host === localConn.host && remoteConn.database === localConn.database;
708
- })(),
709
- },
710
- ] as {
711
- label: string;
712
- config: Knex.Config;
713
- toSkip?: boolean;
714
- }[];
715
-
716
- // 1. 기준DB 스키마를 덤프
717
- console.log("DUMP...");
718
- const dumpFilename = `/tmp/sonamu-fixture-init-${Date.now()}.sql`;
719
- const srcConn =
720
- /* SAFETY: CLI 파서와 빌드 도구 입력 계약이 이 값의 타입을 보장한다. */ srcConfig.connection as Knex.ConnectionConfig;
721
- const migrationsDump = `/tmp/sonamu-fixture-init-migrations-${Date.now()}.sql`;
722
- await writeMysqlDump(
723
- [
724
- ...mysqlConnectionArgs(srcConn),
725
- "--single-transaction",
726
- "-d",
727
- "--no-create-db",
728
- "--triggers",
729
- String(srcConn.database),
730
- ],
731
- dumpFilename,
732
- );
733
- const sourceDb = knex(srcConfig);
734
- const [[migrations]] = await sourceDb.raw(
735
- "SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = ? AND table_name = 'knex_migrations'",
736
- [srcConn.database],
737
- );
738
- if (migrations.count > 0) {
739
- await writeMysqlDump(
740
- [
741
- ...mysqlConnectionArgs(srcConn),
742
- "--single-transaction",
743
- "--no-create-db",
744
- "--triggers",
745
- String(srcConn.database),
746
- "knex_migrations",
747
- "knex_migrations_lock",
748
- ],
749
- migrationsDump,
750
- );
751
- }
752
-
753
- // 2. 대상DB 각각에 대하여 존재여부 확인 후 붓기
754
- for (const { label, config, toSkip } of targets) {
755
- const conn =
756
- /* SAFETY: CLI 파서와 빌드 도구 입력 계약이 이 값의 타입을 보장한다. */ config.connection as Knex.ConnectionConfig;
757
-
758
- if (toSkip === true) {
759
- console.log(chalk.red(`${label}: Skipped!`));
760
- continue;
761
- }
762
-
763
- // SAFETY: Sonamu DB 구성은 Knex 연결 구성으로 검증된 뒤 이 명령에 전달된다.
764
- const databaseConnection = (config.connection ?? {}) as Knex.ConnectionConfig;
765
- const db = knex({
766
- ...config,
767
- connection: {
768
- ...databaseConnection,
769
- database: undefined,
770
- },
771
- });
772
- const [[row]] = await db.raw(`SHOW DATABASES LIKE "${conn.database}"`);
773
- if (row) {
774
- console.log(chalk.yellow(`${label}: Database "${conn.database}" Already exists`));
775
- await db.destroy();
776
- continue;
777
- }
778
-
779
- console.log(`SYNC to ${label}...`);
780
- const connectionArgs = mysqlConnectionArgs(conn);
781
- const database = String(conn.database);
782
- const escapedDatabase = database.replaceAll("`", "``");
783
- runMysql([...connectionArgs, "-e", `DROP DATABASE IF EXISTS \`${escapedDatabase}\``]);
784
- runMysql([...connectionArgs, "-e", `CREATE DATABASE \`${escapedDatabase}\``]);
785
- await runMysqlWithInput([...connectionArgs, database], dumpFilename);
786
- if (await exists(migrationsDump)) {
787
- await runMysqlWithInput([...connectionArgs, database], migrationsDump);
788
- }
789
-
790
- await db.destroy();
791
- }
792
-
793
- await sourceDb.destroy();
794
- }
795
-
796
- async function fixture_import(entityId: string, recordIds: number[]) {
797
- await setupFixtureManager();
798
-
799
- await FixtureManager.importFixture(entityId, recordIds);
800
- await FixtureManager.sync();
801
- }
802
-
803
- async function fixture_sync() {
804
- await setupFixtureManager();
805
-
806
- await FixtureManager.sync();
807
- }
808
-
809
- /**
810
- * fixture gen 명령어
811
- * 옵션을 process.argv에서 파싱
812
- */
813
- async function fixture_gen() {
814
- const options = parseOptions(process.argv);
815
- await fixtureGenCommand(options);
816
- }
817
-
818
- /**
819
- * fixture fetch 명령어
820
- * 옵션을 process.argv에서 파싱
821
- */
822
- async function fixture_fetch() {
823
- const options = parseOptions(process.argv);
824
- await fixtureFetchCommand(options);
825
- }
826
-
827
- /**
828
- * fixture explore 명령어
829
- * 옵션을 process.argv에서 파싱
830
- */
831
- async function fixture_explore() {
832
- const options = parseOptions(process.argv);
833
- await fixtureExploreCommand(options);
834
- }
835
-
836
- /**
837
- * 간단한 옵션 파서
838
- */
839
- function parseOptions(
840
- argv: string[],
841
- ): Record<string, string | boolean | string[]> & { _: string[] } {
842
- const options: Record<string, string | boolean | string[]> & { _: string[] } = { _: [] };
843
-
844
- for (let i = 0; i < argv.length; i++) {
845
- const arg = argv[i];
846
-
847
- if (arg.startsWith("--")) {
848
- const key = arg.slice(2);
849
-
850
- if (key.includes("=")) {
851
- const [k, v] = key.split("=");
852
- options[k] = v;
853
- } else {
854
- const next = argv[i + 1];
855
- if (next && !next.startsWith("--")) {
856
- options[key] = next;
857
- i++;
858
- } else {
859
- options[key] = true;
860
- }
861
- }
862
- } else if (arg.startsWith("-")) {
863
- const key = arg.slice(1);
864
- const next = argv[i + 1];
865
-
866
- if (next && !next.startsWith("-")) {
867
- options[key] = next;
868
- i++;
869
- } else {
870
- options[key] = true;
871
- }
872
- } else {
873
- options._.push(arg);
874
- }
875
- }
876
-
877
- return options;
878
- }
879
-
880
- async function stub_practice(name: string) {
881
- const practiceDir = path.join(Sonamu.apiRootPath, "src", "practices");
882
- const fileNames = await readdir(practiceDir);
883
-
884
- const maxSeqNo = await (async () => {
885
- if (!(await exists(practiceDir))) {
886
- await mkdir(practiceDir, { recursive: true });
887
- }
888
-
889
- const filteredSeqs = fileNames
890
- .filter((fileName) => fileName.startsWith("p") && fileName.endsWith(".ts"))
891
- .map((fileName) => {
892
- const [, seqNo] = fileName.match(/^p([0-9]+)-/) ?? ["0", "0"];
893
- return parseInt(seqNo);
894
- })
895
- .toSorted((a, b) => b - a);
896
-
897
- if (filteredSeqs.length > 0) {
898
- return filteredSeqs[0];
899
- }
900
-
901
- return 0;
902
- })();
903
-
904
- const currentSeqNo = maxSeqNo + 1;
905
- const fileName = `p${currentSeqNo}-${name}.ts`;
906
- const dstPath = path.join(practiceDir, fileName);
907
-
908
- const code = [
909
- `import { Sonamu } from "sonamu";`,
910
- "",
911
- `console.clear();`,
912
- `console.log("${fileName}");`,
913
- "",
914
- `Sonamu.runScript(async () => {`,
915
- ` // TODO`,
916
- `});`,
917
- "",
918
- ].join("\n");
919
- await writeFile(dstPath, code);
920
-
921
- openInCode(dstPath);
922
-
923
- const runCode = `yarn node -r dotenv/config --enable-source-maps dist/practices/${fileName.replace(
924
- ".ts",
925
- ".js",
926
- )}`;
927
- console.log(`${chalk.blue(runCode)} copied to clipboard.`);
928
- copyToClipboard(runCode);
929
- }
930
-
931
- async function stub_entity(entityId: string) {
932
- await Sonamu.syncer.createEntity({ entityId, title: entityId });
933
-
934
- const { flags } = parseCliOptions();
935
- const useAI = flags.has("ai");
936
- const noCones = flags.has("no-cones");
937
-
938
- // --no-cones: cone 생성 스킵
939
- if (noCones) {
940
- console.log(`✓ Entity '${entityId}' created without cones`);
941
- return;
942
- }
943
-
944
- const entity = EntityManager.get(entityId);
945
- if (!entity) {
946
- console.error(`Entity not found: ${entityId}`);
947
- return;
948
- }
949
-
950
- // --ai: LLM으로 cone 생성
951
- if (useAI) {
952
- console.log(`✓ Entity '${entityId}' created`);
953
- console.log(`🌟 Generating AI-powered cones...`);
954
- try {
955
- const configLocale = Sonamu.config.i18n?.defaultLocale;
956
- const locale =
957
- configLocale === "ko" || configLocale === "en" || configLocale === "ja"
958
- ? configLocale
959
- : "ko";
960
-
961
- const result = await entity.generateCones({
962
- preserveExisting: false,
963
- onlyEmpty: false,
964
- locale,
965
- });
966
-
967
- console.log(`✅ Done (${result.tokensUsed} tokens)`);
968
- } catch (error) {
969
- if (error instanceof Error && error.message.includes("ANTHROPIC_API_KEY")) {
970
- console.error(`\n❌ ${error.message}`);
971
- console.error(`\n💡 Remove --ai flag to use template cones instead`);
972
- } else {
973
- throw error;
974
- }
975
- }
976
- return;
977
- }
978
-
979
- // 기본: 템플릿 cone 자동 생성
980
- // LLM 없이 faker-mappings.ts를 활용하여 기본 cone 메타데이터를 생성합니다.
981
- // 이를 통해 ANTHROPIC_API_KEY가 없어도 Sonamu를 사용할 수 있으며,
982
- // 생성된 템플릿 cone은 나중에 'cone gen' 명령어로 AI를 통해 업그레이드할 수 있습니다.
983
- console.log(`🌟 Generating template cones...`);
984
- await entity.generateTemplateCones();
985
- console.log(`✓ Entity '${entityId}' created with template cones`);
986
- console.log(`💡 Tip: Run 'pnpm sonamu cone gen ${entityId}' to improve with AI`);
987
- }
988
-
989
- /**
990
- * AI를 사용하여 entity의 cone 메타데이터를 생성하거나 업그레이드합니다.
991
- *
992
- * 옵션:
993
- * - --regenerate: 전체 재생성 (기존 cone 덮어쓰기)
994
- * - --locale <ko|en|ja>: 생성 언어 지정
995
- * - 기본: onlyEmpty 모드 (기존 fixtureHint 보존)
996
- *
997
- * ANTHROPIC_API_KEY 필요 (sonamu.secret.ts 또는 환경변수)
998
- */
999
- async function cone_gen(entityId: string) {
1000
- const { flags, options } = parseCliOptions();
1001
-
1002
- // --all 옵션: 모든 entity cone 생성
1003
- if (flags.has("all") || entityId === "all") {
1004
- const allEntities = EntityManager.getAllEntities();
1005
- console.log(`🌟 Generating AI-powered cones for ${allEntities.length} entities...\n`);
1006
-
1007
- let totalTokens = 0;
1008
- const errors: string[] = [];
1009
-
1010
- for (const entity of allEntities) {
1011
- try {
1012
- console.log(`Processing ${entity.id}...`);
1013
- const configLocale = options.locale || Sonamu.config.i18n?.defaultLocale;
1014
- const locale =
1015
- configLocale === "ko" || configLocale === "en" || configLocale === "ja"
1016
- ? configLocale
1017
- : "ko";
1018
-
1019
- const result = await entity.generateCones({
1020
- preserveExisting: !flags.has("regenerate"),
1021
- onlyEmpty: !flags.has("regenerate"),
1022
- locale,
1023
- });
1024
-
1025
- totalTokens += result.tokensUsed;
1026
- console.log(` ✓ ${entity.id} (${result.tokensUsed} tokens)\n`);
1027
- } catch (error) {
1028
- const message = error instanceof Error ? error.message : "Unknown error";
1029
- errors.push(`${entity.id}: ${message}`);
1030
- console.error(` ✗ ${entity.id}: ${message}\n`);
1031
- }
1032
- }
1033
-
1034
- console.log(`\n✅ Done! Total: ${totalTokens} tokens used`);
1035
- const estimatedCost = (totalTokens * 9) / 1_000_000;
1036
- console.log(`💰 Estimated cost: ~$${estimatedCost.toFixed(4)}`);
1037
-
1038
- if (errors.length > 0) {
1039
- console.error(`\n❌ Failed entities (${errors.length}):`);
1040
- for (const err of errors) {
1041
- console.error(` - ${err}`);
1042
- }
1043
- }
1044
- return;
1045
- }
1046
-
1047
- // 단일 entity cone 생성
1048
- const entity = EntityManager.get(entityId);
1049
- if (!entity) {
1050
- console.error(`Entity not found: ${entityId}`);
1051
- return;
1052
- }
1053
-
1054
- const mode = flags.has("regenerate") ? "regenerating" : "generating";
1055
- console.log(
1056
- `🌟 ${mode === "regenerating" ? "Regenerating" : "Generating"} AI-powered cones for ${entityId}...`,
1057
- );
1058
-
1059
- try {
1060
- const configLocale = options.locale || Sonamu.config.i18n?.defaultLocale;
1061
- const locale =
1062
- configLocale === "ko" || configLocale === "en" || configLocale === "ja" ? configLocale : "ko";
1063
-
1064
- const result = await entity.generateCones({
1065
- preserveExisting: !flags.has("regenerate"),
1066
- onlyEmpty: !flags.has("regenerate"),
1067
- locale,
1068
- });
1069
-
1070
- console.log(`✅ Done! (${result.tokensUsed} tokens used)`);
1071
-
1072
- // 토큰 비용 계산 (대략적인 추정)
1073
- // Claude Sonnet 4.5: input $3/M tokens, output $15/M tokens
1074
- // 간단하게 평균 $9/M tokens로 계산
1075
- const estimatedCost = (result.tokensUsed * 9) / 1_000_000;
1076
- console.log(`💰 Estimated cost: ~$${estimatedCost.toFixed(4)}`);
1077
- } catch (error) {
1078
- if (error instanceof Error) {
1079
- if (error.message.includes("ANTHROPIC_API_KEY")) {
1080
- console.error(`\n❌ ${error.message}`);
1081
- console.error(`\n💡 To use AI-powered cone generation:`);
1082
- console.error(` 1. Get an API key from https://console.anthropic.com/`);
1083
- console.error(
1084
- ` 2. Add it to sonamu.secret.ts or set ANTHROPIC_API_KEY environment variable`,
1085
- );
1086
- } else if (error.message.includes("Rate limit")) {
1087
- console.error(`\n❌ ${error.message}`);
1088
- console.error(`\n💡 Please wait a moment and try again.`);
1089
- } else {
1090
- console.error(`\n❌ Failed to generate cones: ${error.message}`);
1091
- }
1092
- } else {
1093
- console.error(`\n❌ Failed to generate cones: Unknown error`);
1094
- }
1095
- }
1096
- }
1097
-
1098
- async function scaffold_model(entityId: string) {
1099
- await Sonamu.syncer.generateTemplate("model", {
1100
- entityId,
1101
- });
1102
- }
1103
-
1104
- async function scaffold_model_test(entityId: string) {
1105
- await Sonamu.syncer.generateTemplate("model_test", {
1106
- entityId,
1107
- });
1108
- }
1109
-
1110
- async function scaffold_view_list(entityId: string) {
1111
- await Sonamu.syncer.generateTemplate("view_list", {
1112
- entityId,
1113
- extra: undefined,
1114
- });
1115
- }
1116
-
1117
- async function scaffold_view_form(entityId: string) {
1118
- await Sonamu.syncer.generateTemplate("view_form", {
1119
- entityId,
1120
- });
1121
- }
1122
-
1123
- async function auth_generate() {
1124
- // --plugins 옵션 파싱
1125
- const pluginsArg = process.argv.find((arg) => arg.startsWith("--plugins"));
1126
- const plugins: BetterAuthPluginId[] = [];
1127
-
1128
- if (pluginsArg) {
1129
- const pluginValue = pluginsArg.includes("=")
1130
- ? pluginsArg.split("=")[1]
1131
- : process.argv[process.argv.indexOf(pluginsArg) + 1];
1132
-
1133
- if (pluginValue) {
1134
- const pluginIds = pluginValue.split(",").map((p) => p.trim());
1135
-
1136
- for (const id of pluginIds) {
1137
- if (isValidPluginId(id)) {
1138
- plugins.push(id);
1139
- } else {
1140
- console.log(chalk.yellow(`⚠ Unknown plugin: ${id}`));
1141
- console.log(chalk.dim(` Supported plugins: ${SUPPORTED_PLUGIN_IDS.join(", ")}`));
1142
- }
1143
- }
1144
- }
1145
- }
1146
-
1147
- console.log(chalk.yellow.bold("🔐 Generating better-auth entities...\n"));
1148
-
1149
- if (plugins.length > 0) {
1150
- console.log(chalk.dim(` Plugins: ${plugins.join(", ")}`));
1151
- }
1152
-
1153
- await generateBetterAuthEntities({ plugins });
1154
- }
1155
-
1156
- /**
1157
- * pnpm sonamu auth add-companions 하면 실행되는 함수입니다.
1158
- * 기존 프로젝트의 entity.json에 fixtureCompanions를 소급 추가합니다.
1159
- *
1160
- * 이미 fixtureCompanions가 있는 entity는 스킵합니다 (덮어쓰기 없음).
1161
- */
1162
- async function auth_add_companions() {
1163
- console.log(chalk.yellow.bold("🔐 Adding fixtureCompanions to better-auth entities...\n"));
1164
- await addCompanionsToEntities();
1165
- console.log(chalk.bold("\n✅ Done!"));
1166
- }