@kaitranntt/ccs 7.77.1-dev.12 → 7.77.1-dev.13
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/web-server/shared-routes-collections.d.ts +13 -0
- package/dist/web-server/shared-routes-collections.d.ts.map +1 -0
- package/dist/web-server/shared-routes-collections.js +170 -0
- package/dist/web-server/shared-routes-collections.js.map +1 -0
- package/dist/web-server/shared-routes-content.d.ts +16 -0
- package/dist/web-server/shared-routes-content.d.ts.map +1 -0
- package/dist/web-server/shared-routes-content.js +152 -0
- package/dist/web-server/shared-routes-content.js.map +1 -0
- package/dist/web-server/shared-routes-markdown-walker.d.ts +12 -0
- package/dist/web-server/shared-routes-markdown-walker.d.ts.map +1 -0
- package/dist/web-server/shared-routes-markdown-walker.js +97 -0
- package/dist/web-server/shared-routes-markdown-walker.js.map +1 -0
- package/dist/web-server/shared-routes-markdown.d.ts +18 -0
- package/dist/web-server/shared-routes-markdown.d.ts.map +1 -0
- package/dist/web-server/shared-routes-markdown.js +167 -0
- package/dist/web-server/shared-routes-markdown.js.map +1 -0
- package/dist/web-server/shared-routes-path-guards.d.ts +16 -0
- package/dist/web-server/shared-routes-path-guards.d.ts.map +1 -0
- package/dist/web-server/shared-routes-path-guards.js +93 -0
- package/dist/web-server/shared-routes-path-guards.js.map +1 -0
- package/dist/web-server/shared-routes-plugin-registry-content.d.ts +12 -0
- package/dist/web-server/shared-routes-plugin-registry-content.d.ts.map +1 -0
- package/dist/web-server/shared-routes-plugin-registry-content.js +93 -0
- package/dist/web-server/shared-routes-plugin-registry-content.js.map +1 -0
- package/dist/web-server/shared-routes-plugins.d.ts +21 -0
- package/dist/web-server/shared-routes-plugins.d.ts.map +1 -0
- package/dist/web-server/shared-routes-plugins.js +197 -0
- package/dist/web-server/shared-routes-plugins.js.map +1 -0
- package/dist/web-server/shared-routes-symlink-status.d.ts +27 -0
- package/dist/web-server/shared-routes-symlink-status.d.ts.map +1 -0
- package/dist/web-server/shared-routes-symlink-status.js +135 -0
- package/dist/web-server/shared-routes-symlink-status.js.map +1 -0
- package/dist/web-server/shared-routes-types.d.ts +23 -0
- package/dist/web-server/shared-routes-types.d.ts.map +1 -0
- package/dist/web-server/shared-routes-types.js +15 -0
- package/dist/web-server/shared-routes-types.js.map +1 -0
- package/dist/web-server/shared-routes.d.ts +1 -0
- package/dist/web-server/shared-routes.d.ts.map +1 -1
- package/dist/web-server/shared-routes.js +30 -730
- package/dist/web-server/shared-routes.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared Data Routes (Phase 07)
|
|
4
4
|
*
|
|
5
|
+
* Thin Express router — delegates all logic to focused sub-modules.
|
|
5
6
|
* API routes for commands, skills, agents, and plugins from ~/.ccs/shared/
|
|
6
7
|
*/
|
|
7
8
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -32,50 +33,50 @@ exports.sharedRoutes = void 0;
|
|
|
32
33
|
const express_1 = require("express");
|
|
33
34
|
const fs = __importStar(require("fs"));
|
|
34
35
|
const path = __importStar(require("path"));
|
|
35
|
-
const yaml = __importStar(require("js-yaml"));
|
|
36
|
-
const claude_config_path_1 = require("../utils/claude-config-path");
|
|
37
|
-
const auth_middleware_1 = require("./middleware/auth-middleware");
|
|
38
36
|
const config_loader_facade_1 = require("../config/config-loader-facade");
|
|
37
|
+
const auth_middleware_1 = require("./middleware/auth-middleware");
|
|
38
|
+
const shared_routes_types_1 = require("./shared-routes-types");
|
|
39
|
+
const shared_routes_path_guards_1 = require("./shared-routes-path-guards");
|
|
40
|
+
const shared_routes_collections_1 = require("./shared-routes-collections");
|
|
41
|
+
const shared_routes_content_1 = require("./shared-routes-content");
|
|
42
|
+
const shared_routes_symlink_status_1 = require("./shared-routes-symlink-status");
|
|
43
|
+
/** Strips extended fields so the summary endpoint stays wire-compatible. */
|
|
44
|
+
function symlinkStatusForSummary() {
|
|
45
|
+
const { valid, message } = (0, shared_routes_symlink_status_1.checkSymlinkStatus)();
|
|
46
|
+
return { valid, message };
|
|
47
|
+
}
|
|
39
48
|
exports.sharedRoutes = (0, express_1.Router)();
|
|
40
49
|
exports.sharedRoutes.use((req, res, next) => {
|
|
41
50
|
if ((0, auth_middleware_1.requireLocalAccessWhenAuthDisabled)(req, res, 'Shared-content endpoints require localhost access when dashboard auth is disabled.')) {
|
|
42
51
|
next();
|
|
43
52
|
}
|
|
44
53
|
});
|
|
45
|
-
const MAX_DIRECTORY_TRAVERSAL_DEPTH = 10;
|
|
46
|
-
const MAX_DESCRIPTION_LENGTH = 140;
|
|
47
|
-
const MAX_MARKDOWN_FILE_BYTES = 1024 * 1024; // 1 MiB
|
|
48
|
-
const MAX_CONTENT_FILE_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
49
|
-
const SHARED_ITEMS_CACHE_TTL_MS = 1000;
|
|
50
|
-
const PLUGIN_REGISTRY_PATH_PREFIX = 'plugin-registry:';
|
|
51
|
-
const PLUGIN_INFRASTRUCTURE_DIRECTORIES = new Set(['cache', 'data', 'marketplaces']);
|
|
52
|
-
const sharedItemsCache = new Map();
|
|
53
54
|
/**
|
|
54
55
|
* GET /api/shared/commands
|
|
55
56
|
*/
|
|
56
57
|
exports.sharedRoutes.get('/commands', (_req, res) => {
|
|
57
|
-
const items = getSharedItems('commands');
|
|
58
|
+
const items = (0, shared_routes_collections_1.getSharedItems)('commands');
|
|
58
59
|
res.json({ items });
|
|
59
60
|
});
|
|
60
61
|
/**
|
|
61
62
|
* GET /api/shared/skills
|
|
62
63
|
*/
|
|
63
64
|
exports.sharedRoutes.get('/skills', (_req, res) => {
|
|
64
|
-
const items = getSharedItems('skills');
|
|
65
|
+
const items = (0, shared_routes_collections_1.getSharedItems)('skills');
|
|
65
66
|
res.json({ items });
|
|
66
67
|
});
|
|
67
68
|
/**
|
|
68
69
|
* GET /api/shared/agents
|
|
69
70
|
*/
|
|
70
71
|
exports.sharedRoutes.get('/agents', (_req, res) => {
|
|
71
|
-
const items = getSharedItems('agents');
|
|
72
|
+
const items = (0, shared_routes_collections_1.getSharedItems)('agents');
|
|
72
73
|
res.json({ items });
|
|
73
74
|
});
|
|
74
75
|
/**
|
|
75
76
|
* GET /api/shared/plugins
|
|
76
77
|
*/
|
|
77
78
|
exports.sharedRoutes.get('/plugins', (_req, res) => {
|
|
78
|
-
const items = getSharedItems('plugins');
|
|
79
|
+
const items = (0, shared_routes_collections_1.getSharedItems)('plugins');
|
|
79
80
|
res.json({ items });
|
|
80
81
|
});
|
|
81
82
|
/**
|
|
@@ -84,7 +85,7 @@ exports.sharedRoutes.get('/plugins', (_req, res) => {
|
|
|
84
85
|
exports.sharedRoutes.get('/content', (req, res) => {
|
|
85
86
|
const typeParam = req.query.type;
|
|
86
87
|
const itemPathParam = req.query.path;
|
|
87
|
-
if (!isSharedContentType(typeParam)) {
|
|
88
|
+
if (!(0, shared_routes_types_1.isSharedContentType)(typeParam)) {
|
|
88
89
|
res.status(400).json({ error: 'Invalid or missing type parameter' });
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
@@ -98,11 +99,11 @@ exports.sharedRoutes.get('/content', (req, res) => {
|
|
|
98
99
|
res.status(404).json({ error: 'Shared directory not found' });
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
|
-
const sharedDirRoot = safeRealPath(sharedDir) ?? path.resolve(sharedDir);
|
|
102
|
+
const sharedDirRoot = (0, shared_routes_path_guards_1.safeRealPath)(sharedDir) ?? path.resolve(sharedDir);
|
|
102
103
|
const allowedRoots = typeParam === 'settings'
|
|
103
|
-
? resolveSettingsAllowedRoots(ccsDir, sharedDirRoot)
|
|
104
|
-
: resolveAllowedRoots(typeParam, ccsDir, sharedDirRoot);
|
|
105
|
-
const contentResult = getSharedItemContent(typeParam, itemPathParam, allowedRoots, sharedDirRoot);
|
|
104
|
+
? (0, shared_routes_path_guards_1.resolveSettingsAllowedRoots)(ccsDir, sharedDirRoot)
|
|
105
|
+
: (0, shared_routes_path_guards_1.resolveAllowedRoots)(typeParam, ccsDir, sharedDirRoot);
|
|
106
|
+
const contentResult = (0, shared_routes_content_1.getSharedItemContent)(typeParam, itemPathParam, allowedRoots, sharedDirRoot);
|
|
106
107
|
if (!contentResult) {
|
|
107
108
|
res.status(404).json({ error: 'Shared content not found' });
|
|
108
109
|
return;
|
|
@@ -113,20 +114,20 @@ exports.sharedRoutes.get('/content', (req, res) => {
|
|
|
113
114
|
* GET /api/shared/summary
|
|
114
115
|
*/
|
|
115
116
|
exports.sharedRoutes.get('/summary', (_req, res) => {
|
|
116
|
-
const commands = getSharedItems('commands').length;
|
|
117
|
-
const skills = getSharedItems('skills').length;
|
|
118
|
-
const agents = getSharedItems('agents').length;
|
|
119
|
-
const plugins = getSharedItems('plugins').length;
|
|
117
|
+
const commands = (0, shared_routes_collections_1.getSharedItems)('commands').length;
|
|
118
|
+
const skills = (0, shared_routes_collections_1.getSharedItems)('skills').length;
|
|
119
|
+
const agents = (0, shared_routes_collections_1.getSharedItems)('agents').length;
|
|
120
|
+
const plugins = (0, shared_routes_collections_1.getSharedItems)('plugins').length;
|
|
120
121
|
const ccsDir = (0, config_loader_facade_1.getCcsDir)();
|
|
121
122
|
const settingsPath = path.join(ccsDir, 'shared', 'settings.json');
|
|
122
|
-
const sharedDirRoot = safeRealPath(path.join(ccsDir, 'shared'));
|
|
123
|
-
const resolvedSettingsPath = safeRealPath(settingsPath);
|
|
123
|
+
const sharedDirRoot = (0, shared_routes_path_guards_1.safeRealPath)(path.join(ccsDir, 'shared'));
|
|
124
|
+
const resolvedSettingsPath = (0, shared_routes_path_guards_1.safeRealPath)(settingsPath);
|
|
124
125
|
const settingsAllowedRoots = sharedDirRoot
|
|
125
|
-
? resolveSettingsAllowedRoots(ccsDir, sharedDirRoot)
|
|
126
|
+
? (0, shared_routes_path_guards_1.resolveSettingsAllowedRoots)(ccsDir, sharedDirRoot)
|
|
126
127
|
: new Set();
|
|
127
128
|
const hasSettings = Boolean(sharedDirRoot) &&
|
|
128
129
|
Boolean(resolvedSettingsPath) &&
|
|
129
|
-
isPathWithinAny(resolvedSettingsPath, settingsAllowedRoots);
|
|
130
|
+
(0, shared_routes_path_guards_1.isPathWithinAny)(resolvedSettingsPath, settingsAllowedRoots);
|
|
130
131
|
res.json({
|
|
131
132
|
commands,
|
|
132
133
|
skills,
|
|
@@ -134,708 +135,7 @@ exports.sharedRoutes.get('/summary', (_req, res) => {
|
|
|
134
135
|
plugins,
|
|
135
136
|
settings: hasSettings,
|
|
136
137
|
total: commands + skills + agents + plugins + (hasSettings ? 1 : 0),
|
|
137
|
-
symlinkStatus:
|
|
138
|
+
symlinkStatus: symlinkStatusForSummary(),
|
|
138
139
|
});
|
|
139
140
|
});
|
|
140
|
-
function isSharedCollectionType(value) {
|
|
141
|
-
return value === 'commands' || value === 'skills' || value === 'agents' || value === 'plugins';
|
|
142
|
-
}
|
|
143
|
-
function isSharedContentType(value) {
|
|
144
|
-
return isSharedCollectionType(value) || value === 'settings';
|
|
145
|
-
}
|
|
146
|
-
function resolveAllowedRoots(type, ccsDir, sharedDirRoot) {
|
|
147
|
-
if (type === 'commands' || type === 'plugins') {
|
|
148
|
-
return new Set([sharedDirRoot]);
|
|
149
|
-
}
|
|
150
|
-
return new Set([
|
|
151
|
-
sharedDirRoot,
|
|
152
|
-
...[
|
|
153
|
-
path.join((0, claude_config_path_1.getClaudeConfigDir)(), type),
|
|
154
|
-
path.join(ccsDir, '.claude', type),
|
|
155
|
-
path.join(ccsDir, 'shared', type),
|
|
156
|
-
]
|
|
157
|
-
.map((dirPath) => safeRealPath(dirPath))
|
|
158
|
-
.filter((dirPath) => typeof dirPath === 'string'),
|
|
159
|
-
]);
|
|
160
|
-
}
|
|
161
|
-
function resolveSettingsAllowedRoots(ccsDir, sharedDirRoot) {
|
|
162
|
-
const claudeConfigDir = (0, claude_config_path_1.getClaudeConfigDir)();
|
|
163
|
-
return new Set([
|
|
164
|
-
sharedDirRoot,
|
|
165
|
-
safeRealPath(path.join(claudeConfigDir, 'settings.json')),
|
|
166
|
-
safeRealPath(path.join(ccsDir, '.claude', 'settings.json')),
|
|
167
|
-
].filter((dirPath) => typeof dirPath === 'string'));
|
|
168
|
-
}
|
|
169
|
-
function getSharedItems(type) {
|
|
170
|
-
const ccsDir = (0, config_loader_facade_1.getCcsDir)();
|
|
171
|
-
const sharedDir = path.join(ccsDir, 'shared', type);
|
|
172
|
-
const now = Date.now();
|
|
173
|
-
if (!fs.existsSync(sharedDir)) {
|
|
174
|
-
sharedItemsCache.delete(type);
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
const cached = sharedItemsCache.get(type);
|
|
178
|
-
if (cached && cached.sharedDir === sharedDir && cached.expiresAt > now) {
|
|
179
|
-
return cached.items;
|
|
180
|
-
}
|
|
181
|
-
const items = [];
|
|
182
|
-
const sharedDirRoot = safeRealPath(sharedDir) ?? path.resolve(sharedDir);
|
|
183
|
-
const allowedRoots = resolveAllowedRoots(type, ccsDir, sharedDirRoot);
|
|
184
|
-
if (type === 'commands') {
|
|
185
|
-
const commandItems = getCommandItems(sharedDir, allowedRoots);
|
|
186
|
-
sharedItemsCache.set(type, {
|
|
187
|
-
items: commandItems,
|
|
188
|
-
sharedDir,
|
|
189
|
-
expiresAt: now + SHARED_ITEMS_CACHE_TTL_MS,
|
|
190
|
-
});
|
|
191
|
-
return commandItems;
|
|
192
|
-
}
|
|
193
|
-
if (type === 'plugins') {
|
|
194
|
-
const pluginItems = getPluginItems(sharedDir, allowedRoots);
|
|
195
|
-
sharedItemsCache.set(type, {
|
|
196
|
-
items: pluginItems,
|
|
197
|
-
sharedDir,
|
|
198
|
-
expiresAt: now + SHARED_ITEMS_CACHE_TTL_MS,
|
|
199
|
-
});
|
|
200
|
-
return pluginItems;
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
const entries = fs.readdirSync(sharedDir, { withFileTypes: true });
|
|
204
|
-
for (const entry of entries) {
|
|
205
|
-
try {
|
|
206
|
-
const entryPath = path.join(sharedDir, entry.name);
|
|
207
|
-
const resolvedEntryPath = safeRealPath(entryPath);
|
|
208
|
-
if (!resolvedEntryPath || !isPathWithinAny(resolvedEntryPath, allowedRoots)) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const stats = fs.statSync(resolvedEntryPath);
|
|
212
|
-
const item = getSkillOrAgentItem(type, entry, entryPath, resolvedEntryPath, allowedRoots, stats);
|
|
213
|
-
if (!item) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
items.push(item);
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
// Fail soft per entry so one bad item does not hide valid results.
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
// Directory read failed
|
|
225
|
-
}
|
|
226
|
-
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
|
227
|
-
sharedItemsCache.set(type, {
|
|
228
|
-
items: sortedItems,
|
|
229
|
-
sharedDir,
|
|
230
|
-
expiresAt: now + SHARED_ITEMS_CACHE_TTL_MS,
|
|
231
|
-
});
|
|
232
|
-
return sortedItems;
|
|
233
|
-
}
|
|
234
|
-
function getPluginItems(sharedDir, allowedRoots) {
|
|
235
|
-
const registryItems = getPluginRegistryItems(sharedDir, allowedRoots);
|
|
236
|
-
const legacyDirectoryItems = getLegacyPluginDirectoryItems(sharedDir, allowedRoots);
|
|
237
|
-
const itemsByPath = new Map();
|
|
238
|
-
for (const item of [...registryItems, ...legacyDirectoryItems]) {
|
|
239
|
-
itemsByPath.set(item.path, item);
|
|
240
|
-
}
|
|
241
|
-
return [...itemsByPath.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
242
|
-
}
|
|
243
|
-
function getPluginRegistryItems(sharedDir, allowedRoots) {
|
|
244
|
-
const registry = readPluginInstalledRegistry(sharedDir, allowedRoots);
|
|
245
|
-
if (!registry) {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
return Object.entries(registry.plugins).map(([pluginName, metadata]) => ({
|
|
249
|
-
name: pluginName,
|
|
250
|
-
description: describeInstalledPlugin(pluginName, metadata),
|
|
251
|
-
path: encodePluginRegistryPath(pluginName),
|
|
252
|
-
type: 'plugin',
|
|
253
|
-
}));
|
|
254
|
-
}
|
|
255
|
-
function getLegacyPluginDirectoryItems(sharedDir, allowedRoots) {
|
|
256
|
-
let entries = [];
|
|
257
|
-
try {
|
|
258
|
-
entries = fs.readdirSync(sharedDir, { withFileTypes: true });
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
return [];
|
|
262
|
-
}
|
|
263
|
-
const items = [];
|
|
264
|
-
for (const entry of entries) {
|
|
265
|
-
if (!entry.isDirectory() || PLUGIN_INFRASTRUCTURE_DIRECTORIES.has(entry.name)) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
const entryPath = path.join(sharedDir, entry.name);
|
|
269
|
-
const resolvedEntryPath = safeRealPath(entryPath);
|
|
270
|
-
if (!resolvedEntryPath || !isPathWithinAny(resolvedEntryPath, allowedRoots)) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const description = readFirstMarkdownDescription([
|
|
274
|
-
path.join(resolvedEntryPath, 'README.md'),
|
|
275
|
-
path.join(resolvedEntryPath, 'readme.md'),
|
|
276
|
-
path.join(resolvedEntryPath, 'PLUGIN.md'),
|
|
277
|
-
], allowedRoots);
|
|
278
|
-
if (!description) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
items.push({
|
|
282
|
-
name: entry.name,
|
|
283
|
-
description,
|
|
284
|
-
path: entryPath,
|
|
285
|
-
type: 'plugin',
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
return items;
|
|
289
|
-
}
|
|
290
|
-
function getCommandItems(sharedDir, allowedRoots) {
|
|
291
|
-
const markdownFiles = collectMarkdownFiles(sharedDir, allowedRoots);
|
|
292
|
-
const items = [];
|
|
293
|
-
for (const markdownFile of markdownFiles) {
|
|
294
|
-
const description = readMarkdownDescription(markdownFile.resolvedPath, allowedRoots);
|
|
295
|
-
if (!description) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const relativePath = path.relative(sharedDir, markdownFile.displayPath);
|
|
299
|
-
const normalizedName = relativePath.split(path.sep).join('/').replace(/\.md$/i, '');
|
|
300
|
-
if (!normalizedName) {
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
items.push({
|
|
304
|
-
name: normalizedName,
|
|
305
|
-
description,
|
|
306
|
-
path: markdownFile.displayPath,
|
|
307
|
-
type: 'command',
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
311
|
-
}
|
|
312
|
-
function getSkillOrAgentItem(type, entry, entryPath, resolvedEntryPath, allowedRoots, stats) {
|
|
313
|
-
if (type === 'skills') {
|
|
314
|
-
if (!stats.isDirectory()) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
const description = readMarkdownDescription(path.join(resolvedEntryPath, 'SKILL.md'), allowedRoots);
|
|
318
|
-
if (!description) {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
name: entry.name,
|
|
323
|
-
description,
|
|
324
|
-
path: entryPath,
|
|
325
|
-
type: 'skill',
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if (stats.isDirectory()) {
|
|
329
|
-
const description = readFirstMarkdownDescription([
|
|
330
|
-
path.join(resolvedEntryPath, 'prompt.md'),
|
|
331
|
-
path.join(resolvedEntryPath, 'AGENT.md'),
|
|
332
|
-
path.join(resolvedEntryPath, 'agent.md'),
|
|
333
|
-
], allowedRoots);
|
|
334
|
-
if (!description) {
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
return {
|
|
338
|
-
name: entry.name,
|
|
339
|
-
description,
|
|
340
|
-
path: entryPath,
|
|
341
|
-
type: 'agent',
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
if (!stats.isFile() || !entry.name.toLowerCase().endsWith('.md')) {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
const description = readMarkdownDescription(resolvedEntryPath, allowedRoots);
|
|
348
|
-
if (!description) {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
name: entry.name.replace(/\.md$/i, ''),
|
|
353
|
-
description,
|
|
354
|
-
path: entryPath,
|
|
355
|
-
type: 'agent',
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function collectMarkdownFiles(sharedDir, allowedRoots) {
|
|
359
|
-
const directoriesToVisit = [
|
|
360
|
-
{ path: sharedDir, depth: 0 },
|
|
361
|
-
];
|
|
362
|
-
const visitedDirectories = new Set();
|
|
363
|
-
const markdownFiles = [];
|
|
364
|
-
while (directoriesToVisit.length > 0) {
|
|
365
|
-
const current = directoriesToVisit.pop();
|
|
366
|
-
if (!current) {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
const currentDir = current.path;
|
|
370
|
-
const resolvedCurrentDir = safeRealPath(currentDir);
|
|
371
|
-
if (!resolvedCurrentDir || !isPathWithinAny(resolvedCurrentDir, allowedRoots)) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const normalizedDirPath = normalizeForPathComparison(resolvedCurrentDir);
|
|
375
|
-
if (visitedDirectories.has(normalizedDirPath)) {
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
visitedDirectories.add(normalizedDirPath);
|
|
379
|
-
let entries = [];
|
|
380
|
-
try {
|
|
381
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
for (const entry of entries) {
|
|
387
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
388
|
-
const resolvedEntryPath = safeRealPath(entryPath);
|
|
389
|
-
if (!resolvedEntryPath || !isPathWithinAny(resolvedEntryPath, allowedRoots)) {
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
let stats;
|
|
393
|
-
try {
|
|
394
|
-
stats = fs.statSync(resolvedEntryPath);
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (stats.isDirectory()) {
|
|
400
|
-
if (current.depth < MAX_DIRECTORY_TRAVERSAL_DEPTH) {
|
|
401
|
-
directoriesToVisit.push({ path: entryPath, depth: current.depth + 1 });
|
|
402
|
-
}
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (stats.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
406
|
-
markdownFiles.push({
|
|
407
|
-
displayPath: entryPath,
|
|
408
|
-
resolvedPath: resolvedEntryPath,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return markdownFiles;
|
|
414
|
-
}
|
|
415
|
-
function extractDescription(content) {
|
|
416
|
-
const frontmatterDescription = extractFrontmatterDescription(content);
|
|
417
|
-
if (frontmatterDescription) {
|
|
418
|
-
return trimDescription(frontmatterDescription);
|
|
419
|
-
}
|
|
420
|
-
// Extract first non-empty, non-heading line from the markdown body.
|
|
421
|
-
const lines = stripFrontmatter(content).split('\n');
|
|
422
|
-
for (const line of lines) {
|
|
423
|
-
const trimmed = line.trim();
|
|
424
|
-
if (isDescriptionBodyLine(trimmed)) {
|
|
425
|
-
return trimDescription(trimmed);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return 'No description';
|
|
429
|
-
}
|
|
430
|
-
function isDescriptionBodyLine(line) {
|
|
431
|
-
if (!line) {
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
if (line === '---' || line === '...') {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
return !line.startsWith('#') && !line.startsWith('<!--');
|
|
438
|
-
}
|
|
439
|
-
function extractFrontmatterDescription(content) {
|
|
440
|
-
const frontmatterMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/);
|
|
441
|
-
if (!frontmatterMatch) {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
try {
|
|
445
|
-
const parsed = yaml.load(frontmatterMatch[1]);
|
|
446
|
-
const description = parsed?.description;
|
|
447
|
-
if (typeof description !== 'string') {
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
const trimmed = description.trim();
|
|
451
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
function stripFrontmatter(content) {
|
|
458
|
-
return content.replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/, '');
|
|
459
|
-
}
|
|
460
|
-
function trimDescription(description) {
|
|
461
|
-
if (description.length <= MAX_DESCRIPTION_LENGTH) {
|
|
462
|
-
return description;
|
|
463
|
-
}
|
|
464
|
-
return `${description.slice(0, MAX_DESCRIPTION_LENGTH - 3).trimEnd()}...`;
|
|
465
|
-
}
|
|
466
|
-
function readFirstMarkdownDescription(markdownPaths, allowedRoots) {
|
|
467
|
-
for (const markdownPath of markdownPaths) {
|
|
468
|
-
const description = readMarkdownDescription(markdownPath, allowedRoots);
|
|
469
|
-
if (description) {
|
|
470
|
-
return description;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
function readMarkdownDescription(markdownPath, allowedRoots) {
|
|
476
|
-
try {
|
|
477
|
-
const resolvedMarkdownPath = safeRealPath(markdownPath);
|
|
478
|
-
if (!resolvedMarkdownPath || !isPathWithinAny(resolvedMarkdownPath, allowedRoots)) {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
const stats = fs.statSync(resolvedMarkdownPath);
|
|
482
|
-
if (!stats.isFile()) {
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
if (stats.size > MAX_MARKDOWN_FILE_BYTES) {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
const content = fs.readFileSync(resolvedMarkdownPath, 'utf8');
|
|
489
|
-
return extractDescription(content);
|
|
490
|
-
}
|
|
491
|
-
catch {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function readMarkdownContent(markdownPath, allowedRoots) {
|
|
496
|
-
try {
|
|
497
|
-
const resolvedMarkdownPath = safeRealPath(markdownPath);
|
|
498
|
-
if (!resolvedMarkdownPath || !isPathWithinAny(resolvedMarkdownPath, allowedRoots)) {
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
const stats = fs.statSync(resolvedMarkdownPath);
|
|
502
|
-
if (!stats.isFile()) {
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
if (stats.size > MAX_CONTENT_FILE_BYTES) {
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
return fs.readFileSync(resolvedMarkdownPath, 'utf8');
|
|
509
|
-
}
|
|
510
|
-
catch {
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
function encodePluginRegistryPath(pluginName) {
|
|
515
|
-
return `${PLUGIN_REGISTRY_PATH_PREFIX}${encodeURIComponent(pluginName)}`;
|
|
516
|
-
}
|
|
517
|
-
function decodePluginRegistryPath(itemPath) {
|
|
518
|
-
if (!itemPath.startsWith(PLUGIN_REGISTRY_PATH_PREFIX)) {
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
const encodedName = itemPath.slice(PLUGIN_REGISTRY_PATH_PREFIX.length);
|
|
523
|
-
const pluginName = decodeURIComponent(encodedName);
|
|
524
|
-
return pluginName.length > 0 ? pluginName : null;
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function readJsonObject(filePath, allowedRoots) {
|
|
531
|
-
const resolvedPath = safeRealPath(filePath);
|
|
532
|
-
if (!resolvedPath || !isPathWithinAny(resolvedPath, allowedRoots)) {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
const stats = fs.statSync(resolvedPath);
|
|
537
|
-
if (!stats.isFile() || stats.size > MAX_CONTENT_FILE_BYTES) {
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
541
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
return parsed;
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
function readPluginInstalledRegistry(sharedDir, allowedRoots) {
|
|
551
|
-
const parsed = readJsonObject(path.join(sharedDir, 'installed_plugins.json'), allowedRoots);
|
|
552
|
-
if (!parsed ||
|
|
553
|
-
!parsed.plugins ||
|
|
554
|
-
typeof parsed.plugins !== 'object' ||
|
|
555
|
-
Array.isArray(parsed.plugins)) {
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
version: typeof parsed.version === 'number' ? parsed.version : undefined,
|
|
560
|
-
plugins: parsed.plugins,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
function describeInstalledPlugin(pluginName, metadata) {
|
|
564
|
-
const recordCount = Array.isArray(metadata) ? metadata.length : 1;
|
|
565
|
-
const marketplace = extractMarketplaceFromPluginName(pluginName);
|
|
566
|
-
const recordLabel = `${recordCount} ${recordCount === 1 ? 'record' : 'records'}`;
|
|
567
|
-
return marketplace
|
|
568
|
-
? `Installed from ${marketplace}; ${recordLabel} in shared registry`
|
|
569
|
-
: `Installed plugin; ${recordLabel} in shared registry`;
|
|
570
|
-
}
|
|
571
|
-
function extractMarketplaceFromPluginName(pluginName) {
|
|
572
|
-
const atIndex = pluginName.lastIndexOf('@');
|
|
573
|
-
if (atIndex <= 0 || atIndex === pluginName.length - 1) {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
return pluginName.slice(atIndex + 1);
|
|
577
|
-
}
|
|
578
|
-
function resolveReadableMarkdownPath(markdownPaths, allowedRoots) {
|
|
579
|
-
for (const markdownPath of markdownPaths) {
|
|
580
|
-
const resolvedMarkdownPath = safeRealPath(markdownPath);
|
|
581
|
-
if (!resolvedMarkdownPath || !isPathWithinAny(resolvedMarkdownPath, allowedRoots)) {
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
const stats = fs.statSync(resolvedMarkdownPath);
|
|
586
|
-
if (!stats.isFile() || stats.size > MAX_CONTENT_FILE_BYTES) {
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
return resolvedMarkdownPath;
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return null;
|
|
596
|
-
}
|
|
597
|
-
function getSharedItemContent(type, itemPath, allowedRoots, sharedDir) {
|
|
598
|
-
if (type === 'settings') {
|
|
599
|
-
return getSharedSettingsContent(itemPath, allowedRoots);
|
|
600
|
-
}
|
|
601
|
-
if (type === 'plugins') {
|
|
602
|
-
const pluginRegistryContent = getPluginRegistryContent(itemPath, sharedDir, allowedRoots);
|
|
603
|
-
if (pluginRegistryContent) {
|
|
604
|
-
return pluginRegistryContent;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
const resolvedItemPath = safeRealPath(itemPath);
|
|
608
|
-
if (!resolvedItemPath || !isPathWithinAny(resolvedItemPath, allowedRoots)) {
|
|
609
|
-
return null;
|
|
610
|
-
}
|
|
611
|
-
let itemStats;
|
|
612
|
-
try {
|
|
613
|
-
itemStats = fs.statSync(resolvedItemPath);
|
|
614
|
-
}
|
|
615
|
-
catch {
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
let markdownPath = null;
|
|
619
|
-
if (type === 'commands') {
|
|
620
|
-
if (!itemStats.isFile() || !itemPath.toLowerCase().endsWith('.md')) {
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
markdownPath = resolvedItemPath;
|
|
624
|
-
}
|
|
625
|
-
else if (type === 'skills') {
|
|
626
|
-
if (!itemStats.isDirectory()) {
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
markdownPath = resolveReadableMarkdownPath([path.join(resolvedItemPath, 'SKILL.md')], allowedRoots);
|
|
630
|
-
}
|
|
631
|
-
else if (type === 'plugins') {
|
|
632
|
-
if (!itemStats.isDirectory() || isPluginInfrastructurePath(resolvedItemPath, sharedDir)) {
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
markdownPath = resolveReadableMarkdownPath([
|
|
636
|
-
path.join(resolvedItemPath, 'README.md'),
|
|
637
|
-
path.join(resolvedItemPath, 'readme.md'),
|
|
638
|
-
path.join(resolvedItemPath, 'PLUGIN.md'),
|
|
639
|
-
], allowedRoots);
|
|
640
|
-
if (!markdownPath) {
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
if (itemStats.isDirectory()) {
|
|
646
|
-
markdownPath = resolveReadableMarkdownPath([
|
|
647
|
-
path.join(resolvedItemPath, 'prompt.md'),
|
|
648
|
-
path.join(resolvedItemPath, 'AGENT.md'),
|
|
649
|
-
path.join(resolvedItemPath, 'agent.md'),
|
|
650
|
-
], allowedRoots);
|
|
651
|
-
}
|
|
652
|
-
else if (itemStats.isFile() && itemPath.toLowerCase().endsWith('.md')) {
|
|
653
|
-
markdownPath = resolvedItemPath;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (!markdownPath) {
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
659
|
-
const content = readMarkdownContent(markdownPath, allowedRoots);
|
|
660
|
-
if (!content) {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
return {
|
|
664
|
-
content,
|
|
665
|
-
contentPath: markdownPath,
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
function isPluginInfrastructurePath(candidatePath, sharedDir) {
|
|
669
|
-
for (const directoryName of PLUGIN_INFRASTRUCTURE_DIRECTORIES) {
|
|
670
|
-
const resolvedInfrastructurePath = safeRealPath(path.join(sharedDir, directoryName));
|
|
671
|
-
if (resolvedInfrastructurePath && isPathWithin(candidatePath, resolvedInfrastructurePath)) {
|
|
672
|
-
return true;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
function getPluginRegistryContent(itemPath, sharedDir, allowedRoots) {
|
|
678
|
-
const pluginName = decodePluginRegistryPath(itemPath);
|
|
679
|
-
if (!pluginName) {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
const registryPath = path.join(sharedDir, 'installed_plugins.json');
|
|
683
|
-
const resolvedRegistryPath = safeRealPath(registryPath);
|
|
684
|
-
if (!resolvedRegistryPath || !isPathWithinAny(resolvedRegistryPath, allowedRoots)) {
|
|
685
|
-
return null;
|
|
686
|
-
}
|
|
687
|
-
const registry = readPluginInstalledRegistry(sharedDir, allowedRoots);
|
|
688
|
-
if (!registry || !Object.prototype.hasOwnProperty.call(registry.plugins, pluginName)) {
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
const metadata = registry.plugins[pluginName];
|
|
692
|
-
const marketplace = extractMarketplaceFromPluginName(pluginName);
|
|
693
|
-
const marketplaceEntry = marketplace
|
|
694
|
-
? readJsonObject(path.join(sharedDir, 'known_marketplaces.json'), allowedRoots)?.[marketplace]
|
|
695
|
-
: null;
|
|
696
|
-
const blocklistEntry = findPluginBlocklistEntry(sharedDir, allowedRoots, pluginName);
|
|
697
|
-
const lines = [
|
|
698
|
-
`# ${pluginName}`,
|
|
699
|
-
'',
|
|
700
|
-
`Registry: \`${resolvedRegistryPath}\``,
|
|
701
|
-
'',
|
|
702
|
-
'## Installed Plugin',
|
|
703
|
-
'',
|
|
704
|
-
`- Source: shared \`installed_plugins.json\` registry`,
|
|
705
|
-
`- Registry version: ${registry.version ?? 'unknown'}`,
|
|
706
|
-
`- Marketplace: ${marketplace ?? 'not recorded in plugin name'}`,
|
|
707
|
-
`- Install records: ${Array.isArray(metadata) ? metadata.length : 1}`,
|
|
708
|
-
];
|
|
709
|
-
if (marketplaceEntry) {
|
|
710
|
-
lines.push('', '## Marketplace Registry Entry', '', '```json', stringifyJson(marketplaceEntry), '```');
|
|
711
|
-
}
|
|
712
|
-
if (blocklistEntry) {
|
|
713
|
-
lines.push('', '## Blocklist Notice', '', '```json', stringifyJson(blocklistEntry), '```');
|
|
714
|
-
}
|
|
715
|
-
lines.push('', '## Plugin Registry Entry', '', '```json', stringifyJson(metadata), '```');
|
|
716
|
-
return {
|
|
717
|
-
content: lines.join('\n'),
|
|
718
|
-
contentPath: resolvedRegistryPath,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
function findPluginBlocklistEntry(sharedDir, allowedRoots, pluginName) {
|
|
722
|
-
const blocklist = readJsonObject(path.join(sharedDir, 'blocklist.json'), allowedRoots);
|
|
723
|
-
const plugins = blocklist?.plugins;
|
|
724
|
-
if (!Array.isArray(plugins)) {
|
|
725
|
-
return null;
|
|
726
|
-
}
|
|
727
|
-
return (plugins.find((entry) => entry &&
|
|
728
|
-
typeof entry === 'object' &&
|
|
729
|
-
'plugin' in entry &&
|
|
730
|
-
entry.plugin === pluginName) ?? null);
|
|
731
|
-
}
|
|
732
|
-
function stringifyJson(value) {
|
|
733
|
-
return JSON.stringify(value, null, 2) ?? 'null';
|
|
734
|
-
}
|
|
735
|
-
function safeRealPath(targetPath) {
|
|
736
|
-
try {
|
|
737
|
-
return fs.realpathSync(targetPath);
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
function getSharedSettingsContent(itemPath, allowedRoots) {
|
|
744
|
-
if (path.basename(itemPath) !== 'settings.json') {
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
const sharedRoot = Array.from(allowedRoots)[0];
|
|
748
|
-
if (!sharedRoot) {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
const settingsPath = path.join(sharedRoot, 'settings.json');
|
|
752
|
-
const resolvedSettingsPath = safeRealPath(settingsPath);
|
|
753
|
-
if (!resolvedSettingsPath || !isPathWithinAny(resolvedSettingsPath, allowedRoots)) {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
let stats;
|
|
757
|
-
try {
|
|
758
|
-
stats = fs.statSync(resolvedSettingsPath);
|
|
759
|
-
}
|
|
760
|
-
catch {
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
if (!stats.isFile() || stats.size > MAX_CONTENT_FILE_BYTES) {
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
try {
|
|
767
|
-
const rawContent = fs.readFileSync(resolvedSettingsPath, 'utf8');
|
|
768
|
-
const parsed = JSON.parse(rawContent);
|
|
769
|
-
return {
|
|
770
|
-
content: JSON.stringify(parsed, null, 2),
|
|
771
|
-
contentPath: resolvedSettingsPath,
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
catch {
|
|
775
|
-
const content = readMarkdownContent(resolvedSettingsPath, allowedRoots);
|
|
776
|
-
return content
|
|
777
|
-
? {
|
|
778
|
-
content,
|
|
779
|
-
contentPath: resolvedSettingsPath,
|
|
780
|
-
}
|
|
781
|
-
: null;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
function isPathWithin(candidatePath, basePath) {
|
|
785
|
-
const normalizedCandidate = normalizeForPathComparison(candidatePath);
|
|
786
|
-
const normalizedBase = normalizeForPathComparison(basePath);
|
|
787
|
-
const relative = path.relative(normalizedBase, normalizedCandidate);
|
|
788
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
789
|
-
}
|
|
790
|
-
function isPathWithinAny(candidatePath, basePaths) {
|
|
791
|
-
for (const basePath of basePaths) {
|
|
792
|
-
if (isPathWithin(candidatePath, basePath)) {
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
function normalizeForPathComparison(targetPath) {
|
|
799
|
-
const normalized = path.resolve(targetPath);
|
|
800
|
-
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
801
|
-
}
|
|
802
|
-
function checkSymlinkStatus() {
|
|
803
|
-
const ccsDir = (0, config_loader_facade_1.getCcsDir)();
|
|
804
|
-
const sharedDir = path.join(ccsDir, 'shared');
|
|
805
|
-
if (!fs.existsSync(sharedDir)) {
|
|
806
|
-
return { valid: false, message: 'Shared directory not found' };
|
|
807
|
-
}
|
|
808
|
-
// Check all three symlinks: commands, skills, agents
|
|
809
|
-
const linkTypes = ['commands', 'skills', 'agents'];
|
|
810
|
-
let validLinks = 0;
|
|
811
|
-
for (const linkType of linkTypes) {
|
|
812
|
-
const linkPath = path.join(sharedDir, linkType);
|
|
813
|
-
try {
|
|
814
|
-
if (fs.existsSync(linkPath)) {
|
|
815
|
-
const stats = fs.lstatSync(linkPath);
|
|
816
|
-
if (stats.isSymbolicLink()) {
|
|
817
|
-
const target = fs.readlinkSync(linkPath);
|
|
818
|
-
// Check if it points to Claude config dir.
|
|
819
|
-
const expectedTarget = path.join((0, claude_config_path_1.getClaudeConfigDir)(), linkType);
|
|
820
|
-
if (path.resolve(path.dirname(linkPath), target) === path.resolve(expectedTarget)) {
|
|
821
|
-
validLinks++;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
catch {
|
|
827
|
-
// Not a symlink or read error
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
if (validLinks === linkTypes.length) {
|
|
831
|
-
return { valid: true, message: 'Symlinks active' };
|
|
832
|
-
}
|
|
833
|
-
else if (validLinks > 0) {
|
|
834
|
-
return {
|
|
835
|
-
valid: false,
|
|
836
|
-
message: `Symlinks partially configured (${validLinks}/${linkTypes.length})`,
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
return { valid: false, message: 'Symlinks not configured' };
|
|
840
|
-
}
|
|
841
141
|
//# sourceMappingURL=shared-routes.js.map
|