anpm-io 1.0.0

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 (139) hide show
  1. package/README.md +174 -0
  2. package/dist/commands/access.d.ts +2 -0
  3. package/dist/commands/access.js +90 -0
  4. package/dist/commands/adopt.d.ts +2 -0
  5. package/dist/commands/adopt.js +177 -0
  6. package/dist/commands/changelog.d.ts +2 -0
  7. package/dist/commands/changelog.js +67 -0
  8. package/dist/commands/check-update.d.ts +2 -0
  9. package/dist/commands/check-update.js +76 -0
  10. package/dist/commands/config.d.ts +2 -0
  11. package/dist/commands/config.js +84 -0
  12. package/dist/commands/create.d.ts +2 -0
  13. package/dist/commands/create.js +227 -0
  14. package/dist/commands/deploy-record.d.ts +2 -0
  15. package/dist/commands/deploy-record.js +93 -0
  16. package/dist/commands/deploy.d.ts +2 -0
  17. package/dist/commands/deploy.js +284 -0
  18. package/dist/commands/diff.d.ts +2 -0
  19. package/dist/commands/diff.js +92 -0
  20. package/dist/commands/feedback.d.ts +2 -0
  21. package/dist/commands/feedback.js +71 -0
  22. package/dist/commands/grant.d.ts +33 -0
  23. package/dist/commands/grant.js +190 -0
  24. package/dist/commands/hub.d.ts +2 -0
  25. package/dist/commands/hub.js +171 -0
  26. package/dist/commands/init.d.ts +13 -0
  27. package/dist/commands/init.js +172 -0
  28. package/dist/commands/install.d.ts +2 -0
  29. package/dist/commands/install.js +626 -0
  30. package/dist/commands/join.d.ts +6 -0
  31. package/dist/commands/join.js +90 -0
  32. package/dist/commands/link.d.ts +2 -0
  33. package/dist/commands/link.js +112 -0
  34. package/dist/commands/list.d.ts +2 -0
  35. package/dist/commands/list.js +144 -0
  36. package/dist/commands/login.d.ts +7 -0
  37. package/dist/commands/login.js +235 -0
  38. package/dist/commands/orgs.d.ts +10 -0
  39. package/dist/commands/orgs.js +128 -0
  40. package/dist/commands/outdated.d.ts +2 -0
  41. package/dist/commands/outdated.js +70 -0
  42. package/dist/commands/package.d.ts +57 -0
  43. package/dist/commands/package.js +569 -0
  44. package/dist/commands/ping.d.ts +2 -0
  45. package/dist/commands/ping.js +40 -0
  46. package/dist/commands/publish.d.ts +98 -0
  47. package/dist/commands/publish.js +899 -0
  48. package/dist/commands/run.d.ts +2 -0
  49. package/dist/commands/run.js +249 -0
  50. package/dist/commands/search.d.ts +2 -0
  51. package/dist/commands/search.js +57 -0
  52. package/dist/commands/status.d.ts +2 -0
  53. package/dist/commands/status.js +159 -0
  54. package/dist/commands/uninstall.d.ts +2 -0
  55. package/dist/commands/uninstall.js +132 -0
  56. package/dist/commands/update.d.ts +2 -0
  57. package/dist/commands/update.js +171 -0
  58. package/dist/commands/versions.d.ts +2 -0
  59. package/dist/commands/versions.js +44 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +91 -0
  62. package/dist/lib/agent-status.d.ts +23 -0
  63. package/dist/lib/agent-status.js +127 -0
  64. package/dist/lib/ai-tools.d.ts +34 -0
  65. package/dist/lib/ai-tools.js +104 -0
  66. package/dist/lib/anpm-config.d.ts +39 -0
  67. package/dist/lib/anpm-config.js +112 -0
  68. package/dist/lib/api.d.ts +24 -0
  69. package/dist/lib/api.js +151 -0
  70. package/dist/lib/auto-detect.d.ts +30 -0
  71. package/dist/lib/auto-detect.js +112 -0
  72. package/dist/lib/cloud-providers/anthropic.d.ts +19 -0
  73. package/dist/lib/cloud-providers/anthropic.js +200 -0
  74. package/dist/lib/cloud-providers/package-mapper.d.ts +9 -0
  75. package/dist/lib/cloud-providers/package-mapper.js +34 -0
  76. package/dist/lib/cloud-providers/provider.d.ts +60 -0
  77. package/dist/lib/cloud-providers/provider.js +7 -0
  78. package/dist/lib/command-adapter.d.ts +41 -0
  79. package/dist/lib/command-adapter.js +188 -0
  80. package/dist/lib/config.d.ts +50 -0
  81. package/dist/lib/config.js +274 -0
  82. package/dist/lib/contact-format.d.ts +7 -0
  83. package/dist/lib/contact-format.js +23 -0
  84. package/dist/lib/device-hash.d.ts +1 -0
  85. package/dist/lib/device-hash.js +16 -0
  86. package/dist/lib/error-report.d.ts +5 -0
  87. package/dist/lib/error-report.js +28 -0
  88. package/dist/lib/git-installer.d.ts +16 -0
  89. package/dist/lib/git-installer.js +97 -0
  90. package/dist/lib/git-operations.d.ts +38 -0
  91. package/dist/lib/git-operations.js +183 -0
  92. package/dist/lib/hub-notify.d.ts +9 -0
  93. package/dist/lib/hub-notify.js +66 -0
  94. package/dist/lib/install-source.d.ts +33 -0
  95. package/dist/lib/install-source.js +98 -0
  96. package/dist/lib/installer.d.ts +40 -0
  97. package/dist/lib/installer.js +358 -0
  98. package/dist/lib/local-installer.d.ts +15 -0
  99. package/dist/lib/local-installer.js +73 -0
  100. package/dist/lib/lockfile.d.ts +13 -0
  101. package/dist/lib/lockfile.js +42 -0
  102. package/dist/lib/manifest.d.ts +65 -0
  103. package/dist/lib/manifest.js +113 -0
  104. package/dist/lib/migration.d.ts +10 -0
  105. package/dist/lib/migration.js +91 -0
  106. package/dist/lib/paths.d.ts +10 -0
  107. package/dist/lib/paths.js +22 -0
  108. package/dist/lib/preamble.d.ts +22 -0
  109. package/dist/lib/preamble.js +133 -0
  110. package/dist/lib/relay-config.d.ts +13 -0
  111. package/dist/lib/relay-config.js +46 -0
  112. package/dist/lib/requires-suggest.d.ts +23 -0
  113. package/dist/lib/requires-suggest.js +295 -0
  114. package/dist/lib/setup-command.d.ts +6 -0
  115. package/dist/lib/setup-command.js +72 -0
  116. package/dist/lib/slug.d.ts +24 -0
  117. package/dist/lib/slug.js +100 -0
  118. package/dist/lib/step-tracker.d.ts +8 -0
  119. package/dist/lib/step-tracker.js +28 -0
  120. package/dist/lib/storage.d.ts +6 -0
  121. package/dist/lib/storage.js +23 -0
  122. package/dist/lib/update-cache.d.ts +2 -0
  123. package/dist/lib/update-cache.js +51 -0
  124. package/dist/lib/version-check.d.ts +10 -0
  125. package/dist/lib/version-check.js +75 -0
  126. package/dist/mcp/server.d.ts +3 -0
  127. package/dist/mcp/server.js +112 -0
  128. package/dist/postinstall.d.ts +8 -0
  129. package/dist/postinstall.js +41 -0
  130. package/dist/prompts/_error-handling.md +38 -0
  131. package/dist/prompts/create.md +170 -0
  132. package/dist/prompts/explore.md +30 -0
  133. package/dist/prompts/index.d.ts +3 -0
  134. package/dist/prompts/index.js +22 -0
  135. package/dist/relay-compat.d.ts +2 -0
  136. package/dist/relay-compat.js +7 -0
  137. package/dist/types.d.ts +118 -0
  138. package/dist/types.js +2 -0
  139. package/package.json +51 -0
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.computeContentsDiff = computeContentsDiff;
7
+ exports.syncContentsToRelay = syncContentsToRelay;
8
+ exports.findOrphanItems = findOrphanItems;
9
+ exports.resolveRelayDir = resolveRelayDir;
10
+ exports.initGlobalAgentHome = initGlobalAgentHome;
11
+ exports.registerPackage = registerPackage;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const crypto_1 = __importDefault(require("crypto"));
16
+ const js_yaml_1 = __importDefault(require("js-yaml"));
17
+ const ai_tools_js_1 = require("../lib/ai-tools.js");
18
+ const paths_js_1 = require("../lib/paths.js");
19
+ const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
20
+ /** from 또는 path 중 존재하는 값을 반환 */
21
+ function getFromPath(entry) {
22
+ const val = entry.from ?? entry.path;
23
+ if (!val) {
24
+ throw new Error(`contents 항목 "${entry.name}"에 from 또는 path가 필요합니다.`);
25
+ }
26
+ return val;
27
+ }
28
+ // ─── Helpers ───
29
+ function fileHash(filePath) {
30
+ const content = fs_1.default.readFileSync(filePath);
31
+ return crypto_1.default.createHash('md5').update(content).digest('hex');
32
+ }
33
+ /**
34
+ * 소스와 .relay/를 비교하여 diff를 생성한다.
35
+ */
36
+ function computeDiff(sourceFiles, relayFiles) {
37
+ const relayMap = new Map(relayFiles.map((f) => [f.relPath, f.hash]));
38
+ const sourceMap = new Map(sourceFiles.map((f) => [f.relPath, f.hash]));
39
+ const diff = [];
40
+ // 소스에 있는 파일
41
+ for (const [relPath, hash] of sourceMap) {
42
+ const relayHash = relayMap.get(relPath);
43
+ if (!relayHash) {
44
+ diff.push({ relPath, status: 'added' });
45
+ }
46
+ else if (relayHash !== hash) {
47
+ diff.push({ relPath, status: 'modified' });
48
+ }
49
+ else {
50
+ diff.push({ relPath, status: 'unchanged' });
51
+ }
52
+ }
53
+ // .relay/에만 있는 파일 (소스에서 삭제됨)
54
+ for (const [relPath] of relayMap) {
55
+ if (!sourceMap.has(relPath)) {
56
+ diff.push({ relPath, status: 'deleted' });
57
+ }
58
+ }
59
+ return diff.sort((a, b) => a.relPath.localeCompare(b.relPath));
60
+ }
61
+ /**
62
+ * 소스에서 .relay/로 파일을 동기화한다.
63
+ */
64
+ function syncToRelay(sourceBase, relayDir, diff) {
65
+ for (const entry of diff) {
66
+ const sourcePath = path_1.default.join(sourceBase, entry.relPath);
67
+ const relayPath = path_1.default.join(relayDir, entry.relPath);
68
+ if (entry.status === 'added' || entry.status === 'modified') {
69
+ fs_1.default.mkdirSync(path_1.default.dirname(relayPath), { recursive: true });
70
+ fs_1.default.copyFileSync(sourcePath, relayPath);
71
+ }
72
+ else if (entry.status === 'deleted') {
73
+ if (fs_1.default.existsSync(relayPath)) {
74
+ fs_1.default.unlinkSync(relayPath);
75
+ // 빈 디렉토리 정리
76
+ const parentDir = path_1.default.dirname(relayPath);
77
+ try {
78
+ const remaining = fs_1.default.readdirSync(parentDir).filter((f) => !f.startsWith('.'));
79
+ if (remaining.length === 0)
80
+ fs_1.default.rmdirSync(parentDir);
81
+ }
82
+ catch { /* ignore */ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ // ─── Contents-based Helpers ───
88
+ /**
89
+ * from 경로를 절대 경로로 해석한다.
90
+ * ~/로 시작하면 홈 디렉토리, 그 외는 projectPath 기준 상대 경로.
91
+ */
92
+ function resolveFromPath(fromPath, projectPath) {
93
+ if (fromPath.startsWith('~/')) {
94
+ return path_1.default.join(os_1.default.homedir(), fromPath.slice(2));
95
+ }
96
+ return path_1.default.join(projectPath, fromPath);
97
+ }
98
+ /**
99
+ * 파일 또는 디렉토리의 모든 파일을 재귀 스캔하여 FileEntry[]를 반환한다.
100
+ * relPath는 baseDir 기준.
101
+ */
102
+ function scanPath(absPath) {
103
+ if (!fs_1.default.existsSync(absPath))
104
+ return [];
105
+ const stat = fs_1.default.statSync(absPath);
106
+ if (stat.isFile()) {
107
+ return [{ relPath: path_1.default.basename(absPath), hash: fileHash(absPath) }];
108
+ }
109
+ // 디렉토리
110
+ const entries = [];
111
+ function walk(dir) {
112
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
113
+ if (entry.name.startsWith('.'))
114
+ continue;
115
+ const fullPath = path_1.default.join(dir, entry.name);
116
+ if (entry.isDirectory()) {
117
+ walk(fullPath);
118
+ }
119
+ else {
120
+ entries.push({ relPath: path_1.default.relative(absPath, fullPath), hash: fileHash(fullPath) });
121
+ }
122
+ }
123
+ }
124
+ walk(absPath);
125
+ return entries;
126
+ }
127
+ /**
128
+ * contents 매니페스트 기반으로 각 항목의 원본과 .relay/ 복사본을 비교한다.
129
+ */
130
+ function computeContentsDiff(contents, relayDir, projectPath) {
131
+ const diff = [];
132
+ for (const entry of contents) {
133
+ const absFrom = resolveFromPath(getFromPath(entry), projectPath);
134
+ if (!fs_1.default.existsSync(absFrom)) {
135
+ diff.push({ name: entry.name, type: entry.type, status: 'source_missing' });
136
+ continue;
137
+ }
138
+ // from 경로에서 .relay/ 내 대응 위치 결정
139
+ // from: .claude/skills/code-review → .relay/skills/code-review
140
+ // from: ~/.claude/skills/code-review → .relay/skills/code-review
141
+ const relaySubPath = deriveRelaySubPath(entry);
142
+ const relayItemDir = path_1.default.join(relayDir, relaySubPath);
143
+ const sourceFiles = scanPath(absFrom);
144
+ const relayFiles = scanPath(relayItemDir);
145
+ const fileDiff = computeDiff(sourceFiles, relayFiles);
146
+ const hasChanges = fileDiff.some((d) => d.status !== 'unchanged');
147
+ diff.push({
148
+ name: entry.name,
149
+ type: entry.type,
150
+ status: hasChanges ? 'modified' : 'unchanged',
151
+ files: hasChanges ? fileDiff.filter((d) => d.status !== 'unchanged') : undefined,
152
+ });
153
+ }
154
+ // 소스 디렉토리를 다시 스캔하여 contents에 없는 새 항목 탐지
155
+ const newItems = discoverNewItems(contents, projectPath);
156
+ return { diff, newItems };
157
+ }
158
+ /**
159
+ * contents 항목의 from 경로에서 .relay/ 내 서브경로를 유도한다.
160
+ * 예: .claude/skills/code-review → skills/code-review
161
+ * ~/.claude/agents/dev-lead.md → agents/dev-lead.md
162
+ */
163
+ function deriveRelaySubPath(entry) {
164
+ const fromPath = getFromPath(entry);
165
+ const from = fromPath.startsWith('~/') ? fromPath.slice(2) : fromPath;
166
+ // skills/xxx, agents/xxx 등의 패턴을 추출
167
+ for (const dir of SYNC_DIRS) {
168
+ const idx = from.indexOf(`/${dir}/`);
169
+ if (idx !== -1) {
170
+ return from.slice(idx + 1); // /skills/code-review → skills/code-review
171
+ }
172
+ }
173
+ // fallback: type + name
174
+ return `${entry.type}s/${entry.name}`;
175
+ }
176
+ /**
177
+ * contents에 등록되지 않은 새 항목을 소스 디렉토리에서 찾는다.
178
+ */
179
+ function discoverNewItems(contents, projectPath) {
180
+ const existingNames = new Set(contents.map((c) => `${c.type}:${c.name}`));
181
+ const newItems = [];
182
+ // 로컬 소스 스캔
183
+ const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
184
+ for (const tool of localTools) {
185
+ const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
186
+ for (const item of items) {
187
+ if (!existingNames.has(`${item.type}:${item.name}`)) {
188
+ newItems.push({
189
+ name: item.name,
190
+ type: item.type,
191
+ source: tool.skillsDir,
192
+ relativePath: item.relativePath,
193
+ });
194
+ }
195
+ }
196
+ }
197
+ // 글로벌 소스 스캔
198
+ const globalTools = (0, ai_tools_js_1.detectGlobalCLIs)();
199
+ for (const tool of globalTools) {
200
+ const items = (0, ai_tools_js_1.scanGlobalItems)(tool);
201
+ for (const item of items) {
202
+ if (!existingNames.has(`${item.type}:${item.name}`)) {
203
+ newItems.push({
204
+ name: item.name,
205
+ type: item.type,
206
+ source: `~/${tool.skillsDir}`,
207
+ relativePath: item.relativePath,
208
+ });
209
+ }
210
+ }
211
+ }
212
+ return newItems;
213
+ }
214
+ /**
215
+ * contents 항목 단위로 from → .relay/ 동기화한다.
216
+ */
217
+ function syncContentsToRelay(contents, contentsDiff, relayDir, projectPath) {
218
+ const removed = [];
219
+ for (const diffEntry of contentsDiff) {
220
+ const content = contents.find((c) => c.name === diffEntry.name && c.type === diffEntry.type);
221
+ // source_missing: 소스에서 삭제됨 → .relay/에서도 제거
222
+ if (diffEntry.status === 'source_missing') {
223
+ const relaySubPath = content ? deriveRelaySubPath(content) : `${diffEntry.type}s/${diffEntry.name}`;
224
+ const relayTarget = path_1.default.join(relayDir, relaySubPath);
225
+ if (fs_1.default.existsSync(relayTarget)) {
226
+ fs_1.default.rmSync(relayTarget, { recursive: true, force: true });
227
+ removed.push(relaySubPath);
228
+ }
229
+ continue;
230
+ }
231
+ if (diffEntry.status !== 'modified')
232
+ continue;
233
+ if (!content)
234
+ continue;
235
+ const absFrom = resolveFromPath(getFromPath(content), projectPath);
236
+ const relaySubPath = deriveRelaySubPath(content);
237
+ const relayTarget = path_1.default.join(relayDir, relaySubPath);
238
+ // 단일 파일인 경우 직접 복사
239
+ if (fs_1.default.existsSync(absFrom) && fs_1.default.statSync(absFrom).isFile()) {
240
+ fs_1.default.mkdirSync(path_1.default.dirname(relayTarget), { recursive: true });
241
+ fs_1.default.copyFileSync(absFrom, relayTarget);
242
+ continue;
243
+ }
244
+ // 디렉토리인 경우 diff 기반 동기화 (deleted 포함)
245
+ const sourceFiles = scanPath(absFrom);
246
+ const relayFiles = scanPath(relayTarget);
247
+ const fileDiff = computeDiff(sourceFiles, relayFiles);
248
+ syncToRelay(absFrom, relayTarget, fileDiff);
249
+ }
250
+ return { removed };
251
+ }
252
+ /**
253
+ * .relay/ 내에 있지만 contents 매니페스트에 없는 orphan 항목을 찾는다.
254
+ */
255
+ function findOrphanItems(contents, relayDir) {
256
+ const contentPaths = new Set(contents.map((c) => deriveRelaySubPath(c)));
257
+ const orphans = [];
258
+ for (const dir of SYNC_DIRS) {
259
+ const fullDir = path_1.default.join(relayDir, dir);
260
+ if (!fs_1.default.existsSync(fullDir))
261
+ continue;
262
+ for (const entry of fs_1.default.readdirSync(fullDir, { withFileTypes: true })) {
263
+ if (entry.name.startsWith('.'))
264
+ continue;
265
+ const relPath = `${dir}/${entry.name}`;
266
+ if (!contentPaths.has(relPath)) {
267
+ orphans.push(relPath);
268
+ }
269
+ }
270
+ }
271
+ return orphans;
272
+ }
273
+ // ─── Global Agent Home ───
274
+ /**
275
+ * 패키지 홈 디렉토리를 결정한다.
276
+ * 1. 프로젝트에 .relay/가 있으면 → projectPath/.relay/
277
+ * 2. 없으면 → ~/.relay/agents/<slug>/ (slug 필요)
278
+ *
279
+ * slug가 없고 프로젝트에도 .relay/가 없으면 null 반환.
280
+ */
281
+ function resolveRelayDir(projectPath, slug) {
282
+ const projectRelay = path_1.default.join(projectPath, '.relay');
283
+ if (fs_1.default.existsSync(path_1.default.join(projectRelay, 'relay.yaml'))) {
284
+ return projectRelay;
285
+ }
286
+ // .relay/ 디렉토리는 있지만 relay.yaml이 없는 경우도 프로젝트 모드
287
+ if (fs_1.default.existsSync(projectRelay)) {
288
+ return projectRelay;
289
+ }
290
+ // 글로벌 에이전트 홈
291
+ if (slug) {
292
+ return path_1.default.join(os_1.default.homedir(), '.relay', 'agents', slug);
293
+ }
294
+ return null;
295
+ }
296
+ /**
297
+ * 글로벌 에이전트 홈에 패키지 구조를 초기화한다.
298
+ */
299
+ function initGlobalAgentHome(slug, yamlData) {
300
+ const agentDir = path_1.default.join(os_1.default.homedir(), '.relay', 'agents', slug);
301
+ fs_1.default.mkdirSync(agentDir, { recursive: true });
302
+ fs_1.default.mkdirSync(path_1.default.join(agentDir, 'skills'), { recursive: true });
303
+ fs_1.default.mkdirSync(path_1.default.join(agentDir, 'agents'), { recursive: true });
304
+ fs_1.default.writeFileSync(path_1.default.join(agentDir, 'relay.yaml'), js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
305
+ return agentDir;
306
+ }
307
+ // ─── Command ───
308
+ function registerPackage(program) {
309
+ program
310
+ .command('package')
311
+ .description('소스 디렉토리에서 .relay/로 콘텐츠를 동기화합니다')
312
+ .option('--source <dir>', '소스 디렉토리 지정 (예: .claude)')
313
+ .option('--sync', '변경사항을 .relay/에 즉시 반영', false)
314
+ .option('--init', '최초 패키징: 소스 감지 → .relay/ 초기화', false)
315
+ .option('--migrate', '기존 source 필드를 contents로 마이그레이션', false)
316
+ .option('--project <dir>', '프로젝트 루트 경로 (기본: cwd, 환경변수: RELAY_PROJECT_PATH)')
317
+ .option('--home <dir>', '홈 디렉토리 경로 (기본: os.homedir(), 환경변수: RELAY_HOME)')
318
+ .action(async (opts) => {
319
+ const json = program.opts().json ?? false;
320
+ const projectPath = (0, paths_js_1.resolveProjectPath)(opts.project);
321
+ const homeDir = (0, paths_js_1.resolveHome)(opts.home);
322
+ const relayDir = path_1.default.join(projectPath, '.relay');
323
+ const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
324
+ // ─── 최초 패키징 (--init) ───
325
+ if (opts.init || !fs_1.default.existsSync(relayYamlPath)) {
326
+ // 로컬 + 글로벌 소스를 모두 스캔하여 개별 항목 목록 생성
327
+ const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
328
+ const globalTools = (0, ai_tools_js_1.detectGlobalCLIs)(homeDir);
329
+ const sources = [];
330
+ for (const tool of localTools) {
331
+ const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
332
+ if (items.length > 0) {
333
+ sources.push({
334
+ path: tool.skillsDir,
335
+ location: 'local',
336
+ name: tool.name,
337
+ items,
338
+ });
339
+ }
340
+ }
341
+ for (const tool of globalTools) {
342
+ const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
343
+ if (items.length > 0) {
344
+ sources.push({
345
+ path: `~/${tool.skillsDir}`,
346
+ location: 'global',
347
+ name: `${tool.name} (global)`,
348
+ items,
349
+ });
350
+ }
351
+ }
352
+ // ~/.relay/agents/ 에 기존 에이전트 패키지가 있는지 스캔
353
+ const globalAgentsDir = path_1.default.join(homeDir ?? os_1.default.homedir(), '.relay', 'agents');
354
+ const existingAgents = [];
355
+ if (fs_1.default.existsSync(globalAgentsDir)) {
356
+ for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
357
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
358
+ continue;
359
+ const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
360
+ if (fs_1.default.existsSync(agentYaml)) {
361
+ try {
362
+ const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
363
+ existingAgents.push({
364
+ slug: cfg.slug ?? entry.name,
365
+ name: cfg.name ?? entry.name,
366
+ version: cfg.version ?? '0.0.0',
367
+ path: `~/.relay/agents/${entry.name}`,
368
+ });
369
+ }
370
+ catch { /* skip invalid yaml */ }
371
+ }
372
+ }
373
+ }
374
+ if (json) {
375
+ console.log(JSON.stringify({
376
+ status: 'init_required',
377
+ sources,
378
+ existing_agents: existingAgents,
379
+ }));
380
+ }
381
+ else {
382
+ if (sources.length === 0 && existingAgents.length === 0) {
383
+ console.error('배포 가능한 에이전트 콘텐츠를 찾지 못했습니다.');
384
+ console.error('skills/, commands/, agents/, rules/ 중 하나를 만들어주세요.');
385
+ process.exit(1);
386
+ }
387
+ if (sources.length > 0) {
388
+ console.error('\n발견된 에이전트 콘텐츠:\n');
389
+ for (const src of sources) {
390
+ const typeCounts = new Map();
391
+ for (const item of src.items) {
392
+ typeCounts.set(item.type, (typeCounts.get(item.type) ?? 0) + 1);
393
+ }
394
+ const parts = Array.from(typeCounts.entries())
395
+ .map(([t, c]) => `${t} ${c}개`)
396
+ .join(', ');
397
+ const label = src.location === 'global' ? '🌐' : '📁';
398
+ console.error(` ${label} ${src.path}/ — ${parts}`);
399
+ }
400
+ }
401
+ if (existingAgents.length > 0) {
402
+ console.error('\n기존 글로벌 에이전트:\n');
403
+ for (const agent of existingAgents) {
404
+ console.error(` 📦 ${agent.name} (v${agent.version}) — ${agent.path}`);
405
+ }
406
+ }
407
+ console.error('');
408
+ }
409
+ return;
410
+ }
411
+ // ─── 마이그레이션 (--migrate) ───
412
+ if (opts.migrate) {
413
+ const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
414
+ const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
415
+ if (cfgMigrate.contents) {
416
+ if (json) {
417
+ console.log(JSON.stringify({ status: 'already_migrated', message: '이미 contents 형식입니다.' }));
418
+ }
419
+ else {
420
+ console.error('✓ 이미 contents 형식입니다.');
421
+ }
422
+ return;
423
+ }
424
+ const legacySource = cfgMigrate.source;
425
+ if (!legacySource) {
426
+ if (json) {
427
+ console.log(JSON.stringify({ status: 'no_source', message: 'source 필드가 없습니다.' }));
428
+ }
429
+ else {
430
+ console.error('source 필드가 없습니다. anpm package --init으로 초기화하세요.');
431
+ }
432
+ process.exit(1);
433
+ }
434
+ // source 디렉토리를 스캔하여 모든 항목을 contents[]로 변환
435
+ const sourceBase = path_1.default.join(projectPath, legacySource);
436
+ const migratedContents = [];
437
+ if (fs_1.default.existsSync(sourceBase)) {
438
+ const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
439
+ const tool = localTools.find((t) => t.skillsDir === legacySource);
440
+ if (tool) {
441
+ const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
442
+ for (const item of items) {
443
+ migratedContents.push({
444
+ name: item.name,
445
+ type: item.type,
446
+ from: `${legacySource}/${item.relativePath}`,
447
+ });
448
+ }
449
+ }
450
+ }
451
+ // relay.yaml에서 source 제거, contents 저장
452
+ delete cfgMigrate.source;
453
+ cfgMigrate.contents = migratedContents;
454
+ fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
455
+ if (json) {
456
+ console.log(JSON.stringify({ status: 'migrated', contents: migratedContents }));
457
+ }
458
+ else {
459
+ console.error(`✓ source(${legacySource}) → contents(${migratedContents.length}개 항목)로 마이그레이션 완료`);
460
+ }
461
+ return;
462
+ }
463
+ // ─── 재패키징 (contents 매니페스트 기반 동기화) ───
464
+ const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
465
+ const config = js_yaml_1.default.load(yamlContent);
466
+ const rawContents = config.contents;
467
+ const contents = Array.isArray(rawContents) ? rawContents : [];
468
+ // 기존 source 필드 → contents 마이그레이션 안내
469
+ if (!config.contents && config.source) {
470
+ const legacySource = config.source;
471
+ if (json) {
472
+ console.log(JSON.stringify({
473
+ status: 'migration_required',
474
+ message: `relay.yaml의 source 필드를 contents로 마이그레이션해야 합니다.`,
475
+ legacy_source: legacySource,
476
+ }));
477
+ }
478
+ else {
479
+ console.error(`relay.yaml에 기존 source 필드(${legacySource})가 있습니다.`);
480
+ console.error(`contents 형식으로 마이그레이션하려면: anpm package --migrate`);
481
+ }
482
+ process.exit(1);
483
+ }
484
+ if (contents.length === 0) {
485
+ if (json) {
486
+ console.log(JSON.stringify({
487
+ status: 'no_contents',
488
+ message: 'relay.yaml에 contents가 없습니다. anpm package --init으로 패키지를 초기화하세요.',
489
+ }));
490
+ }
491
+ else {
492
+ console.error('relay.yaml에 contents가 없습니다.');
493
+ console.error('anpm package --init으로 패키지를 초기화하세요.');
494
+ }
495
+ process.exit(1);
496
+ }
497
+ // contents 기반 diff 계산
498
+ const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
499
+ const orphans = findOrphanItems(contents, relayDir);
500
+ const summary = {
501
+ modified: contentsDiff.filter((d) => d.status === 'modified').length,
502
+ unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
503
+ source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
504
+ new_available: newItems.length,
505
+ orphaned: orphans.length,
506
+ };
507
+ const hasChanges = summary.modified > 0 || summary.source_missing > 0 || summary.orphaned > 0;
508
+ // --sync: contents 단위 동기화 + orphan 정리
509
+ if (opts.sync && hasChanges) {
510
+ const { removed } = syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
511
+ // orphan 항목 삭제
512
+ for (const orphan of orphans) {
513
+ const orphanPath = path_1.default.join(relayDir, orphan);
514
+ if (fs_1.default.existsSync(orphanPath)) {
515
+ fs_1.default.rmSync(orphanPath, { recursive: true, force: true });
516
+ removed.push(orphan);
517
+ }
518
+ }
519
+ }
520
+ const result = {
521
+ diff: contentsDiff.filter((d) => d.status !== 'unchanged'),
522
+ new_items: newItems,
523
+ orphans,
524
+ synced: opts.sync === true && hasChanges,
525
+ summary,
526
+ };
527
+ if (json) {
528
+ console.log(JSON.stringify(result));
529
+ }
530
+ else {
531
+ if (!hasChanges && newItems.length === 0 && summary.source_missing === 0) {
532
+ console.error('✓ 모든 콘텐츠가 동기화 상태입니다.');
533
+ return;
534
+ }
535
+ console.error('\n📦 콘텐츠 동기화 상태\n');
536
+ for (const entry of contentsDiff) {
537
+ if (entry.status === 'unchanged')
538
+ continue;
539
+ const icon = entry.status === 'modified' ? ' 변경' : ' ⚠ 원본 없음';
540
+ console.error(`${icon}: ${entry.name} (${entry.type})`);
541
+ if (entry.files) {
542
+ for (const f of entry.files) {
543
+ console.error(` ${f.status}: ${f.relPath}`);
544
+ }
545
+ }
546
+ }
547
+ if (orphans.length > 0) {
548
+ console.error('\n \x1b[33m.relay/에만 존재 (소스에서 삭제됨):\x1b[0m');
549
+ for (const orphan of orphans) {
550
+ console.error(` \x1b[31m✗ ${orphan}\x1b[0m`);
551
+ }
552
+ }
553
+ if (newItems.length > 0) {
554
+ console.error('\n 새로 발견된 콘텐츠:');
555
+ for (const item of newItems) {
556
+ console.error(` + ${item.name} (${item.type}) — ${item.source}`);
557
+ }
558
+ }
559
+ console.error('');
560
+ console.error(` 합계: 변경 ${summary.modified}, 유지 ${summary.unchanged}, 원본 없음 ${summary.source_missing}, 신규 ${summary.new_available}, 고아 ${summary.orphaned}`);
561
+ if (opts.sync) {
562
+ console.error('\n✓ .relay/에 반영 완료');
563
+ }
564
+ else if (hasChanges) {
565
+ console.error('\n반영하려면: anpm package --sync');
566
+ }
567
+ }
568
+ });
569
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPing(program: Command): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerPing = registerPing;
4
+ const api_js_1 = require("../lib/api.js");
5
+ const config_js_1 = require("../lib/config.js");
6
+ const slug_js_1 = require("../lib/slug.js");
7
+ function registerPing(program) {
8
+ program
9
+ .command('ping <slug>')
10
+ .description('사용 현황을 기록합니다 (preamble용 경량 명령)')
11
+ .option('--quiet', '출력 없이 실행')
12
+ .action(async (slugInput, opts) => {
13
+ // Resolve slug
14
+ let slug;
15
+ if ((0, slug_js_1.isScopedSlug)(slugInput)) {
16
+ slug = slugInput;
17
+ }
18
+ else {
19
+ const localRegistry = (0, config_js_1.loadInstalled)();
20
+ const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
21
+ const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
22
+ const match = allKeys.find((key) => {
23
+ const parsed = (0, slug_js_1.parseSlug)(key);
24
+ return parsed && parsed.name === slugInput;
25
+ });
26
+ slug = match ?? slugInput;
27
+ }
28
+ // Resolve version and agent_id from installed registry
29
+ const local = (0, config_js_1.loadInstalled)();
30
+ const global = (0, config_js_1.loadGlobalInstalled)();
31
+ const entry = local[slug] ?? global[slug];
32
+ const version = entry?.version;
33
+ const agentId = entry?.agent_id;
34
+ // Fire-and-forget ping (agent_id 없어도 slug fallback으로 전송)
35
+ await (0, api_js_1.sendUsagePing)(agentId ?? null, slug, version);
36
+ if (!opts.quiet) {
37
+ console.log(`RELAY_READY: ${slug}`);
38
+ }
39
+ });
40
+ }