sonamu 0.8.19 → 0.8.21

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 (51) hide show
  1. package/dist/api/config.d.ts +1 -1
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/decorators.js +2 -2
  5. package/dist/api/sonamu.d.ts +1 -1
  6. package/dist/api/sonamu.d.ts.map +1 -1
  7. package/dist/api/sonamu.js +19 -14
  8. package/dist/bin/cli.js +30 -1
  9. package/dist/database/base-model.js +2 -2
  10. package/dist/database/db.d.ts +1 -1
  11. package/dist/database/db.d.ts.map +1 -1
  12. package/dist/database/db.js +1 -1
  13. package/dist/stream/sse.d.ts +2 -0
  14. package/dist/stream/sse.d.ts.map +1 -1
  15. package/dist/stream/sse.js +29 -6
  16. package/dist/syncer/syncer.js +2 -2
  17. package/dist/testing/global-setup.d.ts.map +1 -1
  18. package/dist/testing/global-setup.js +4 -1
  19. package/dist/ui/api.d.ts.map +1 -1
  20. package/dist/ui/api.js +15 -12
  21. package/dist/ui/cdd-service.d.ts +4 -2
  22. package/dist/ui/cdd-service.d.ts.map +1 -1
  23. package/dist/ui/cdd-service.js +99 -1
  24. package/dist/ui/cdd-types.d.ts +25 -0
  25. package/dist/ui/cdd-types.d.ts.map +1 -1
  26. package/dist/ui/cdd-types.js +1 -1
  27. package/dist/ui-web/assets/index-B7gc0Ygb.css +1 -0
  28. package/dist/ui-web/assets/{index-D_19-Pi4.js → index-DP968oXY.js} +71 -71
  29. package/dist/ui-web/index.html +2 -2
  30. package/package.json +3 -3
  31. package/src/api/config.ts +1 -1
  32. package/src/api/decorators.ts +1 -1
  33. package/src/api/sonamu.ts +20 -15
  34. package/src/bin/cli.ts +34 -0
  35. package/src/database/base-model.ts +1 -1
  36. package/src/database/db.ts +1 -1
  37. package/src/shared/app.shared.ts.txt +10 -1
  38. package/src/shared/web.shared.ts.txt +1 -1
  39. package/src/skills/AGENTS.md +10 -0
  40. package/src/skills/sonamu/auth.md +40 -1
  41. package/src/skills/sonamu/cdd.md +42 -0
  42. package/src/skills/sonamu/model.md +1 -1
  43. package/src/skills/sonamu/scaffolding.md +6 -0
  44. package/src/skills/sonamu/workflow.md +1 -1
  45. package/src/stream/sse.ts +34 -5
  46. package/src/syncer/syncer.ts +1 -1
  47. package/src/testing/global-setup.ts +4 -0
  48. package/src/ui/api.ts +20 -6
  49. package/src/ui/cdd-service.ts +106 -0
  50. package/src/ui/cdd-types.ts +28 -0
  51. package/dist/ui-web/assets/index-D4XFBV-f.css +0 -1
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>{{projectName}}: Sonamu UI</title>
8
- <script type="module" crossorigin src="/sonamu-ui/assets/index-D_19-Pi4.js"></script>
9
- <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-D4XFBV-f.css">
8
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-DP968oXY.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-B7gc0Ygb.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -124,9 +124,9 @@
124
124
  "vite": "7.3.0",
125
125
  "vitest": "^4.0.10",
126
126
  "@sonamu-kit/hmr-hook": "^0.4.1",
127
- "@sonamu-kit/hmr-runner": "^0.1.1",
128
127
  "@sonamu-kit/tasks": "^0.2.0",
129
- "@sonamu-kit/ts-loader": "^2.1.3"
128
+ "@sonamu-kit/ts-loader": "^2.1.3",
129
+ "@sonamu-kit/hmr-runner": "^0.1.1"
130
130
  },
131
131
  "devDependencies": {
132
132
  "@biomejs/biome": "^2.3.13",
package/src/api/config.ts CHANGED
@@ -83,7 +83,7 @@ export type SonamuConfig<TSinkId extends string = string, TFilterId extends stri
83
83
  targets: string[]; // "web", "app" 등
84
84
  };
