sonamu 0.10.7 → 0.10.8

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 (56) hide show
  1. package/dist/bin/cli.js +10 -253
  2. package/dist/migration/code-generation.js +2 -2
  3. package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
  4. package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
  5. package/dist/ui-web/index.html +2 -2
  6. package/package.json +3 -3
  7. package/src/bin/cli.ts +12 -375
  8. package/src/migration/code-generation.ts +1 -1
  9. package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
  10. package/src/skills/AGENTS.md +0 -108
  11. package/src/skills/sonamu/SKILL.md +0 -75
  12. package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
  13. package/src/skills/sonamu-api/SKILL.md +0 -480
  14. package/src/skills/sonamu-auth/SKILL.md +0 -327
  15. package/src/skills/sonamu-auth/references/plugins.md +0 -310
  16. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
  17. package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
  18. package/src/skills/sonamu-config/SKILL.md +0 -203
  19. package/src/skills/sonamu-config/references/database.md +0 -452
  20. package/src/skills/sonamu-config/references/environments.md +0 -178
  21. package/src/skills/sonamu-config/references/server-options.md +0 -400
  22. package/src/skills/sonamu-entity/SKILL.md +0 -180
  23. package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
  24. package/src/skills/sonamu-entity/references/design-guides.md +0 -243
  25. package/src/skills/sonamu-entity/references/field-types.md +0 -170
  26. package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
  27. package/src/skills/sonamu-entity/references/relations.md +0 -463
  28. package/src/skills/sonamu-entity/references/subset.md +0 -156
  29. package/src/skills/sonamu-fixture/SKILL.md +0 -180
  30. package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
  31. package/src/skills/sonamu-fixture/references/cone.md +0 -298
  32. package/src/skills/sonamu-frontend/SKILL.md +0 -142
  33. package/src/skills/sonamu-frontend/references/components.md +0 -323
  34. package/src/skills/sonamu-frontend/references/examples.md +0 -64
  35. package/src/skills/sonamu-frontend/references/hooks.md +0 -273
  36. package/src/skills/sonamu-frontend/references/runtime.md +0 -165
  37. package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
  38. package/src/skills/sonamu-i18n/SKILL.md +0 -287
  39. package/src/skills/sonamu-migration/SKILL.md +0 -316
  40. package/src/skills/sonamu-naite/SKILL.md +0 -266
  41. package/src/skills/sonamu-query/SKILL.md +0 -48
  42. package/src/skills/sonamu-query/references/model-patterns.md +0 -390
  43. package/src/skills/sonamu-query/references/model.md +0 -366
  44. package/src/skills/sonamu-query/references/puri.md +0 -413
  45. package/src/skills/sonamu-query/references/search.md +0 -238
  46. package/src/skills/sonamu-query/references/upsert.md +0 -324
  47. package/src/skills/sonamu-tasks/SKILL.md +0 -236
  48. package/src/skills/sonamu-testing/SKILL.md +0 -251
  49. package/src/skills/sonamu-testing/references/devrunner.md +0 -405
  50. package/src/skills/sonamu-testing/references/helpers.md +0 -185
  51. package/src/skills/sonamu-testing/references/patterns.md +0 -263
  52. package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
  53. package/src/skills/sonamu-testing/references/quick-start.md +0 -285
  54. package/src/skills/sonamu-testing/references/type-safety.md +0 -172
  55. package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
  56. package/src/skills/sonamu-vector/SKILL.md +0 -222
package/src/bin/cli.ts CHANGED
@@ -1,18 +1,7 @@
1
1
  import assert from "assert";
2
2
  import { execSync, spawn } from "child_process";
3
- import {
4
- cp,
5
- mkdir,
6
- readdir,
7
- readFile,
8
- readlink,
9
- realpath,
10
- rm,
11
- symlink,
12
- writeFile,
13
- } from "fs/promises";
3
+ import { mkdir, readdir, writeFile } from "fs/promises";
14
4
  import { createRequire } from "module";
