sonamu 0.10.4 → 0.10.6
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.
- package/dist/bin/cli.js +206 -187
- package/dist/cone/cone-generator.js +3 -9
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +2 -2
- package/dist/database/puri.d.ts +17 -0
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +102 -3
- package/dist/migration/code-generation.js +6 -6
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +51 -6
- package/dist/ui-web/assets/{index-CDd6xT-F.js → index-D0MHYbxl.js} +2 -2
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +2 -2
- package/src/bin/cli.ts +283 -282
- package/src/cone/cone-generator.ts +2 -18
- package/src/database/__tests__/puri.test.ts +183 -0
- package/src/database/knex.ts +4 -1
- package/src/database/puri.ts +229 -2
- package/src/database/puri.types.test-d.ts +56 -0
- package/src/migration/code-generation.ts +5 -5
- package/src/migration/migrator.ts +81 -7
- package/src/skills/AGENTS.md +76 -50
- package/src/skills/sonamu/SKILL.md +46 -227
- package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
- package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
- package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
- package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
- package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
- package/src/skills/sonamu-config/SKILL.md +203 -0
- package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
- package/src/skills/sonamu-config/references/environments.md +178 -0
- package/src/skills/sonamu-config/references/server-options.md +400 -0
- package/src/skills/sonamu-entity/SKILL.md +180 -0
- package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
- package/src/skills/sonamu-entity/references/design-guides.md +243 -0
- package/src/skills/sonamu-entity/references/field-types.md +170 -0
- package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
- package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
- package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
- package/src/skills/sonamu-fixture/SKILL.md +180 -0
- package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
- package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
- package/src/skills/sonamu-frontend/SKILL.md +142 -0
- package/src/skills/sonamu-frontend/references/components.md +323 -0
- package/src/skills/sonamu-frontend/references/examples.md +64 -0
- package/src/skills/sonamu-frontend/references/hooks.md +273 -0
- package/src/skills/sonamu-frontend/references/runtime.md +165 -0
- package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
- package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
- package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
- package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
- package/src/skills/sonamu-query/SKILL.md +48 -0
- package/src/skills/sonamu-query/references/model-patterns.md +390 -0
- package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
- package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
- package/src/skills/sonamu-query/references/search.md +238 -0
- package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
- package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
- package/src/skills/sonamu-testing/SKILL.md +251 -0
- package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
- package/src/skills/sonamu-testing/references/helpers.md +185 -0
- package/src/skills/sonamu-testing/references/patterns.md +263 -0
- package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
- package/src/skills/sonamu-testing/references/quick-start.md +285 -0
- package/src/skills/sonamu-testing/references/type-safety.md +172 -0
- package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
- package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
- package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
- package/src/skills/commands/sonamu-skills.md +0 -20
- package/src/skills/project/README.md +0 -19
- package/src/skills/project/architecture.md +0 -373
- package/src/skills/sonamu/cdd.md +0 -129
- package/src/skills/sonamu/config.md +0 -772
- package/src/skills/sonamu/create-sonamu.md +0 -208
- package/src/skills/sonamu/entity-basic.md +0 -678
- package/src/skills/sonamu/framework-change.md +0 -96
- package/src/skills/sonamu/frontend.md +0 -915
- package/src/skills/sonamu/project-init.md +0 -477
- package/src/skills/sonamu/skill-contribution.md +0 -247
- package/src/skills/sonamu/testing.md +0 -2163
package/src/bin/cli.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
2
|
import { execSync, spawn } from "child_process";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cp,
|
|
5
|
+
mkdir,
|
|
6
|
+
readdir,
|
|
7
|
+
readFile,
|
|
8
|
+
readlink,
|
|
9
|
+
realpath,
|
|
10
|
+
rm,
|
|
11
|
+
symlink,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "fs/promises";
|
|
4
14
|
import { createRequire } from "module";
|
|
5
15
|
import os from "os";
|
|
6
16
|
import path from "path";
|
|
@@ -177,7 +187,7 @@ async function bootstrap() {
|
|
|
177
187
|
["dev", "web"],
|
|
178
188
|
["start"],
|
|
179
189
|
["skills", "sync"],
|
|
180
|
-
["skills", "
|
|
190
|
+
["skills", "index"],
|
|
181
191
|
["test"],
|
|
182
192
|
["auth", "generate"],
|
|
183
193
|
["auth", "add-companions"],
|
|
@@ -197,8 +207,8 @@ async function bootstrap() {
|
|
|
197
207
|
stub_entity,
|
|
198
208
|
scaffold_model,
|
|
199
209
|
scaffold_model_test,
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
scaffold_view_list,
|
|
211
|
+
scaffold_view_form,
|
|
202
212
|
cone_gen,
|
|
203
213
|
sync,
|
|
204
214
|
build_all,
|
|
@@ -209,7 +219,7 @@ async function bootstrap() {
|
|
|
209
219
|
dev_web,
|
|
210
220
|
start,
|
|
211
221
|
skills_sync,
|
|
212
|
-
|
|
222
|
+
skills_index,
|
|
213
223
|
test: testCommand,
|
|
214
224
|
auth_generate,
|
|
215
225
|
"auth_add-companions": auth_add_companions,
|
|
@@ -983,6 +993,19 @@ async function scaffold_model_test(entityId: string) {
|
|
|
983
993
|
});
|
|
984
994
|
}
|
|
985
995
|
|
|
996
|
+
async function scaffold_view_list(entityId: string) {
|
|
997
|
+
await Sonamu.syncer.generateTemplate("view_list", {
|
|
998
|
+
entityId,
|
|
999
|
+
extra: undefined,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function scaffold_view_form(entityId: string) {
|
|
1004
|
+
await Sonamu.syncer.generateTemplate("view_form", {
|
|
1005
|
+
entityId,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
986
1009
|
/**
|
|
987
1010
|
* pnpm sonamu skills sync 하면 실행되는 함수입니다.
|
|
988
1011
|
* 공식 Skills를 로컬 프로젝트 또는 글로벌 ~/.claude/로 동기화합니다.
|
|
@@ -997,117 +1020,236 @@ async function skills_sync() {
|
|
|
997
1020
|
// 빌드 후 - cli.js: node_modules/sonamu/dist/bin/cli.js (실제 실행)
|
|
998
1021
|
// skills 위치: node_modules/sonamu/src/skills (npm 배포 시)
|
|
999
1022
|
const sourceBase = path.resolve(import.meta.dirname, "..", "..", "src", "skills");
|
|
1000
|
-
|
|
1001
|
-
const sourceClaudeMd = path.join(sourceBase, "CLAUDE.md");
|
|
1002
|
-
|
|
1003
|
-
if (!(await exists(sourceSkillsDir))) {
|
|
1023
|
+
if (!(await exists(sourceBase))) {
|
|
1004
1024
|
console.log(chalk.yellow("Skills source not found in sonamu package."));
|
|
1005
1025
|
return;
|
|
1006
1026
|
}
|
|
1007
1027
|
|
|
1008
1028
|
if (isGlobal) {
|
|
1009
|
-
|
|
1010
|
-
await skills_sync_to(homeClaudeDir, sourceSkillsDir, sourceClaudeMd, {
|
|
1029
|
+
await skills_sync_to(os.homedir(), {
|
|
1011
1030
|
useSymlink: false,
|
|
1012
|
-
|
|
1013
|
-
isGlobal: true,
|
|
1031
|
+
sourceBase,
|
|
1014
1032
|
});
|
|
1015
1033
|
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if (await exists(sourceCommand)) {
|
|
1020
|
-
const targetCommandsDir = path.join(homeClaudeDir, "commands");
|
|
1021
|
-
await mkdir(targetCommandsDir, { recursive: true });
|
|
1022
|
-
await cp(sourceCommand, path.join(targetCommandsDir, "sonamu-skills.md"));
|
|
1023
|
-
console.log(chalk.green(`✓ /sonamu-skills command installed → ~/.claude/commands/`));
|
|
1024
|
-
}
|
|
1034
|
+
// 구버전이 설치한 /sonamu-skills 커맨드 제거.
|
|
1035
|
+
// 수동 스킬 메뉴는 자동 discovery와 AGENTS.md 스킬 인덱스로 대체됐습니다.
|
|
1036
|
+
await rm(path.join(os.homedir(), ".claude", "commands", "sonamu-skills.md"), { force: true });
|
|
1025
1037
|
|
|
1026
|
-
console.log(chalk.cyan(`\n Global sync complete → ~/.
|
|
1027
|
-
console.log(chalk.dim(` These skills are available in
|
|
1038
|
+
console.log(chalk.cyan(`\n Global sync complete → ~/.agents/skills/`));
|
|
1039
|
+
console.log(chalk.dim(` These skills are available in every agent session.`));
|
|
1028
1040
|
console.log(
|
|
1029
1041
|
chalk.dim(
|
|
1030
1042
|
` Once a project is created, run 'pnpm sonamu skills sync' for project-local sync.`,
|
|
1031
1043
|
),
|
|
1032
1044
|
);
|
|
1033
1045
|
} else {
|
|
1034
|
-
|
|
1035
|
-
const claudeDir = path.join(workspaceRoot, ".claude");
|
|
1036
|
-
await skills_sync_to(claudeDir, sourceSkillsDir, sourceClaudeMd, {
|
|
1046
|
+
await skills_sync_to(findProjectRoot(), {
|
|
1037
1047
|
useSymlink: true,
|
|
1038
|
-
|
|
1048
|
+
createSettings: true,
|
|
1039
1049
|
sourceBase,
|
|
1040
1050
|
});
|
|
1041
1051
|
}
|
|
1042
1052
|
}
|
|
1043
1053
|
|
|
1044
1054
|
/**
|
|
1045
|
-
*
|
|
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/`에는 심볼릭 링크만 겁니다.
|
|
1046
1194
|
*/
|
|
1047
1195
|
async function skills_sync_to(
|
|
1048
|
-
|
|
1049
|
-
sourceSkillsDir: string,
|
|
1050
|
-
sourceClaudeMd: string,
|
|
1196
|
+
root: string,
|
|
1051
1197
|
options: {
|
|
1052
1198
|
useSymlink: boolean;
|
|
1053
|
-
|
|
1199
|
+
createSettings?: boolean;
|
|
1054
1200
|
sourceBase?: string;
|
|
1055
|
-
isGlobal?: boolean;
|
|
1056
1201
|
},
|
|
1057
1202
|
) {
|
|
1058
|
-
const
|
|
1203
|
+
const agentsDir = path.join(root, ".agents");
|
|
1204
|
+
const skillsRoot = path.join(agentsDir, "skills");
|
|
1205
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
1059
1206
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
} catch {
|
|
1065
|
-
// 파일이 없으면 무시
|
|
1066
|
-
}
|
|
1207
|
+
await removeLegacyFlatSkillDir(root);
|
|
1208
|
+
|
|
1209
|
+
// 에이전트는 skills/<name>/SKILL.md 단위로 스킬을 인식하므로 각각 별도 배치해야 합니다.
|
|
1210
|
+
const targets = await listSkillDirs(options.sourceBase);
|
|
1067
1211
|
|
|
1068
|
-
|
|
1212
|
+
for (const { source, name } of targets) {
|
|
1213
|
+
const targetDir = path.join(skillsRoot, name);
|
|
1069
1214
|
|
|
1070
|
-
|
|
1215
|
+
// exists()는 broken symlink를 감지하지 못하므로 rm을 무조건 시도합니다
|
|
1071
1216
|
try {
|
|
1072
|
-
await
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
console.log(
|
|
1076
|
-
chalk.yellow(`⚠ Symlink failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
1077
|
-
);
|
|
1078
|
-
console.log(chalk.yellow(` Falling back to copy...`));
|
|
1079
|
-
await skillsCopy(sourceSkillsDir, targetSkillsDir);
|
|
1217
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
1218
|
+
} catch {
|
|
1219
|
+
// 파일이 없으면 무시
|
|
1080
1220
|
}
|
|
1081
|
-
} else {
|
|
1082
|
-
await skillsCopy(sourceSkillsDir, targetSkillsDir);
|
|
1083
|
-
}
|
|
1084
1221
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
console.error(
|
|
1097
|
-
chalk.red(
|
|
1098
|
-
`✗ Failed to initialize project templates: ${error instanceof Error ? error.message : String(error)}`,
|
|
1099
|
-
),
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
} else {
|
|
1103
|
-
console.log(chalk.dim(`⏭ Project templates already exist (preserved)`));
|
|
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);
|
|
1104
1233
|
}
|
|
1234
|
+
} else {
|
|
1235
|
+
await skillsCopy(source, targetDir);
|
|
1105
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
await linkClaudeEntry(root, path.join("skills", name));
|
|
1106
1239
|
}
|
|
1107
1240
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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");
|
|
1111
1253
|
if (!(await exists(settingsLocalPath))) {
|
|
1112
1254
|
try {
|
|
1113
1255
|
const settingsContent = {
|
|
@@ -1139,191 +1281,91 @@ async function skills_sync_to(
|
|
|
1139
1281
|
}
|
|
1140
1282
|
}
|
|
1141
1283
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// 글로벌 모드에서는 상대 경로를 절대 경로로 변환합니다
|
|
1148
|
-
const sourceContent = options.isGlobal
|
|
1149
|
-
? rawContent.replaceAll(".claude/skills/sonamu/", "~/.claude/skills/sonamu/")
|
|
1150
|
-
: rawContent;
|
|
1151
|
-
|
|
1152
|
-
if (await exists(targetClaudeMd)) {
|
|
1153
|
-
const targetContent = await readFile(targetClaudeMd, "utf-8");
|
|
1154
|
-
const startMarker = "<!-- SONAMU:START -->";
|
|
1155
|
-
const endMarker = "<!-- SONAMU:END -->";
|
|
1156
|
-
if (targetContent.includes(startMarker) && targetContent.includes(endMarker)) {
|
|
1157
|
-
const startIdx = targetContent.indexOf(startMarker);
|
|
1158
|
-
const endIdx = targetContent.indexOf(endMarker);
|
|
1159
|
-
|
|
1160
|
-
if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
|
|
1161
|
-
const before = targetContent.substring(0, startIdx);
|
|
1162
|
-
const after = targetContent.substring(endIdx + endMarker.length);
|
|
1163
|
-
const newContent = `${before}${startMarker}\n${sourceContent}\n${endMarker}${after}`;
|
|
1164
|
-
await writeFile(targetClaudeMd, newContent);
|
|
1165
|
-
console.log(chalk.green(`✓ CLAUDE.md updated (marker region)`));
|
|
1166
|
-
} else {
|
|
1167
|
-
console.log(chalk.yellow(`⏭ CLAUDE.md marker positions invalid, skipped`));
|
|
1168
|
-
}
|
|
1169
|
-
} else {
|
|
1170
|
-
// 마커가 없는 기존 CLAUDE.md에 Sonamu 섹션을 추가합니다
|
|
1171
|
-
const appended = `${targetContent.trimEnd()}\n\n<!-- SONAMU:START -->\n${sourceContent}\n<!-- SONAMU:END -->\n`;
|
|
1172
|
-
await writeFile(targetClaudeMd, appended);
|
|
1173
|
-
console.log(chalk.green(`✓ CLAUDE.md updated (appended Sonamu section)`));
|
|
1174
|
-
}
|
|
1175
|
-
} else {
|
|
1176
|
-
const withMarkers = `<!-- SONAMU:START -->\n${sourceContent}\n<!-- SONAMU:END -->\n`;
|
|
1177
|
-
await writeFile(targetClaudeMd, withMarkers);
|
|
1178
|
-
console.log(chalk.green(`✓ CLAUDE.md created`));
|
|
1179
|
-
}
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
console.error(
|
|
1182
|
-
chalk.red(
|
|
1183
|
-
`✗ Failed to update CLAUDE.md: ${error instanceof Error ? error.message : String(error)}`,
|
|
1184
|
-
),
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
async function skillsCopy(src: string, dest: string) {
|
|
1191
|
-
try {
|
|
1192
|
-
await cp(src, dest, { recursive: true });
|
|
1193
|
-
console.log(chalk.green(`✓ Skills copied`));
|
|
1194
|
-
} catch (copyError) {
|
|
1195
|
-
console.error(
|
|
1196
|
-
chalk.red(
|
|
1197
|
-
`✗ Failed to copy skills: ${copyError instanceof Error ? copyError.message : String(copyError)}`,
|
|
1198
|
-
),
|
|
1199
|
-
);
|
|
1200
|
-
throw copyError;
|
|
1201
|
-
}
|
|
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
|
+
);
|
|
1202
1289
|
}
|
|
1203
1290
|
|
|
1204
1291
|
/**
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1292
|
+
* 루트 스킬(`sonamu`)의 라우팅 테이블과 공통 규약을 프로젝트 AGENTS.md에 씁니다.
|
|
1293
|
+
*
|
|
1294
|
+
* 개별 스킬의 description은 각자의 발동 순간에만 걸리므로, 어느 스킬에도 맞지
|
|
1295
|
+
* 않는 작업에서는 아무것도 안 걸립니다. 상시 로드되는 AGENTS.md에 인덱스를 두면
|
|
1296
|
+
* 그 공백을 메울 수 있습니다.
|
|
1297
|
+
*
|
|
1298
|
+
* 다만 사용자의 에이전트 프롬프트 파일을 고치는 동작이므로 `skills sync`에
|
|
1299
|
+
* 포함하지 않고, 사용자가 명시적으로 실행할 때만 수행합니다.
|
|
1300
|
+
* `<!-- SONAMU:START -->` ~ `<!-- SONAMU:END -->` 구간만 갱신하며 나머지는 보존합니다.
|
|
1207
1301
|
*/
|
|
1208
|
-
async function
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
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");
|
|
1211
1306
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
console.error(chalk.red("✗ Skill name is required"));
|
|
1307
|
+
if (!(await exists(rootSkill))) {
|
|
1308
|
+
console.log(chalk.yellow("Root skill not found in sonamu package."));
|
|
1215
1309
|
return;
|
|
1216
1310
|
}
|
|
1217
1311
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
.replace(/\s+/g, "-")
|
|
1221
|
-
// 경로 구분자 제거
|
|
1222
|
-
.replace(/[/\\]/g, "-")
|
|
1223
|
-
// Path traversal 방지
|
|
1224
|
-
.replace(/\.\./g, "")
|
|
1225
|
-
// Windows 금지 문자 제거
|
|
1226
|
-
.replace(/[<>:"|?*]/g, "")
|
|
1227
|
-
// 시작/끝 점, 하이픈, 언더스코어 제거
|
|
1228
|
-
.replace(/^[.\-_]+|[.\-_]+$/g, "")
|
|
1229
|
-
// 연속된 하이픈을 하나로
|
|
1230
|
-
.replace(/-+/g, "-")
|
|
1231
|
-
// 알파벳, 숫자, 하이픈, 언더스코어, 한글만 허용
|
|
1232
|
-
.replace(/[^a-zA-Z0-9-_가-힣]/g, "");
|
|
1233
|
-
|
|
1234
|
-
// 길이 제한
|
|
1235
|
-
const MAX_LENGTH = 100;
|
|
1236
|
-
if (sanitized.length > MAX_LENGTH) {
|
|
1237
|
-
sanitized = sanitized.substring(0, MAX_LENGTH);
|
|
1238
|
-
console.log(chalk.yellow(`⚠ Name truncated to ${MAX_LENGTH} characters`));
|
|
1239
|
-
}
|
|
1312
|
+
// frontmatter는 스킬 메타데이터이므로 주입 대상에서 제외합니다
|
|
1313
|
+
const body = (await readFile(rootSkill, "utf-8")).replace(/^---\n[\s\S]*?\n---\n+/, "");
|
|
1240
1314
|
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
"PRN",
|
|
1245
|
-
"AUX",
|
|
1246
|
-
"NUL",
|
|
1247
|
-
"COM1",
|
|
1248
|
-
"COM2",
|
|
1249
|
-
"COM3",
|
|
1250
|
-
"COM4",
|
|
1251
|
-
"COM5",
|
|
1252
|
-
"COM6",
|
|
1253
|
-
"COM7",
|
|
1254
|
-
"COM8",
|
|
1255
|
-
"COM9",
|
|
1256
|
-
"LPT1",
|
|
1257
|
-
"LPT2",
|
|
1258
|
-
"LPT3",
|
|
1259
|
-
"LPT4",
|
|
1260
|
-
"LPT5",
|
|
1261
|
-
"LPT6",
|
|
1262
|
-
"LPT7",
|
|
1263
|
-
"LPT8",
|
|
1264
|
-
"LPT9",
|
|
1265
|
-
];
|
|
1266
|
-
if (RESERVED_NAMES.includes(sanitized.toUpperCase())) {
|
|
1267
|
-
sanitized = `skill-${sanitized}`;
|
|
1268
|
-
console.log(chalk.yellow(`⚠ Reserved name detected, prefixed with "skill-"`));
|
|
1269
|
-
}
|
|
1315
|
+
const startMarker = "<!-- SONAMU:START -->";
|
|
1316
|
+
const endMarker = "<!-- SONAMU:END -->";
|
|
1317
|
+
const section = `${startMarker}\n${body.trim()}\n${endMarker}`;
|
|
1270
1318
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
console.error(chalk.red("✗ Invalid skill name after sanitization"));
|
|
1274
|
-
console.log(chalk.dim(` Original: "${name}"`));
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1319
|
+
const targetPath = path.join(appRoot, "AGENTS.md");
|
|
1320
|
+
const existing = (await exists(targetPath)) ? await readFile(targetPath, "utf-8") : undefined;
|
|
1277
1321
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
+
}
|
|
1281
1337
|
}
|
|
1282
1338
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (await exists(filePath)) {
|
|
1286
|
-
console.log(chalk.yellow(`Skill "${sanitized}" already exists.`));
|
|
1339
|
+
if (next === existing) {
|
|
1340
|
+
console.log(chalk.dim(`⏭ AGENTS.md already up to date`));
|
|
1287
1341
|
return;
|
|
1288
1342
|
}
|
|
1289
1343
|
|
|
1290
|
-
await
|
|
1291
|
-
|
|
1292
|
-
const template = `---
|
|
1293
|
-
name: ${sanitized}
|
|
1294
|
-
category: other
|
|
1295
|
-
created_at: ${new Date().toISOString().split("T")[0]}
|
|
1296
|
-
status: draft
|
|
1297
|
-
---
|
|
1298
|
-
|
|
1299
|
-
# ${sanitized}
|
|
1300
|
-
|
|
1301
|
-
## 상황
|
|
1302
|
-
|
|
1303
|
-
[어떤 문제였는지]
|
|
1304
|
-
|
|
1305
|
-
## 해결 방법
|
|
1344
|
+
await writeFile(targetPath, next);
|
|
1345
|
+
console.log(chalk.green(`✓ AGENTS.md ${action} (Sonamu marker region)`));
|
|
1306
1346
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
+
}
|
|
1310
1351
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
\`\`\`
|
|
1314
|
-
`;
|
|
1352
|
+
console.log(chalk.dim(` Remove the marker region to undo.`));
|
|
1353
|
+
}
|
|
1315
1354
|
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
}
|
|
1318
1367
|
}
|
|
1319
1368
|
|
|
1320
|
-
/**
|
|
1321
|
-
* pnpm sonamu auth generate 하면 실행되는 함수입니다.
|
|
1322
|
-
* better-auth 엔티티들(User, Session, Account, Verification)을 생성합니다.
|
|
1323
|
-
*
|
|
1324
|
-
* 옵션:
|
|
1325
|
-
* --plugins phone-number,2fa 플러그인 엔티티도 함께 생성
|
|
1326
|
-
*/
|
|
1327
1369
|
async function auth_generate() {
|
|
1328
1370
|
// --plugins 옵션 파싱
|
|
1329
1371
|
const pluginsArg = process.argv.find((arg) => arg.startsWith("--plugins"));
|
|
@@ -1368,44 +1410,3 @@ async function auth_add_companions() {
|
|
|
1368
1410
|
await addCompanionsToEntities();
|
|
1369
1411
|
console.log(chalk.bold("\n✅ Done!"));
|
|
1370
1412
|
}
|
|
1371
|
-
|
|
1372
|
-
/**
|
|
1373
|
-
* 워크스페이스 루트를 찾습니다.
|
|
1374
|
-
* 우선순위: pnpm-workspace.yaml > package.json(workspaces) > .agents/
|
|
1375
|
-
*
|
|
1376
|
-
* CLAUDE.md는 서브패키지에도 존재할 수 있으므로 사용하지 않습니다.
|
|
1377
|
-
* .agents/는 agents init이 생성하는 디렉토리로, 워크스페이스 루트에만 존재합니다.
|
|
1378
|
-
*/
|
|
1379
|
-
async function findWorkspaceRoot() {
|
|
1380
|
-
let dir = process.cwd();
|
|
1381
|
-
|
|
1382
|
-
while (dir !== path.dirname(dir)) {
|
|
1383
|
-
// 1. pnpm-workspace.yaml: 확실한 monorepo 루트.
|
|
1384
|
-
if (await exists(path.join(dir, "pnpm-workspace.yaml"))) {
|
|
1385
|
-
return dir;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// 2. package.json에 workspaces 필드가 있으면 monorepo 루트.
|
|
1389
|
-
const packagePath = path.join(dir, "package.json");
|
|
1390
|
-
if (await exists(packagePath)) {
|
|
1391
|
-
try {
|
|
1392
|
-
const packageJson = JSON.parse(await readFile(packagePath, "utf-8"));
|
|
1393
|
-
if (packageJson.workspaces) {
|
|
1394
|
-
return dir;
|
|
1395
|
-
}
|
|
1396
|
-
} catch {
|
|
1397
|
-
// 파싱 실패시 무시
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// 3. .agents/: agents init이 생성한 디렉토리. 서브패키지에는 존재하지 않음.
|
|
1402
|
-
if (await exists(path.join(dir, ".agents"))) {
|
|
1403
|
-
return dir;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
dir = path.dirname(dir);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// 찾지 못하면 api 폴더의 부모 사용
|
|
1410
|
-
return findAppRootPath();
|
|
1411
|
-
}
|