sonamu 0.9.0 → 0.9.2

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 (55) hide show
  1. package/dist/api/config.d.ts.map +1 -1
  2. package/dist/api/config.js +1 -4
  3. package/dist/api/sonamu.d.ts +5 -2
  4. package/dist/api/sonamu.d.ts.map +1 -1
  5. package/dist/api/sonamu.js +61 -22
  6. package/dist/auth/plugins/entity-definitions/api-key.d.ts.map +1 -1
  7. package/dist/auth/plugins/entity-definitions/api-key.js +16 -12
  8. package/dist/auth/plugins/wrappers/admin.d.ts +169 -73
  9. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  10. package/dist/auth/plugins/wrappers/api-key.d.ts +2 -3
  11. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -1
  12. package/dist/auth/plugins/wrappers/api-key.js +4 -3
  13. package/dist/auth/plugins/wrappers/phone-number.d.ts +19 -18
  14. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  15. package/dist/auth/plugins/wrappers/sso.d.ts +119 -38
  16. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
  17. package/dist/auth/plugins/wrappers/two-factor.d.ts +29 -19
  18. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  19. package/dist/auth/plugins/wrappers/username.d.ts +14 -13
  20. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  21. package/dist/bin/cli.js +7 -2
  22. package/dist/database/knex.d.ts.map +1 -1
  23. package/dist/database/knex.js +10 -18
  24. package/dist/database/puri.d.ts +6 -6
  25. package/dist/database/puri.d.ts.map +1 -1
  26. package/dist/database/puri.js +1 -1
  27. package/dist/database/puri.types.d.ts +2 -1
  28. package/dist/database/puri.types.d.ts.map +1 -1
  29. package/dist/database/puri.types.js +1 -1
  30. package/dist/database/upsert-builder.js +1 -1
  31. package/dist/testing/fixture-manager.js +3 -3
  32. package/dist/ui-web/assets/index-CfgbCoOJ.js +199 -0
  33. package/dist/ui-web/assets/index-Dr8pRJC_.css +1 -0
  34. package/dist/ui-web/index.html +2 -2
  35. package/package.json +14 -12
  36. package/src/api/config.ts +0 -4
  37. package/src/api/sonamu.ts +70 -23
  38. package/src/auth/plugins/entity-definitions/api-key.ts +5 -10
  39. package/src/auth/plugins/wrappers/api-key.ts +4 -4
  40. package/src/bin/cli.ts +7 -1
  41. package/src/database/knex.ts +9 -19
  42. package/src/database/puri.ts +6 -6
  43. package/src/database/puri.types.ts +2 -1
  44. package/src/database/upsert-builder.ts +1 -1
  45. package/src/skills/AGENTS.md +3 -3
  46. package/src/skills/project/README.md +1 -3
  47. package/src/skills/sonamu/auth-plugins.md +21 -13
  48. package/src/skills/sonamu/auth.md +17 -0
  49. package/src/skills/sonamu/cdd.md +18 -26
  50. package/src/skills/sonamu/create-sonamu.md +8 -0
  51. package/src/skills/sonamu/entity-basic.md +1 -1
  52. package/src/skills/sonamu/scaffolding.md +13 -0
  53. package/src/testing/fixture-manager.ts +2 -2
  54. package/dist/ui-web/assets/index-C-Zz-wYg.css +0 -1
  55. package/dist/ui-web/assets/index-DejDON8K.js +0 -238
@@ -43,19 +43,13 @@ export const apiKeyEntityDef: BetterAuthEntityDef = {
43
43
  { name: "enabled", type: "boolean", desc: "활성화 여부" },
44
44
  { name: "permissions", type: "string", nullable: true, desc: "권한" },
45
45
  { name: "metadata", type: "string", nullable: true, desc: "메타데이터 (JSON)" },
46
+ { name: "reference_id", type: "string", desc: "참조 대상 ID" },
47
+ { name: "config_id", type: "string", dbDefault: "'default'", desc: "설정 ID" },
46
48
  { name: "created_at", type: "date", dbDefault: "CURRENT_TIMESTAMP", desc: "생성일시" },
47
49
  { name: "updated_at", type: "date", nullable: true, desc: "수정일시" },
48
- {
49
- type: "relation",
50
- name: "user",
51
- with: "User",
52
- relationType: "BelongsToOne",
53
- onDelete: "CASCADE",
54
- desc: "사용자",
55
- },
56
50
  ],