15
- import os from "os";
16
5
  import path from "path";
17
6
  import process from "process";
18
7
 
@@ -187,7 +176,6 @@ async function bootstrap() {
187
176
  ["dev", "web"],
188
177
  ["start"],
189
178
  ["skills", "sync"],
190
- ["skills", "index"],
191
179
  ["test"],
192
180
  ["auth", "generate"],
193
181
  ["auth", "add-companions"],
@@ -218,8 +206,7 @@ async function bootstrap() {
218
206
  dev_api,
219
207
  dev_web,
220
208
  start,
221
- skills_sync,
222
- skills_index,
209
+ skills_sync: skillsMigrationNotice,
223
210
  test: testCommand,
224
211
  auth_generate,
225
212
  "auth_add-companions": auth_add_companions,
@@ -250,6 +237,16 @@ async function sync() {
250
237
  }
251
238
  }
252
239
 
240
+ /**
241
+ * 이전 생성 프로젝트의 postinstall이 업그레이드를 막지 않도록 구 명령만 안전하게 종료합니다.
242
+ */
243
+ async function skillsMigrationNotice() {
244
+ console.log(chalk.yellow("'sonamu skills sync' is no longer supported."));
245
+ console.log(
246
+ chalk.cyan("Install Sonamu skills with 'npx skills@latest add cartanova-ai/skills'."),
247
+ );
248
+ }
249
+
253
250
  /**
254
251
  * API 개발 서버를 실행하는 공통 로직입니다.
255
252
  * dev_all과 dev_api에서 공유합니다.
@@ -1006,366 +1003,6 @@ async function scaffold_view_form(entityId: string) {
1006
1003
  });
1007
1004
  }
1008
1005
 
1009
- /**
1010
- * pnpm sonamu skills sync 하면 실행되는 함수입니다.
1011
- * 공식 Skills를 로컬 프로젝트 또는 글로벌 ~/.claude/로 동기화합니다.
1012
- *
1013
- * --global 플래그: ~/.claude/에 동기화 (프로젝트 생성 전 사용 가능)
1014
- */
1015
- async function skills_sync() {
1016
- const { flags } = parseCliOptions();
1017
- const isGlobal = flags.has("global");
1018
-
1019
- // 개발 환경 - cli.ts: sonamu/modules/sonamu/src/bin/cli.ts
1020
- // 빌드 후 - cli.js: node_modules/sonamu/dist/bin/cli.js (실제 실행)
1021
- // skills 위치: node_modules/sonamu/src/skills (npm 배포 시)
1022
- const sourceBase = path.resolve(import.meta.dirname, "..", "..", "src", "skills");
1023
- if (!(await exists(sourceBase))) {
1024
- console.log(chalk.yellow("Skills source not found in sonamu package."));
1025
- return;
1026
- }
1027
-
1028
- if (isGlobal) {
1029
- await skills_sync_to(os.homedir(), {
1030
- useSymlink: false,
1031
- sourceBase,
1032
- });
1033
-
1034
- // 구버전이 설치한 /sonamu-skills 커맨드 제거.
1035
- // 수동 스킬 메뉴는 자동 discovery와 AGENTS.md 스킬 인덱스로 대체됐습니다.
1036
- await rm(path.join(os.homedir(), ".claude", "commands", "sonamu-skills.md"), { force: true });
1037
-
1038
- console.log(chalk.cyan(`\n Global sync complete → ~/.agents/skills/`));
1039
- console.log(chalk.dim(` These skills are available in every agent session.`));
1040
- console.log(
1041
- chalk.dim(
1042
- ` Once a project is created, run 'pnpm sonamu skills sync' for project-local sync.`,
1043
- ),
1044
- );
1045
- } else {
1046
- await skills_sync_to(findProjectRoot(), {
1047
- useSymlink: true,
1048
- createSettings: true,
1049
- sourceBase,
1050
- });
1051
- }
1052
- }
1053
-
1054
- /**
1055
- * 두 경로가 심볼릭 링크를 따라 같은 실체를 가리키는지 확인합니다.
1056
- */
1057
- async function resolvesToSameDir(a: string, b: string): Promise<boolean> {
1058
- try {
1059
- return (await realpath(a)) === (await realpath(b));
1060
- } catch {
1061
- return false;
1062
- }
1063
- }
1064
-
1065
- /**
1066
- * `.claude/<entry>`가 `../.agents/<entry>`를 가리키도록 보장합니다.
1067
- *
1068
- * 원본은 에이전트 중립인 `.agents/`에 두고, Claude Code용 `.claude/`는 심볼릭
1069
- * 링크로만 연결합니다. 이 저장소의 `CLAUDE.md -> AGENTS.md` 규약과 같은 방식입니다.
1070
- * 이미 심볼릭 링크가 아닌 실체가 있으면 사용자 파일일 수 있으므로 건드리지 않습니다.
1071
- */
1072
- async function linkClaudeEntry(root: string, relative: string): Promise<boolean> {
1073
- // `.claude` 자체가 `.agents`를 가리키는 심볼릭 링크인 저장소에서는 두 경로가
1074
- // 같은 실체를 가리킵니다. 이때 링크를 걸면 원본을 자기 자신을 가리키는
1075
- // 링크로 덮어쓰게 되므로 아무것도 하지 않습니다.
1076
- if (await resolvesToSameDir(path.join(root, ".claude"), path.join(root, ".agents"))) {
1077
- return false;
1078
- }
1079
-
1080
- const target = path.join(root, ".claude", relative);
1081
- const source = path.join(root, ".agents", relative);
1082
-
1083
- let existing: string | undefined;
1084
- try {
1085
- existing = await readlink(target);
1086
- } catch {
1087
- // 심볼릭 링크가 아니거나 존재하지 않음
1088
- }
1089
-
1090
- if (existing === undefined && (await exists(target))) {
1091
- return false;
1092
- }
1093
-
1094
- await mkdir(path.dirname(target), { recursive: true });
1095
- await rm(target, { recursive: true, force: true });
1096
- await symlink(path.relative(path.dirname(target), source), target, "dir");
1097
- return true;
1098
- }
1099
-
1100
- /**
1101
- * 스킬을 배치할 프로젝트 루트를 찾습니다.
1102
- *
1103
- * Sonamu 프로젝트는 두 가지 레이아웃이 공존합니다.
1104
- *
1105
- * - `<root>/api` (예: examples/miomock)
1106
- * - `<root>/packages/api` (create-sonamu가 생성하는 레이아웃)
1107
- *
1108
- * `findAppRootPath()`는 api의 부모를 그대로 돌려주므로 후자에서 `<root>/packages`가
1109
- * 나오고, `findWorkspaceRoot()`는 pnpm-workspace.yaml을 찾아 올라가므로 모노레포에
1110
- * 든 프로젝트에서 바깥 저장소가 잡힙니다. api의 부모가 `packages`면 한 단계 더
1111
- * 올라가는 방식으로 두 경우를 모두 맞춥니다.
1112
- */
1113
- function findProjectRoot(): string {
1114
- const apiParent = findAppRootPath();
1115
- return path.basename(apiParent) === "packages" ? path.dirname(apiParent) : apiParent;
1116
- }
1117
-
1118
- /**
1119
- * 구버전이 남긴 `skills/sonamu` 평면 디렉토리를 제거합니다.
1120
- *
1121
- * 예전에는 모든 스킬 문서를 `skills/sonamu/*.md` 한 디렉토리에 복사했습니다.
1122
- * 지금은 `sonamu`가 라우팅 테이블을 담은 루트 스킬 이름이라 경로가 겹치는데,
1123
- * 남아 있는 구버전 디렉토리에는 삭제된 문서를 가리키는 옛 SKILL.md가 들어 있어
1124
- * 그대로 두면 낡은 라우팅 테이블이 스킬로 계속 로드됩니다.
1125
- *
1126
- * 심볼릭 링크는 정상 배치 경로에서 처리하므로 건드리지 않습니다. 최상위에
1127
- * SKILL.md 외의 `.md`가 있으면 구버전 산출물로 판정합니다 — 새 루트 스킬은
1128
- * SKILL.md 하나만 가집니다.
1129
- */
1130
- async function removeLegacyFlatSkillDir(root: string): Promise<void> {
1131
- for (const base of [".agents", ".claude"]) {
1132
- const target = path.join(root, base, "skills", "sonamu");
1133
-
1134
- try {
1135
- await readlink(target);
1136
- continue; // 심볼릭 링크는 정상 경로에서 갱신됩니다
1137
- } catch {
1138
- // 심볼릭 링크가 아님
1139
- }
1140
-
1141
- if (!(await exists(target))) {
1142
- continue;
1143
- }
1144
-
1145
- let entries: string[];
1146
- try {
1147
- entries = await readdir(target);
1148
- } catch {
1149
- continue;
1150
- }
1151
-
1152
- const hasLegacyDocs = entries.some((e) => e.endsWith(".md") && e !== "SKILL.md");
1153
- if (!hasLegacyDocs) {
1154
- continue;
1155
- }
1156
-
1157
- await rm(target, { recursive: true, force: true });
1158
- console.log(chalk.yellow(`✓ Removed legacy ${base}/skills/sonamu (flat layout)`));
1159
- }
1160
- }
1161
-
1162
- /**
1163
- * 배치 대상 스킬 디렉토리를 나열합니다.
1164
- *
1165
- * 에이전트는 `skills/<name>/SKILL.md` 단위로 스킬을 인식하므로,
1166
- * SKILL.md를 가진 디렉토리만 대상으로 삼습니다.
1167
- */
1168
- async function listSkillDirs(sourceBase?: string): Promise<{ source: string; name: string }[]> {
1169
- if (!sourceBase || !(await exists(sourceBase))) {
1170
- return [];
1171
- }
1172
-
1173
- const entries = await readdir(sourceBase, { withFileTypes: true });
1174
- const result: { source: string; name: string }[] = [];
1175
-
1176
- for (const entry of entries) {
1177
- if (!entry.isDirectory() || !entry.name.startsWith("sonamu")) {
1178
- continue;
1179
- }
1180
- const source = path.join(sourceBase, entry.name);
1181
- if (!(await exists(path.join(source, "SKILL.md")))) {
1182
- continue;
1183
- }
1184
- result.push({ source, name: entry.name });
1185
- }
1186
-
1187
- return result.sort((a, b) => a.name.localeCompare(b.name));
1188
- }
1189
-
1190
- /**
1191
- * 프로젝트(또는 홈) 루트에 skills를 동기화하는 공통 로직입니다.
1192
- *
1193
- * 원본은 `.agents/`에 두고 `.claude/`에는 심볼릭 링크만 겁니다.
1194
- */
1195
- async function skills_sync_to(
1196
- root: string,
1197
- options: {
1198
- useSymlink: boolean;
1199
- createSettings?: boolean;
1200
- sourceBase?: string;
1201
- },
1202
- ) {
1203
- const agentsDir = path.join(root, ".agents");
1204
- const skillsRoot = path.join(agentsDir, "skills");
1205
- await mkdir(skillsRoot, { recursive: true });
1206
-
1207
- await removeLegacyFlatSkillDir(root);
1208
-
1209
- // 에이전트는 skills/<name>/SKILL.md 단위로 스킬을 인식하므로 각각 별도 배치해야 합니다.
1210
- const targets = await listSkillDirs(options.sourceBase);
1211
-
1212
- for (const { source, name } of targets) {
1213
- const targetDir = path.join(skillsRoot, name);
1214
-
1215
- // exists()는 broken symlink를 감지하지 못하므로 rm을 무조건 시도합니다
1216
- try {
1217
- await rm(targetDir, { recursive: true, force: true });
1218
- } catch {
1219
- // 파일이 없으면 무시
1220
- }
1221
-
1222
- if (options.useSymlink) {
1223
- try {
1224
- await symlink(source, targetDir, "dir");
1225
- } catch (error) {
1226
- console.log(
1227
- chalk.yellow(
1228
- `⚠ Symlink failed (${name}): ${error instanceof Error ? error.message : String(error)}`,
1229
- ),
1230
- );
1231
- console.log(chalk.yellow(` Falling back to copy...`));
1232
- await skillsCopy(source, targetDir);
1233
- }
1234
- } else {
1235
- await skillsCopy(source, targetDir);
1236
- }
1237
-
1238
- await linkClaudeEntry(root, path.join("skills", name));
1239
- }
1240
-
1241
- console.log(
1242
- chalk.green(
1243
- `✓ Skills ${options.useSymlink ? "linked" : "copied"} → .agents/skills/ (${targets.length}): ${targets.map((t) => t.name).join(", ")}`,
1244
- ),
1245
- );
1246
- console.log(chalk.dim(` .claude/skills/* symlinked for Claude Code`));
1247
-
1248
- // settings.local.json — Claude Code 전용 파일이므로 .claude/에 직접 둡니다.
1249
- // project-local 모드에서만, 없을 때만 생성합니다.
1250
- if (options.createSettings) {
1251
- await mkdir(path.join(root, ".claude"), { recursive: true });
1252
- const settingsLocalPath = path.join(root, ".claude", "settings.local.json");
1253
- if (!(await exists(settingsLocalPath))) {
1254
- try {
1255
- const settingsContent = {
1256
- hooks: {
1257
- PostToolUse: [
1258
- {
1259
- matcher: "Edit|Write|MultiEdit",
1260
- hooks: [
1261
- {
1262
- type: "command",
1263
- command: "pnpm check 2>&1 | head -60",
1264
- },
1265
- ],
1266
- },
1267
- ],
1268
- },
1269
- };
1270
- await writeFile(settingsLocalPath, `${JSON.stringify(settingsContent, null, 2)}\n`);
1271
- console.log(chalk.green(`✓ .claude/settings.local.json created`));
1272
- } catch (error) {
1273
- console.error(
1274
- chalk.red(
1275
- `✗ Failed to create settings.local.json: ${error instanceof Error ? error.message : String(error)}`,
1276
- ),
1277
- );
1278
- }
1279
- } else {
1280
- console.log(chalk.dim(`⏭ .claude/settings.local.json already exists (preserved)`));
1281
- }
1282
- }
1283
-
1284
- console.log(
1285
- chalk.dim(
1286
- `\n To also write the skill index into this project's AGENTS.md, run 'sonamu skills index'.`,
1287
- ),
1288
- );
1289
- }
1290
-
1291
- /**
1292
- * 루트 스킬(`sonamu`)의 라우팅 테이블과 공통 규약을 프로젝트 AGENTS.md에 씁니다.
1293
- *
1294
- * 개별 스킬의 description은 각자의 발동 순간에만 걸리므로, 어느 스킬에도 맞지
1295
- * 않는 작업에서는 아무것도 안 걸립니다. 상시 로드되는 AGENTS.md에 인덱스를 두면
1296
- * 그 공백을 메울 수 있습니다.
1297
- *
1298
- * 다만 사용자의 에이전트 프롬프트 파일을 고치는 동작이므로 `skills sync`에
1299
- * 포함하지 않고, 사용자가 명시적으로 실행할 때만 수행합니다.
1300
- * `<!-- SONAMU:START -->` ~ `<!-- SONAMU:END -->` 구간만 갱신하며 나머지는 보존합니다.
1301
- */
1302
- async function skills_index() {
1303
- const appRoot = findProjectRoot();
1304
- const sourceBase = path.resolve(import.meta.dirname, "..", "..", "src", "skills");
1305
- const rootSkill = path.join(sourceBase, "sonamu", "SKILL.md");
1306
-
1307
- if (!(await exists(rootSkill))) {
1308
- console.log(chalk.yellow("Root skill not found in sonamu package."));
1309
- return;
1310
- }
1311
-
1312
- // frontmatter는 스킬 메타데이터이므로 주입 대상에서 제외합니다
1313
- const body = (await readFile(rootSkill, "utf-8")).replace(/^---\n[\s\S]*?\n---\n+/, "");
1314
-
1315
- const startMarker = "<!-- SONAMU:START -->";
1316
- const endMarker = "<!-- SONAMU:END -->";
1317
- const section = `${startMarker}\n${body.trim()}\n${endMarker}`;
1318
-
1319
- const targetPath = path.join(appRoot, "AGENTS.md");
1320
- const existing = (await exists(targetPath)) ? await readFile(targetPath, "utf-8") : undefined;
1321
-
1322
- let next: string;
1323
- let action: string;
1324
- if (existing === undefined) {
1325
- next = `${section}\n`;
1326
- action = "created";
1327
- } else {
1328
- const startIdx = existing.indexOf(startMarker);
1329
- const endIdx = existing.indexOf(endMarker);
1330
- if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
1331
- next = existing.slice(0, startIdx) + section + existing.slice(endIdx + endMarker.length);
1332
- action = "updated";
1333
- } else {
1334
- next = `${existing.trimEnd()}\n\n${section}\n`;
1335
- action = "appended";
1336
- }
1337
- }
1338
-
1339
- if (next === existing) {
1340
- console.log(chalk.dim(`⏭ AGENTS.md already up to date`));
1341
- return;
1342
- }
1343
-
1344
- await writeFile(targetPath, next);
1345
- console.log(chalk.green(`✓ AGENTS.md ${action} (Sonamu marker region)`));
1346
-
1347
- if (!(await exists(path.join(appRoot, "CLAUDE.md")))) {
1348
- await symlink("AGENTS.md", path.join(appRoot, "CLAUDE.md"));
1349
- console.log(chalk.dim(` CLAUDE.md → AGENTS.md`));
1350
- }
1351
-
1352
- console.log(chalk.dim(` Remove the marker region to undo.`));
1353
- }
1354
-
1355
- async function skillsCopy(src: string, dest: string) {
1356
- try {
1357
- await cp(src, dest, { recursive: true });
1358
- console.log(chalk.green(`✓ Skills copied`));
1359
- } catch (copyError) {
1360
- console.error(
1361
- chalk.red(
1362
- `✗ Failed to copy skills: ${copyError instanceof Error ? copyError.message : String(copyError)}`,
1363
- ),
1364
- );
1365
- throw copyError;
1366
- }
1367
- }
1368
-
1369
1006
  async function auth_generate() {
1370
1007
  // --plugins 옵션 파싱
1371
1008
  const pluginsArg = process.argv.find((arg) => arg.startsWith("--plugins"));
@@ -885,7 +885,7 @@ function genNormalColumnDefinition(column: MigrationColumn): string {
885
885
  if (column.defaultTo !== undefined) {
886
886
  if (typeof column.defaultTo === "string" && column.defaultTo.startsWith(`"`)) {
887
887
  chains.push(`defaultTo(${column.defaultTo})`);
888
- } else if (column.type === "json" && typeof column.defaultTo.startsWith('"')) {
888
+ } else if (column.type === "json" && typeof column.defaultTo === "string") {
889
889
  chains.push(`defaultTo(knex.raw("${column.defaultTo.replaceAll('"', "'")}::jsonb"))`);
890
890
  } else {
891
891
  chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);