85
85
 
86
- database: {
86
+ database?: {
87
87
  // 데이터베이스(pg는 pg 모듈, pgnative는 pg-native 모듈의 설치가 필요합니다.)
88
88
  database?: "pg" | "pgnative";
89
89
  // 기본 데이터베이스 이름
@@ -111,7 +111,7 @@ function checkSingleDecorator(target: DecoratorTarget, propertyKey: string, deco
111
111
  const method = target[propertyKey as keyof typeof target] as { __decoratorType?: symbol };
112
112
  if (method?.__decoratorType && method?.__decoratorType !== decoratorType) {
113
113
  throw new Error(
114
- `@${String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @stream decorator on the same method.`,
114
+ `@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @stream decorator on the same method.`,
115
115
  );
116
116
  } else {
117
117
  method.__decoratorType = decoratorType;
package/src/api/sonamu.ts CHANGED
@@ -144,11 +144,7 @@ class SonamuClass {
144
144
  }
145
145
 
146
146
  private _workflows: WorkflowManager | null = null;
147
- get workflows(): WorkflowManager {
148
- if (this._workflows === null) {
149
- throw new Error("Sonamu has not been initialized");
150
- }
151
-
147
+ get workflows(): WorkflowManager | null {
152
148
  return this._workflows;
153
149
  }
154
150
 
@@ -204,8 +200,10 @@ class SonamuClass {
204
200
  const { loadConfig } = await import("./config");
205
201
  this.config = await loadConfig(this.apiRootPath);
206
202
  // sonamu.config.ts 기본값 설정
207
- this.config.database.database = this.config.database.database ?? "pg";
208
- this.config.database.defaultOptions.client = this.config.database.database ?? "pg";
203
+ if (this.config.database) {
204
+ this.config.database.database = this.config.database.database ?? "pg";
205
+ this.config.database.defaultOptions.client = this.config.database.database ?? "pg";
206
+ }
209
207
 
210
208
  // 로깅 설정
211
209
  const { configureLogTape } = await import("../logger/configure");
@@ -216,11 +214,13 @@ class SonamuClass {
216
214
  }
217
215
 
218
216
  // DB 로드
219
- const { DB } = await import("../database/db");
220
- this.dbConfig = DB.generateDBConfig(this.config.database);
221
- if (!doSilent) {
222
- const chalk = (await import("chalk")).default;
223
- console.log(chalk.green("DB Config Loaded!"));
217
+ if (this.config.database) {
218
+ const { DB } = await import("../database/db");
219
+ this.dbConfig = DB.generateDBConfig(this.config.database);
220
+ if (!doSilent) {
221
+ const chalk = (await import("chalk")).default;
222
+ console.log(chalk.green("DB Config Loaded!"));
223
+ }
224
224
  }
225
225
 
226
226
  // Entity 로드
@@ -1306,6 +1306,11 @@ class SonamuClass {
1306
1306
  }
1307
1307
 
1308
1308
  private async initializeWorkflows(options: SonamuTaskOptions | undefined) {
1309
+ // database 설정이 없으면 WorkflowManager를 초기화하지 않음
1310
+ if (!this.config.database) {
1311
+ return;
1312
+ }
1313
+
1309
1314
  const { WorkflowManager } = await import("../tasks/workflow-manager");
1310
1315
  // NOTE: @sonamu-kit/tasks 안에선 knex config를 수정하기 때문에 connection이 아닌 config 째로 보냅니다.
1311
1316
  this._workflows = new WorkflowManager(DB.getDBConfig("w"));
@@ -1321,7 +1326,7 @@ class SonamuClass {
1321
1326
  };
1322
1327
 
1323
1328
  if (enableWorker) {
1324
- this.workflows.setupWorker({
1329
+ this._workflows?.setupWorker({
1325
1330
  ...defaultWorkerOptions,
1326
1331
  ...options.workerOptions,
1327
1332
  });
@@ -1334,7 +1339,7 @@ class SonamuClass {
1334
1339
 
1335
1340
  server.addHook("onClose", async () => {
1336
1341
  await options.lifecycle?.onShutdown?.(server);
1337
- await this.workflows.destroy();
1342
+ await this.workflows?.destroy();
1338
1343
  await this.destroy();
1339
1344
  });
1340
1345
 
@@ -1358,7 +1363,7 @@ class SonamuClass {
1358
1363
  server
1359
1364
  .listen({ port, host })
1360
1365
  .then(async () => {
1361
- await this.workflows.startWorker();
1366
+ await this.workflows?.startWorker();
1362
1367
  await options.lifecycle?.onStart?.(server);
1363
1368
  })
1364
1369
  .catch(async (err) => {
package/src/bin/cli.ts CHANGED
@@ -1059,6 +1059,40 @@ async function skills_sync_to(
1059
1059
  }
1060
1060
  }
1061
1061
 
1062
+ // settings.local.json — project-local 모드에서만, 없을 때만 생성
1063
+ if (options.copyProjectTemplates) {
1064
+ const settingsLocalPath = path.join(claudeDir, "settings.local.json");
1065
+ if (!(await exists(settingsLocalPath))) {
1066
+ try {
1067
+ const settingsContent = {
1068
+ hooks: {
1069
+ PostToolUse: [
1070
+ {
1071
+ matcher: "Edit|Write|MultiEdit",
1072
+ hooks: [
1073
+ {
1074
+ type: "command",
1075
+ command: "pnpm biome check --changed 2>&1 | head -60",
1076
+ },
1077
+ ],
1078
+ },
1079
+ ],
1080
+ },
1081
+ };
1082
+ await writeFile(settingsLocalPath, `${JSON.stringify(settingsContent, null, 2)}\n`);
1083
+ console.log(chalk.green(`✓ .claude/settings.local.json created`));
1084
+ } catch (error) {
1085
+ console.error(
1086
+ chalk.red(
1087
+ `✗ Failed to create settings.local.json: ${error instanceof Error ? error.message : String(error)}`,
1088
+ ),
1089
+ );
1090
+ }
1091
+ } else {
1092
+ console.log(chalk.dim(`⏭ .claude/settings.local.json already exists (preserved)`));
1093
+ }
1094
+ }
1095
+
1062
1096
  // CLAUDE.md 복사/업데이트
1063
1097
  if (await exists(sourceClaudeMd)) {
1064
1098
  try {
@@ -415,7 +415,7 @@ export class BaseModelClass<
415
415
  const { default: SqlParser } = await import("node-sql-parser");
416
416
  const parser = new SqlParser.Parser();
417
417
  const parsedQuery = parser.astify(countPuri.toQuery(), {
418
- database: Sonamu.config.database.database,
418
+ database: Sonamu.config.database?.database,
419
419
  });
420
420
 
421
421
  const leftJoinTables = getJoinTables(parsedQuery, ["LEFT JOIN"]);
@@ -157,7 +157,7 @@ export class DBClass {
157
157
  this.workerDBs.clear();
158
158
  }
159
159
 
160
- public generateDBConfig(config: SonamuConfig["database"]): SonamuDBConfig {
160
+ public generateDBConfig(config: NonNullable<SonamuConfig["database"]>): SonamuDBConfig {
161
161
  const defaultKnexConfig: Partial<DatabaseConfig> = assign(
162
162
  {
163
163
  client: "postgresql",
@@ -65,8 +65,17 @@ axios.defaults.transformResponse = [
65
65
  },
66
66
  ];
67
67
 
68
+ // Axios + React Native FormData 호환성: Content-Type을 multipart/form-data로 명시 설정하고
69
+ // transformRequest를 우회하여 Android에서 application/x-www-form-urlencoded로 잘못 설정되는 문제 방지
70
+ // ref: https://github.com/axios/axios/issues/4800
68
71
  axios.interceptors.request.use((config) => {
69
72
  config.headers["Accept-Language"] = getCurrentLocale();
73
+ if (config.data instanceof FormData) {
74
+ if (config.headers instanceof axios.AxiosHeaders) {
75
+ config.headers.setContentType("multipart/form-data");
76
+ }
77
+ config.transformRequest = [(data: unknown) => data];
78
+ }
70
79
  return config;
71
80
  });
72
81
 
@@ -77,7 +86,7 @@ export async function fetch(options: AxiosRequestConfig) {
77
86
  });
78
87
  return res.data;
79
88
  } catch (e: unknown) {
80
- if (axios.isAxiosError(e) && e.response && e.response.data) {
89
+ if (axios.isAxiosError(e) && e.response?.data) {
81
90
  const d = e.response.data as {
82
91
  message: string;
83
92
  issues: core.$ZodIssue[];
@@ -61,7 +61,7 @@ export async function fetch(options: AxiosRequestConfig) {
61
61
  });
62
62
  return res.data;
63
63
  } catch (e: unknown) {
64
- if (axios.isAxiosError(e) && e.response && e.response.data) {
64
+ if (axios.isAxiosError(e) && e.response?.data) {
65
65
  const d = e.response.data as {
66
66
  message: string;
67
67
  issues: core.$ZodIssue[];
@@ -46,6 +46,16 @@ See `.claude/skills/sonamu/SKILL.md` for the full skill list.
46
46
  - Resolve type errors through correct type annotations, generic constraints, type narrowing, or interface extension.
47
47
  - Do not use `as any` to work around "excessively deep" or similar TypeScript inference limits — find the correct access pattern instead (e.g. use `getPuri("r")` directly rather than casting the result).
48
48
  - Chaining methods after `as any` bypasses all TypeScript signature checks and leads directly to runtime bugs.
49
+ - Non-null assertion (`!`) is prohibited. Use optional chaining (`?.`) or type guard filters instead.
50
+
51
+ ## Code quality gate
52
+
53
+ After editing any `.ts` or `.tsx` file, always run both checks before considering the task done:
54
+
55
+ 1. `npx tsc --noEmit --skipLibCheck` — type errors
56
+ 2. `pnpm biome check <file>` — lint and format
57
+
58
+ Do not skip biome check even when tsc passes. Biome catches `noNonNullAssertion`, import order, and formatting issues that tsc does not.
49
59
 
50
60
  ## Skill read triggers
51
61
 
@@ -14,8 +14,44 @@ description: Sonamu better-auth 인증 시스템. 엔티티 자동 생성, Guard
14
14
  - 생성 로직: `modules/sonamu/src/auth/auth-generator.ts`
15
15
  - 엔티티 정의: `modules/sonamu/src/auth/better-auth-entities.ts`
16
16
 
17
+ **IMPORTANT: generate 실행 전에 반드시 사용자에게 플러그인 사용 여부를 확인해야 합니다.**
18
+
19
+ 플러그인 선택은 generate 시점에 함께 이루어지며, 나중에 추가도 가능하지만 처음부터 명시하는 것이 좋습니다.
20
+ 지원 플러그인 목록과 용도는 `auth-plugins.md`를 참고하세요.
21
+
22
+ ### 플러그인 확인 흐름
23
+
24
+ **[Step 1] generate 전 확인 (필수)**
25
+
26
+ > "어떤 인증 방식을 사용할 계획인가요? 기본 이메일/소셜 로그인 외에 추가 플러그인이 필요한지 확인해 주세요.
27
+ > 지원 플러그인: `admin`, `organization`, `2fa`, `username`, `phone-number`, `api-key`, `jwt`, `passkey`, `sso`, `anonymous`"
28
+
29
+ **[Step 1-A] 사용자가 "나중에 하겠다"고 응답한 경우:**
30
+
31
+ 아래 안내를 제공한 후 플러그인 없이 generate를 진행합니다:
32
+
33
+ > "알겠습니다. 플러그인은 초기 마이그레이션 실행 전까지 추가하는 것이 가장 좋습니다.
34
+ > 마이그레이션 전에 다시 확인드리겠습니다."
35
+
36
+ 그리고 **`plugins_deferred: true`** 상태를 기억합니다.
37
+
38
+ **[Step 2] migrate run 직전 재확인 (CRITICAL — `plugins_deferred: true`인 경우 반드시 실행)**
39
+
40
+ 마이그레이션을 실행하기 전, 반드시 다시 확인합니다:
41
+
42
+ > "마이그레이션 실행 전입니다. 지금이 플러그인을 추가하기 가장 좋은 시점입니다.
43
+ > 추가할 플러그인이 있으면 알려주세요. 없으면 그대로 진행합니다.
44
+ > 지원 플러그인: `admin`, `organization`, `2fa`, `username`, `phone-number`, `api-key`, `jwt`, `passkey`, `sso`, `anonymous`"
45
+
46
+ - 플러그인 추가 시: `pnpm sonamu auth generate --plugins <목록>` 실행 후 migrate 진행
47
+ - 없으면: migrate 그대로 진행
48
+
17
49
  ```bash
50
+ # 플러그인 없이 기본 엔티티만
18
51
  pnpm sonamu auth generate
52
+
53
+ # 플러그인 포함
54
+ pnpm sonamu auth generate --plugins admin,2fa,username
19
55
  ```
20
56
 
21
57
  생성되는 4개 엔티티 (`betterAuthV1` 배열):
@@ -242,7 +278,10 @@ Enum 추가:
242
278
  ## 체크리스트
243
279
 
244
280
  설정 후 확인 사항:
245
- - [ ] `pnpm sonamu auth generate` 실행
281
+ - [ ] **[generate 전] 사용자에게 플러그인 필요 여부 확인**
282
+ - "나중에" 응답 시 → `plugins_deferred: true` 기억, 최적 시점 안내
283
+ - [ ] `pnpm sonamu auth generate [--plugins ...]` 실행
284
+ - [ ] **[migrate 전] `plugins_deferred: true`인 경우 플러그인 재확인** (CRITICAL)
246
285
  - [ ] 마이그레이션 생성 및 적용
247
286
  - [ ] `sonamu.config.ts`에 `server.auth` 설정
248
287
  - [ ] `guardHandler` 구현
@@ -26,6 +26,8 @@ contract/
26
26
  |- schemas/
27
27
  | |- default-contract.schema.json # contract schema definition
28
28
  | \- default-spec.schema.json # spec schema definition
29
+ |- rules/
30
+ | \- *.rules.json # reusable implementation conventions
29
31
  |- main.contract.json # project root contract
30
32
  |- {domain}/
31
33
  | |- main.contract.json # domain representative contract
@@ -295,6 +297,43 @@ git log --follow -- contract/auth/login.spec.json # track renames
295
297
 
296
298
  ---
297
299
 
300
+ ## Rule Files
301
+
302
+ `contract/rules/*.rules.json` 파일로 프로젝트별 반복 개발 규칙을 관리한다.
303
+
304
+ ### 파일 포맷
305
+
306
+ ```json
307
+ {
308
+ "description": "Rule-set의 범위와 목적 설명",
309
+ "rules": [
310
+ {
311
+ "id": "readonly-money-display-uses-numf",
312
+ "when": "금액을 읽기 전용 텍스트나 테이블 셀로 표시할 때",
313
+ "instruction": "numF()를 적용합니다.",
314
+ "examples": ["numF(row.totalAmount)", "numF(summary.budget)"]
315
+ }
316
+ ]
317
+ }
318
+ ```
319
+
320
+ | 필드 | 설명 |
321
+ |---|---|
322
+ | `description` | rule set의 범위와 의도 |
323
+ | `rules[].id` | 안정적인 식별자 (프롬프트 참조, diff, 리뷰 노트에 사용) |
324
+ | `rules[].when` | 규칙의 적용 조건 |
325
+ | `rules[].instruction` | worker가 따라야 할 구체적 지침 |
326
+ | `rules[].examples` | 선택적 코드/사용 예시 |
327
+
328
+ ### 운영 규칙
329
+
330
+ - Orchestrator는 phase 라우팅 전에 `contract/rules/`를 확인하고, 현재 작업에 해당하는 `*.rules.json` 파일을 읽는다.
331
+ - Orchestrator는 worker를 스폰할 때 해당 파일 경로를 `rules_paths`로 전달한다.
332
+ - 모든 worker는 변경 작업 시작 전에 `rules_paths`의 각 파일을 읽고, 담당 범위에 맞는 규칙을 적용한다.
333
+ - 작업에 매칭되는 rule 파일이 없거나, 참조된 파일이 누락/형식 오류인 경우 blind하게 진행하지 않고 중단 후 보고한다.
334
+
335
+ ---
336
+
298
337
  ## Development Process
299
338
 
300
339
  All processes follow Waterfall. Each stage starts only after the previous stage is complete. If you need to change a previous stage artifact, go back, update the document first, then re-run downstream stages.
@@ -346,6 +385,7 @@ Impact analysis -> Contract/Spec review -> Spec update/fix -> Code update -> Tes
346
385
  **Step 1: Impact analysis**
347
386
  - Find all Spec files whose `sources` include the target files.
348
387
  - Check `contracts` and `dependsOnSpecs` in those Specs to identify chained impact scope.
388
+ - 코드/테스트 변경 시: 변경된 파일을 `sources`로 참조하는 모든 Spec을 찾고, 그 Spec의 `contracts`와 `dependsOnSpecs`까지 정합성을 확인한다.
349
389
 
350
390
  **Step 2: Contract/Spec review**
351
391
  - Read related Specs to understand current module structure and interfaces.
@@ -374,6 +414,7 @@ Impact analysis -> Contract/Spec review -> Spec update/fix -> Code update -> Tes
374
414
  - Validate that updated code follows confirmed Spec exactly.
375
415
  - Verify all `acceptanceCriteria` items are satisfied.
376
416
  - **If mismatch exists, fix code.**
417
+ - 관련 Spec(`dependsOnSpecs`에서 참조하는 Spec 포함), Contract 정합성도 함께 확인한다.
377
418
  - After all validations pass, set `status` to `"done"` and update `lastModified` to today.
378
419
 
379
420
  ### 3. Bug fixes
@@ -487,6 +528,7 @@ The `cdd` CLI tool automates CDD workflow tasks. Run via `pnpm cdd <command>`.
487
528
  | `cdd spec create <n>` | Create a Spec template. Requires `--domain <n>` or `--contract <path>` |
488
529
  | `cdd contract create [name]` | Contract 템플릿 생성. `name` 미지정 시 `main` |
489
530
  | `cdd spec set-status <spec> <status>` | Change Spec status |
531
+ | `cdd rules validate` | `contract/rules/*.rules.json` 포맷 검증 |
490
532
  | `cdd spec list` | List Specs. Filters: `--status`, `--domain`, `--contract` |
491
533
  | `cdd spec get <spec>` | Show full Spec or a specific field (`--field`) |
492
534
  | `cdd spec set <spec>` | Update a Spec field (`--field`, `--value`, `--json`) |
@@ -395,7 +395,7 @@ async enroll(courseId: number, userId: number): Promise<Enrollment> {
395
395
  const { total } = await this.findMany({ course_id: courseId });
396
396
 
397
397
  if (total >= course.max_students) {
398
- throw new Error("정원이 가듍 찼습니다");
398
+ throw new Error("정원이 가득 찼습니다");
399
399
  }
400
400
 
401
401
  // 3단계: 실행
@@ -243,6 +243,12 @@ export const {Entity}SaveParams = {Entity}BaseSchema.partial({
243
243
 
244
244
  ### exhaustive() 타입 에러
245
245
 
246
+ `exhaustive`는 sonamu에서 제공하는 유틸리티 함수입니다.
247
+
248
+ ```typescript
249
+ import { exhaustive } from "sonamu";
250
+ ```
251
+
246
252
  Scaffolding 템플릿은 `OrderBy` enum의 **첫 번째 값만** 자동 처리합니다.
247
253
 
248
254
  ```typescript
@@ -246,7 +246,7 @@ description: Sonamu 전체 개발 워크플로우. 프로젝트 생성부터 Fro
246
246
 
247
247
  42. 사용자에게 fixture 생성할지 확인
248
248
  43. 모든 엔티티의 prop에 `cone.note`가 존재하는지 체크
249
- - cone.note가 비어있는 prop이 있으면 사용자에게 보고하고 `pnpm sonamu cone generate --use-llm`으로 cone을 재생성할지 확인
249
+ - cone.note가 비어있는 prop이 있으면 사용자에게 보고하고 `pnpm sonamu cone gen --use-llm`으로 cone을 재생성할지 확인
250
250
  - cone.note가 있어야 LLM이 맥락에 맞는 fixture 데이터를 생성할 수 있다
251
251
  44. 생성할 데이터의 최소 row 수 확인 (최소 10 ~ 최대 100)
252
252
  45. **better-auth 엔티티 먼저 생성** (의존성 순서 필수):
package/src/stream/sse.ts CHANGED
@@ -12,28 +12,54 @@ export function createSSEFactory<T extends z.ZodObject>(
12
12
 
13
13
  export function createMockSSEFactory<T extends z.ZodObject>(_events: T): SSEConnection<T> {
14
14
  return {
15
+ get closed() {
16
+ return false;
17
+ },
18
+ onClose: (_callback) => {},
15
19
  publish: (_event, _data) => {},
16
20
  end: () => Promise.resolve(),
17
21
  };
18
22
  }
19
23
 
20
24
  export interface SSEConnection<T extends z.ZodObject> {
25
+ get closed(): boolean;
26
+ onClose(callback: () => void): void;
21
27
  publish<K extends keyof z.infer<T>>(event: K, data: z.infer<T>[K]): void;
22
28
  end(): Promise<void>;
23
29
  }
24
30
 
25
31
  class SSEConnectionImpl<T extends z.ZodObject> implements SSEConnection<T> {
26
32
  private _closed = false;
33
+ private _closeCallbacks: Array<() => void> = [];
34
+
35
+ private readonly markClosed = () => {
36
+ this._closed = true;
37
+ this.fireCloseCallbacks();
38
+ };
39
+
40
+ get closed(): boolean {
41
+ return this._closed;
42
+ }
43
+
44
+ onClose(callback: () => void): void {
45
+ this._closeCallbacks.push(callback);
46
+ }
47
+
48
+ // 콜백을 한 번만 실행하고 배열을 비워 중복 호출을 방지
49
+ private fireCloseCallbacks(): void {
50
+ const callbacks = this._closeCallbacks;
51
+ this._closeCallbacks = [];
52
+ for (const cb of callbacks) {
53
+ cb();
54
+ }
55
+ }
27
56
 
28
57
  constructor(
29
58
  private readonly socket: FastifyRequest["socket"],
30
59
  private readonly reply: FastifyReply,
31
60
  ) {
32
- const markClosed = () => {
33
- this._closed = true;
34
- };
35
- this.socket.on("close", markClosed);
36
- this.socket.on("error", markClosed);
61
+ this.socket.on("close", this.markClosed);
62
+ this.socket.on("error", this.markClosed);
37
63
  }
38
64
 
39
65
  publish<K extends keyof z.infer<T>>(event: K, data: z.infer<T>[K]): void {
@@ -53,6 +79,9 @@ class SSEConnectionImpl<T extends z.ZodObject> implements SSEConnection<T> {
53
79
  }
54
80
 
55
81
  this._closed = true;
82
+ this.socket.off("close", this.markClosed);
83
+ this.socket.off("error", this.markClosed);
84
+ this.fireCloseCallbacks();
56
85
 
57
86
  this.reply.sse({
58
87
  event: "end",
@@ -283,7 +283,7 @@ export class Syncer {
283
283
 
284
284
  async autoloadWorkflows() {
285
285
  this.workflows = await loadWorkflows();
286
- await Sonamu.workflows.synchronize(this.workflows);
286
+ await Sonamu.workflows?.synchronize(this.workflows);
287
287
  }
288
288
 
289
289
  async autoloadSSRRoutes(): Promise<void> {
@@ -31,6 +31,10 @@ export function createGlobalSetup() {
31
31
  };
32
32
  }
33
33
 
34
+ if (!config.database) {
35
+ throw new Error("database 설정이 필요합니다 (병렬 테스팅)");
36
+ }
37
+
34
38
  const maxWorkers = config.test.maxWorkers ?? 4;
35
39
  const templateDb = `${config.database.name}_test`;
36
40
 
package/src/ui/api.ts CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  editContent,
42
42
  editSchema,
43
43
  getCddTree,
44
+ getDashboard,
44
45
  listSchemas,
45
46
  openSourceFile,
46
47
  readContent,
@@ -1086,15 +1087,12 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1086
1087
 
1087
1088
  // Tasks API
1088
1089
  server.get("/api/tasks/status", async () => {
1089
- try {
1090
- Sonamu.workflows;
1091
- return { active: true };
1092
- } catch {
1093
- return { active: false };
1094
- }
1090
+ return { active: Sonamu.workflows !== null };
1095
1091
  });
1096
1092
 
1097
1093
  server.get("/api/tasks/workflowDefinitions", async () => {
1094
+ if (!Sonamu.workflows)
1095
+ throw new Error("Workflows not initialized (database not configured)");
1098
1096
  const definitions = Sonamu.workflows.workflowDefinitions;
1099
1097
  return { definitions };
1100
1098
  });
@@ -1111,6 +1109,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1111
1109
  createdBefore?: string;
1112
1110
  };
1113
1111
  }>("/api/tasks/workflowRuns", async (request) => {
1112
+ if (!Sonamu.workflows)
1113
+ throw new Error("Workflows not initialized (database not configured)");
1114
1114
  const backend = Sonamu.workflows.backend;
1115
1115
  const { limit, after, before, order, status, workflowName, createdAfter, createdBefore } =
1116
1116
  request.query;
@@ -1129,6 +1129,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1129
1129
  server.get<{
1130
1130
  Params: { id: string };
1131
1131
  }>("/api/tasks/workflowRuns/:id", async (request) => {
1132
+ if (!Sonamu.workflows)
1133
+ throw new Error("Workflows not initialized (database not configured)");
1132
1134
  const backend = Sonamu.workflows.backend;
1133
1135
  const workflowRun = await backend.getWorkflowRun({
1134
1136
  workflowRunId: request.params.id,
@@ -1142,6 +1144,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1142
1144
  server.post<{
1143
1145
  Params: { id: string };
1144
1146
  }>("/api/tasks/workflowRuns/:id/cancel", async (request) => {
1147
+ if (!Sonamu.workflows)
1148
+ throw new Error("Workflows not initialized (database not configured)");
1145
1149
  const backend = Sonamu.workflows.backend;
1146
1150
  return backend.cancelWorkflowRun({
1147
1151
  workflowRunId: request.params.id,
@@ -1151,6 +1155,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1151
1155
  server.post<{
1152
1156
  Params: { id: string };
1153
1157
  }>("/api/tasks/workflowRuns/:id/pause", async (request) => {
1158
+ if (!Sonamu.workflows)
1159
+ throw new Error("Workflows not initialized (database not configured)");
1154
1160
  const backend = Sonamu.workflows.backend;
1155
1161
  return backend.pauseWorkflowRun({
1156
1162
  workflowRunId: request.params.id,
@@ -1160,6 +1166,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1160
1166
  server.post<{
1161
1167
  Params: { id: string };
1162
1168
  }>("/api/tasks/workflowRuns/:id/resume", async (request) => {
1169
+ if (!Sonamu.workflows)
1170
+ throw new Error("Workflows not initialized (database not configured)");
1163
1171
  const backend = Sonamu.workflows.backend;
1164
1172
  return backend.resumeWorkflowRun({
1165
1173
  workflowRunId: request.params.id,
@@ -1174,6 +1182,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1174
1182
  before?: string;
1175
1183
  };
1176
1184
  }>("/api/tasks/workflowRuns/:id/steps", async (request) => {
1185
+ if (!Sonamu.workflows)
1186
+ throw new Error("Workflows not initialized (database not configured)");
1177
1187
  const backend = Sonamu.workflows.backend;
1178
1188
  const { limit, after, before } = request.query;
1179
1189
  return backend.listStepAttempts({
@@ -1403,6 +1413,10 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
1403
1413
  });
1404
1414
 
1405
1415
  // CDD API
1416
+ server.get("/api/cdd/dashboard", async () => {
1417
+ return getDashboard();
1418
+ });
1419
+
1406
1420
  server.get("/api/cdd/tree", async () => {
1407
1421
  return getCddTree();
1408
1422
  });