57
51
  indexes: [
58
- { type: "index", name: "api_keys_user_id_idx", columns: [{ name: "user_id" }] },
52
+ { type: "index", name: "api_keys_reference_id_idx", columns: [{ name: "reference_id" }] },
59
53
  { type: "unique", name: "api_keys_key_unique", columns: [{ name: "key" }] },
60
54
  ],
61
55
  subsets: {
@@ -80,7 +74,8 @@ export const apiKeyEntityDef: BetterAuthEntityDef = {
80
74
  "metadata",
81
75
  "created_at",
82
76
  "updated_at",
83
- "user.id",
77
+ "reference_id",
78
+ "config_id",
84
79
  ],
85
80
  },
86
81
  enums: {
@@ -1,9 +1,8 @@
1
- import { apiKey as _apiKey } from "better-auth/plugins";
2
- import { type ApiKeyOptions } from "better-auth/plugins";
1
+ import { apiKey as _apiKey, type ApiKeyOptions } from "@better-auth/api-key";
3
2
 
4
3
  import { merge } from "../../../utils/utils";
5
4
 
6
- export type { ApiKeyOptions } from "better-auth/plugins";
5
+ export type { ApiKeyOptions } from "@better-auth/api-key";
7
6
 
8
7
  /**
9
8
  * API Key 플러그인 스키마
@@ -17,7 +16,8 @@ export const API_KEY_SCHEMA: ApiKeyOptions["schema"] = {
17
16
  apikey: {
18
17
  modelName: "api_keys",
19
18
  fields: {
20
- userId: "user_id",
19
+ referenceId: "reference_id",
20
+ configId: "config_id",
21
21
  lastRequest: "last_request",
22
22
  requestCount: "request_count",
23
23
  rateLimitEnabled: "rate_limit_enabled",
package/src/bin/cli.ts CHANGED
@@ -309,7 +309,9 @@ function spawnApiDevServer(options?: { extraEnv?: Record<string, string> }) {
309
309
  * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
310
310
  */
311
311
  function dev_all() {
312
- console.log(chalk.yellow.bold("Starting Sonamu dev server...\n"));
312
+ const require = createRequire(import.meta.url);
313
+ const { version } = require("../../package.json");
314
+ console.log(`🌲 Sonamu v${version}\n`);
313
315
  spawnApiDevServer();
314
316
  }
315
317
 
@@ -498,6 +500,10 @@ async function runBuildSteps<T>(
498
500
  * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
499
501
  */
500
502
  async function start() {
503
+ const require = createRequire(import.meta.url);
504
+ const { version } = require("../../package.json");
505
+ console.log(`🌲 Sonamu v${version}\n`);
506
+
501
507
  const apiRoot = findApiRootPath();
502
508
  const entryPoint = "dist/index.js";
503
509
 
@@ -12,12 +12,20 @@ export function createKnexInstance(config: Knex.Config): Knex {
12
12
  }
13
13
 
14
14
  config.pool = {
15
+ maxConnectionLifetimeMillis: 1800000,
16
+ maxConnectionLifetimeJitterMillis: 300000,
15
17
  ...config.pool,
16
18
  propagateCreateError: false,
17
19
  idleTimeoutMillis: 10000,
18
20
  reapIntervalMillis: 1000,
19
21
  acquireTimeoutMillis: 30000,
20
22
  createTimeoutMillis: 30000,
23
+ validate: (connection: unknown) => {
24
+ if (typeof connection !== "object" || connection === null) return false;
25
+ const conn = connection as Record<string, unknown>;
26
+ if (conn._ending === true || conn._closed === true) return false;
27
+ return true;
28
+ },
21
29
  afterCreate: ((
22
30
  conn: Knex.Client & Record<string, unknown>,
23
31
  done: (err: Error | null, conn: Knex.Client) => void,
@@ -30,27 +38,9 @@ export function createKnexInstance(config: Knex.Config): Knex {
30
38
  stream.stream.setKeepAlive(true, 10000);
31
39
  }
32
40
 
33
- conn.on("error", (err: Error) => {
34
- Object.defineProperty(conn, "__knex__disposed", {
35
- value: err,
36
- writable: true,
37
- configurable: true,
38
- enumerable: true,
39
- });
40
- });
41
-
42
41
  done(null, conn);
43
42
  }) satisfies Knex.PoolConfig["afterCreate"],
44
43
  };
45
44
 
46
- const knexInstance = knex(config);
47
- knexInstance.client.validateConnection = (connection: unknown) => {
48
- if (typeof connection !== "object" || connection === null) return false;
49
- if ("__knex__disposed" in connection) return false;
50
- const conn = connection as Record<string, unknown>;
51
- if (conn._ending === true || conn._closed === true) return false;
52
- return true;
53
- };
54
-
55
- return knexInstance;
45
+ return knex(config);
56
46
  }
@@ -225,23 +225,23 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
225
225
  }
226
226
 
227
227
  // Raw functions for SELECT
228
- static rawString(sql: string, params: unknown[] = []): SqlExpression<"string"> {
228
+ static rawString(sql: string, params: Knex.RawBinding[] = []): SqlExpression<"string"> {
229
229
  return { _type: "sql_expression", _return: "string", _sql: sql, _params: params };
230
230
  }
231
231
 
232
- static rawStringArray(sql: string, params: unknown[] = []): SqlExpression<"string[]"> {
232
+ static rawStringArray(sql: string, params: Knex.RawBinding[] = []): SqlExpression<"string[]"> {
233
233
  return { _type: "sql_expression", _return: "string[]", _sql: sql, _params: params };
234
234
  }
235
235
 
236
- static rawNumber(sql: string, params: unknown[] = []): SqlExpression<"number"> {
236
+ static rawNumber(sql: string, params: Knex.RawBinding[] = []): SqlExpression<"number"> {
237
237
  return { _type: "sql_expression", _return: "number", _sql: sql, _params: params };
238
238
  }
239
239
 
240
- static rawBoolean(sql: string, params: unknown[] = []): SqlExpression<"boolean"> {
240
+ static rawBoolean(sql: string, params: Knex.RawBinding[] = []): SqlExpression<"boolean"> {
241
241
  return { _type: "sql_expression", _return: "boolean", _sql: sql, _params: params };
242
242
  }
243
243
 
244
- static rawDate(sql: string, params: unknown[] = []): SqlExpression<"date"> {
244
+ static rawDate(sql: string, params: Knex.RawBinding[] = []): SqlExpression<"date"> {
245
245
  return { _type: "sql_expression", _return: "date", _sql: sql, _params: params };
246
246
  }
247
247
 
@@ -814,7 +814,7 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
814
814
  }
815
815
 
816
816
  // WHERE RAW
817
- whereRaw(sql: string, bindings?: readonly unknown[]): this {
817
+ whereRaw(sql: string, bindings?: readonly Knex.RawBinding[]): this {
818
818
  this.knexQuery.whereRaw(sql, bindings);
819
819
  return this;
820
820
  }
@@ -1,5 +1,6 @@
1
1
  /* oxlint-disable @typescript-eslint/no-explicit-any */ // Puri.types.ts는 다양한 타입을 사용하고 있습니다.
2
2
 
3
+ import { type Knex } from "knex";
3
4
  import { type QueryResult } from "pg";
4
5
 
5
6
  import { type DatabaseForeignKeys, type DatabaseSchemaExtend } from "../types/types";
@@ -308,7 +309,7 @@ export type SqlExpression<
308
309
  _type: "sql_expression"; // 또는 "computed_value"
309
310
  _return: T;
310
311
  _sql: string;
311
- _params: unknown[];
312
+ _params: Knex.RawBinding[];
312
313
  };
313
314
 
314
315
  // 결과 타입 가독성을 위한 타입 확장
@@ -458,7 +458,7 @@ export class UpsertBuilder {
458
458
 
459
459
  // 각 FK 컬럼에 대한 WHERE IN 조건 추가
460
460
  for (const { column, values } of fkConditions) {
461
- deleteQuery = deleteQuery.whereIn(column, values);
461
+ deleteQuery = deleteQuery.whereIn(column, values as Knex.Value[]);
462
462
  }
463
463
 
464
464
  // 방금 upsert한 ID는 제외
@@ -39,7 +39,7 @@ See `.claude/skills/sonamu/SKILL.md` for the full skill list.
39
39
 
40
40
  ## Cross-workspace gate
41
41
 
42
- - For changes in this scope, root `pnpm check` (Biome) must pass before handoff.
42
+ - For changes in this scope, root `pnpm check` (oxlint + oxfmt) must pass before handoff.
43
43
 
44
44
  ## TypeScript type safety policy
45
45
 
@@ -54,9 +54,9 @@ See `.claude/skills/sonamu/SKILL.md` for the full skill list.
54
54
  After editing any `.ts` or `.tsx` file, always run both checks before considering the task done:
55
55
 
56
56
  1. `npx tsc --noEmit --skipLibCheck` — type errors
57
- 2. `pnpm biome check <file>` — lint and format
57
+ 2. `pnpm check` — lint and format (oxlint + oxfmt)
58
58
 
59
- Do not skip biome check even when tsc passes. Biome catches `noNonNullAssertion`, import order, and formatting issues that tsc does not.
59
+ Do not skip lint/format check even when tsc passes. oxlint catches `noNonNullAssertion`, import order, and other issues that tsc does not.
60
60
 
61
61
  ## Skill read triggers
62
62
 
@@ -4,9 +4,7 @@
4
4
 
5
5
  ## 용도
6
6
 
7
- - **requirements.md**: 프로젝트 요구사항 명세
8
- - **business-logic.md**: 비즈니스 로직 상세 (선택)
9
- - **architecture.md**: 아키텍처 설계 문서 (선택)
7
+ - **architecture.md**: 아키텍처 설계 문서
10
8
 
11
9
  ## 사용 방법
12
10
 
@@ -18,18 +18,18 @@ Use the `auth generate --plugins` command to auto-generate plugin entities.
18
18
 
19
19
  ## Supported Plugins
20
20
 
21
- | Plugin ID | Wrapper function | Package | Purpose |
22
- | -------------- | ---------------- | ---------------------- | ----------------------------------------------------- |
23
- | `admin` | `admin()` | `better-auth/plugins` | Admin features, user ban/unban, session impersonation |
24
- | `organization` | `organization()` | `better-auth/plugins` | Organization, team, member, and invitation management |
25
- | `2fa` | `twoFactor()` | `better-auth/plugins` | TOTP-based two-factor authentication |
26
- | `username` | `username()` | `better-auth/plugins` | Username-based authentication |
27
- | `phone-number` | `phoneNumber()` | `better-auth/plugins` | Phone number authentication |
28
- | `api-key` | `apiKey()` | `better-auth/plugins` | API key issuance/management, rate limiting |
29
- | `jwt` | `jwt()` | `better-auth/plugins` | JWT tokens + JWKS key management |
30
- | `passkey` | `passkey()` | `@better-auth/passkey` | WebAuthn/Passkey authentication |
31
- | `sso` | `sso()` | `@better-auth/sso` | OIDC/SAML SSO integration |
32
- | `anonymous` | `anonymous()` | `better-auth/plugins` | Anonymous user support |
21
+ | Plugin ID | Wrapper function | Package | Purpose |
22
+ | -------------- | ---------------- | ----------------------- | ----------------------------------------------------- |
23
+ | `admin` | `admin()` | `better-auth/plugins` | Admin features, user ban/unban, session impersonation |
24
+ | `organization` | `organization()` | `better-auth/plugins` | Organization, team, member, and invitation management |
25
+ | `2fa` | `twoFactor()` | `better-auth/plugins` | TOTP-based two-factor authentication |
26
+ | `username` | `username()` | `better-auth/plugins` | Username-based authentication |
27
+ | `phone-number` | `phoneNumber()` | `better-auth/plugins` | Phone number authentication |
28
+ | `api-key` | `apiKey()` | `@better-auth/api-key` | API key issuance/management, rate limiting |
29
+ | `jwt` | `jwt()` | `better-auth/plugins` | JWT tokens + JWKS key management |
30
+ | `passkey` | `passkey()` | `@better-auth/passkey` | WebAuthn/Passkey authentication |
31
+ | `sso` | `sso()` | `@better-auth/sso` | OIDC/SAML SSO integration |
32
+ | `anonymous` | `anonymous()` | `better-auth/plugins` | Anonymous user support |
33
33
 
34
34
  ---
35
35
 
@@ -183,6 +183,11 @@ Schema mapping:
183
183
  ### api-key
184
184
 
185
185
  **Additional entities:** ApiKey (table: `api_keys`)
186
+ **Package:** `@better-auth/api-key` (must be installed separately)
187
+
188
+ ```bash
189
+ pnpm add @better-auth/api-key
190
+ ```
186
191
 
187
192
  ```typescript
188
193
  import { apiKey } from "sonamu/auth/plugins";
@@ -192,12 +197,15 @@ apiKey();
192
197
 
193
198
  Schema mapping:
194
199
 
195
- - `userId` → `user_id`, `lastRequest` → `last_request`, `requestCount` → `request_count`
200
+ - `referenceId` → `reference_id`, `configId` → `config_id`
201
+ - `lastRequest` → `last_request`, `requestCount` → `request_count`
196
202
  - `rateLimitEnabled` → `rate_limit_enabled`, `rateLimitTimeWindow` → `rate_limit_time_window`
197
203
  - `rateLimitMax` → `rate_limit_max`, `refillInterval` → `refill_interval`
198
204
  - `refillAmount` → `refill_amount`, `lastRefillAt` → `last_refill_at`
199
205
  - `expiresAt` → `expires_at`, `createdAt` → `created_at`, `updatedAt` → `updated_at`
200
206
 
207
+ Note: v1.5.0에서 `userId`가 `referenceId`로 변경됨. `referenceId`는 user 또는 organization을 참조하는 polymorphic ID.
208
+
201
209
  ### jwt
202
210
 
203
211
  **Additional entities:** Jwks (table: `jwks`)
@@ -71,6 +71,23 @@ The 4 entities generated (`betterAuthV1` array):
71
71
  - Fields with changed types are updated automatically
72
72
  - Uses snake_case column names (better-auth uses camelCase)
73
73
 
74
+ ## Adding Fixture Companions (`auth add-companions`)
75
+
76
+ After running `auth generate`, run this command once to add `fixtureCompanions` to the `id` prop of better-auth entities (User, etc.).
77
+
78
+ ```bash
79
+ pnpm sonamu auth add-companions
80
+ ```
81
+
82
+ **Purpose:** Enables automatic Account fixture creation when generating User fixtures. Without this, fixture gen creates User records without a corresponding credentials Account, breaking auth-dependent tests.
83
+
84
+ **What it does:**
85
+ - Reads `fixtureCompanions` from the `betterAuthV1` definitions
86
+ - Adds them to the existing entity.json `id` prop's cone
87
+ - Skips if `fixtureCompanions` already exists
88
+
89
+ **When to run:** Once, after `auth generate`, before running `fixture gen` for the first time. Re-running is safe (idempotent).
90
+
74
91
  ## Field Mapping (Applied Automatically)
75
92
 
76
93
  **Source code:** `modules/sonamu/src/auth/better-auth-entities.ts` (BASE_FIELD_MAPPINGS)
@@ -12,7 +12,6 @@ description: CDD artifact technical reference. contract.md format, rules.json fo
12
12
  Documents domain rules and decision rationale in a cohesive form. Located at `contract/**/*.contract.md`.
13
13
 
14
14
  **Include:**
15
-
16
15
  - Domain rules and constraints ("Refunds are only allowed within 7 days of payment")
17
16
  - Decision rationale ("Due to PG provider policy")
18
17
  - Cross-module domain workflows ("Order status: pending → confirmed → shipped → completed")
@@ -21,7 +20,6 @@ Documents domain rules and decision rationale in a cohesive form. Located at `co
21
20
  - Edge cases and intended handling
22
21
 
23
22
  **Exclude:**
24
-
25
23
  - Implementation details (file paths, function names, class structure)
26
24
  - API endpoints or data schemas
27
25
  - UI layouts or component structure
@@ -64,13 +62,13 @@ Records code conventions and UI/API rules.
64
62
  }
65
63
  ```
66
64
 
67
- | Field | Description |
68
- | --------------------- | -------------------------------------- |
69
- | `description` | Scope and intent of the rule-set |
70
- | `rules[].id` | Stable identifier |
71
- | `rules[].when` | Condition under which the rule applies |
72
- | `rules[].instruction` | Specific directive to follow |
73
- | `rules[].examples` | Optional code/usage examples |
65
+ | Field | Description |
66
+ |-------|-------------|
67
+ | `description` | Scope and intent of the rule-set |
68
+ | `rules[].id` | Stable identifier |
69
+ | `rules[].when` | Condition under which the rule applies |
70
+ | `rules[].instruction` | Specific directive to follow |
71
+ | `rules[].examples` | Optional code/usage examples |
74
72
 
75
73
  ### `*.known-issues.json`
76
74
 
@@ -96,21 +94,14 @@ Records known bugs, framework constraints, and temporary workarounds. Include in
96
94
  ACs are not managed as separate documents. The describe/test names in test files are the ACs themselves.
97
95
 
98
96
  **AC writing principles:**
99
-
100
97
  - **Small and specific**: one AC = one behavior/outcome
101
98
  - **Include input and expected result in the name**: `"Returns 409 when email is duplicate"` > `"Returns an error"`
102
99
 
103
100
  ```typescript
104
- describe("sign up", () => {
105
- test("returns 409 when email is duplicate", () => {
106
- /* TODO */
107
- });
108
- test("returns 400 when password is shorter than 8 characters", () => {
109
- /* TODO */
110
- });
111
- test("returns the created user_id on success", () => {
112
- /* TODO */
113
- });
101
+ describe('sign up', () => {
102
+ test('returns 409 when email is duplicate', () => { /* TODO */ });
103
+ test('returns 400 when password is shorter than 8 characters', () => { /* TODO */ });
104
+ test('returns the created user_id on success', () => { /* TODO */ });
114
105
  });
115
106
  ```
116
107
 
@@ -122,13 +113,14 @@ describe("sign up", () => {
122
113
  pnpm cdd <command>
123
114
  ```
124
115
 
125
- | Command | Description |
126
- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
116
+ | Command | Description |
117
+ |---------|-------------|
127
118
  | `cdd ac add <file> <test-name>` | Add an empty test skeleton to a test file. Use `--describe <group>` to specify a describe block |
128
- | `cdd ac list [file]` | Print the describe/test tree of a test file. Parses `test()`, `it()`, and `testAs()` patterns |
129
- | `cdd rules validate` | Validate the format of `contract/rules/*.rules.json` |
130
- | `cdd agents init [--force]` | Initialize CDD agent setup in the project (creates `.agents/`, `AGENTS.md`, and symlinks) |
131
- | `cdd agents sync [--dry-run]` | Update CDD agent prompts to the latest version |
119
+ | `cdd ac list [file]` | Print the describe/test tree of a test file. Parses `test()`, `it()`, and `testAs()` patterns |
120
+ | `cdd rules validate` | Validate the format of `contract/rules/*.rules.json` |
121
+ | `cdd agents init [--force]` | Initialize CDD agent setup + contract scaffold (creates `.agents/`, `AGENTS.md`, `contract/planning.md`, `contract/rules/web.rules.json`, `contract/rules/api.rules.json`) |
122
+ | `cdd agents sync [--dry-run]` | Update CDD agent prompts to the latest version |
123
+ | `cdd contract init [--force]` | Scaffold contract directory only (`planning.md`, `rules/web.rules.json`, `rules/api.rules.json`). Alias: `cdd agents contract init` |
132
124
 
133
125
  ### Common options
134
126
 
@@ -114,6 +114,14 @@ pnpm create sonamu my_project \
114
114
 
115
115
  > Will fail if sonamu is an npm version. See "Sonamu Link Setup" below.
116
116
 
117
+ To create a new **project-local** skill file (in `.claude/skills/local/`):
118
+
119
+ ```bash
120
+ pnpm sonamu skills create <name>
121
+ ```
122
+
123
+ This creates `.claude/skills/local/<name>.md` with a frontmatter skeleton. Use it to document project-specific conventions or troubleshooting notes that aren't appropriate for the shared Sonamu skills.
124
+
117
125
  3. Start the dev server
118
126
 
119
127
  ```bash
@@ -622,7 +622,7 @@ When setting a default value on an ENUM field, the value must be wrapped in **es
622
622
  { "name": "status", "type": "enum", "id": "ApprovalStatus", "dbDefault": "pending" }
623
623
  // Error: cannot use column reference in DEFAULT expression
624
624
 
625
- // Incorrect: single quotes — causes Biome format error
625
+ // Incorrect: single quotes — causes oxfmt format error
626
626
  { "name": "status", "type": "enum", "id": "ApprovalStatus", "dbDefault": "'pending'" }
627
627
  ```
628
628
 
@@ -19,6 +19,19 @@ description: Reference for using Sonamu UI Scaffolding. Common errors and soluti
19
19
 
20
20
  **DO NOT:** scaffold only model and model_test and skip the view-related items
21
21
 
22
+ ## CLI Scaffold Commands
23
+
24
+ All scaffold types can be generated via CLI (in addition to Sonamu UI):
25
+
26
+ ```bash
27
+ pnpm sonamu scaffold model <entityId>
28
+ pnpm sonamu scaffold model_test <entityId>
29
+ pnpm sonamu scaffold view_list <entityId>
30
+ pnpm sonamu scaffold view_form <entityId>
31
+ ```
32
+
33
+ `model_test` generates the test file skeleton (`{entity}.model.test.ts`) with the correct imports and `bootstrap(vi)` setup. Run it from `packages/api` after the entity's types.ts and migration are ready.
34
+
22
35
  ---
23
36
 
24
37
  ## Pre-Scaffolding Checklist
@@ -342,7 +342,7 @@ export class FixtureManagerClass {
342
342
  }
343
343
  }
344
344
 
345
- for await (const fixture of fixtures) {
345
+ for (const fixture of fixtures) {
346
346
  const entity = EntityManager.get(fixture.entityId);
347
347
 
348
348
  // 사용자 지정 컬럼 기준 중복 확인 → target
@@ -680,7 +680,7 @@ export class FixtureManagerClass {
680
680
  ]);
681
681
  !isTest() && console.log(chalk.green(`Reset sequence for ${tableName}: ${maxId}`));
682
682
  }
683
- } catch (_err) {
683
+ } catch {
684
684
  // 시퀀스가 없는 테이블(join table 등)은 무시
685
685
  !isTest() && console.log(chalk.gray(`Skipped sequence reset for ${tableName}`));
686
686
  }