crewx 0.8.7-rc.21 → 0.8.7-rc.22
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/assets/{MarketPage-aY16UW1_.js → MarketPage-lLBtAENs.js} +10 -15
- package/dist/assets/{PromptTab-C3aTJS6u.js → PromptTab-Daq54dMi.js} +1 -1
- package/dist/assets/_baseUniq-BJDqmZBO.js +1 -0
- package/dist/assets/{arc-BCQAokB1.js → arc-ByjRIa5p.js} +1 -1
- package/dist/assets/{architectureDiagram-UYN6MBPD-Bc87ujv2.js → architectureDiagram-UYN6MBPD-DcKpqSFb.js} +1 -1
- package/dist/assets/{blockDiagram-ZHA2E4KO-wC9uH8Lb.js → blockDiagram-ZHA2E4KO-YvUeULT5.js} +1 -1
- package/dist/assets/{c4Diagram-6F5ED5ID-CqB7lqni.js → c4Diagram-6F5ED5ID-vggzH2N5.js} +1 -1
- package/dist/assets/channel-1Y6YooBb.js +1 -0
- package/dist/assets/{chunk-5HRBRIJM-Cc1L17Eh.js → chunk-5HRBRIJM-BbaQwJkn.js} +1 -1
- package/dist/assets/{chunk-7U56Z5CX-Df3nu_7G.js → chunk-7U56Z5CX-DVYVpFaa.js} +1 -1
- package/dist/assets/{chunk-ASOPGD6M-DIXslLG-.js → chunk-ASOPGD6M-CXEvWb-8.js} +1 -1
- package/dist/assets/{chunk-KFBOBJHC-FytVljyP.js → chunk-KFBOBJHC-CadVXFV3.js} +1 -1
- package/dist/assets/{chunk-T2TOU4HS-CU5z-cXq.js → chunk-T2TOU4HS-lDZr1sVF.js} +1 -1
- package/dist/assets/{chunk-TMUBEWPD-CWo1AFyn.js → chunk-TMUBEWPD-D5A0MrFD.js} +1 -1
- package/dist/assets/classDiagram-LNE6IOMH-CDxkV2JV.js +1 -0
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-CDxkV2JV.js +1 -0
- package/dist/assets/clone-6xznz_9G.js +1 -0
- package/dist/assets/{dagre-4EVJKHTY-CgZW5tg4.js → dagre-4EVJKHTY-Bncx7Ijq.js} +1 -1
- package/dist/assets/{diagram-QW4FP2JN-CJAng8RC.js → diagram-QW4FP2JN-RkxVNs99.js} +1 -1
- package/dist/assets/{erDiagram-6RL3IURR-C1uHztHX.js → erDiagram-6RL3IURR-DIiKqQdx.js} +1 -1
- package/dist/assets/{flowDiagram-7ASYPVHJ-Yiu2odO-.js → flowDiagram-7ASYPVHJ-DQ5PN7v6.js} +1 -1
- package/dist/assets/{ganttDiagram-NTVNEXSI-tutc0dJP.js → ganttDiagram-NTVNEXSI-BeNWGpB6.js} +1 -1
- package/dist/assets/{gitGraph-YCYPL57B-C3jM6T4G.js → gitGraph-YCYPL57B-CdMe059q.js} +1 -1
- package/dist/assets/{gitGraphDiagram-NRZ2UAAF-0aqHEKzz.js → gitGraphDiagram-NRZ2UAAF-BEke5TyK.js} +1 -1
- package/dist/assets/{graph-DC224ILV.js → graph-DyQIwWJd.js} +1 -1
- package/dist/assets/{infoDiagram-A4XQUW5V-Do7Mpzkx.js → infoDiagram-A4XQUW5V-CUcC8I_5.js} +1 -1
- package/dist/assets/{journeyDiagram-G5WM74LC-DAruEJc7.js → journeyDiagram-G5WM74LC-CQ5DqAH5.js} +1 -1
- package/dist/assets/{kanban-definition-QRCXZQQD-DB63vLR0.js → kanban-definition-QRCXZQQD-BBsgOii7.js} +1 -1
- package/dist/assets/{layout-BiYH5x6Q.js → layout-D7vBs_Wy.js} +1 -1
- package/dist/assets/{linear-CLyGW2s0.js → linear-CEOF9oJz.js} +1 -1
- package/dist/assets/{main-CEKkUqZb.js → main-Bx1nZ7_p.js} +278 -273
- package/dist/assets/main-C4ebvOIJ.css +10 -0
- package/dist/assets/{min-ChJMFU2N.js → min-RK9J5Utn.js} +1 -1
- package/dist/assets/{mindmap-definition-GWI6TPTV-CtADqQgd.js → mindmap-definition-GWI6TPTV-BXvEU7qC.js} +1 -1
- package/dist/assets/{pieDiagram-YF2LJOPJ-BSOfzo3O.js → pieDiagram-YF2LJOPJ-TrXqcpni.js} +1 -1
- package/dist/assets/{quadrantDiagram-OS5C2QUG-BLBQNFVb.js → quadrantDiagram-OS5C2QUG-B-chTEU4.js} +1 -1
- package/dist/assets/{requirementDiagram-MIRIMTAZ-DqorvuS6.js → requirementDiagram-MIRIMTAZ-BgJhNLp_.js} +1 -1
- package/dist/assets/{sankeyDiagram-Y46BX6SQ-4NDcrLkB.js → sankeyDiagram-Y46BX6SQ-BVsgYKK7.js} +1 -1
- package/dist/assets/{sequenceDiagram-G6AWOVSC-yha6BxK2.js → sequenceDiagram-G6AWOVSC-ByJOpkq3.js} +1 -1
- package/dist/assets/{stateDiagram-MAYHULR4-XJ_I41ez.js → stateDiagram-MAYHULR4-Dw7lUL1B.js} +1 -1
- package/dist/assets/stateDiagram-v2-4JROLMXI-DYJiM3bF.js +1 -0
- package/dist/assets/{timeline-definition-U7ZMHBDA-DCNrmvE7.js → timeline-definition-U7ZMHBDA-CSoEYs60.js} +1 -1
- package/dist/assets/{xychartDiagram-6QU3TZC5-9N-Ohhdp.js → xychartDiagram-6QU3TZC5-0_rG7p3L.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -8
- package/packages/cli/package.json +1 -1
- package/dist/assets/_baseUniq-DxEFlwNt.js +0 -1
- package/dist/assets/channel-DNLHkiNp.js +0 -1
- package/dist/assets/classDiagram-LNE6IOMH-B430Onym.js +0 -1
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-B430Onym.js +0 -1
- package/dist/assets/clone-B9O3E3P1.js +0 -1
- package/dist/assets/main-D3YeRndF.css +0 -10
- package/dist/assets/stateDiagram-v2-4JROLMXI-CxcyPJO2.js +0 -1
- package/dist-server/adapters/adapter.module.js +0 -35
- package/dist-server/adapters/adapter.service.js +0 -79
- package/dist-server/adapters/http-router.service.js +0 -91
- package/dist-server/app.module.js +0 -111
- package/dist-server/bootstrap/crewx-server.js +0 -80
- package/dist-server/bootstrap/tls.js +0 -138
- package/dist-server/common/analytics.client.js +0 -180
- package/dist-server/common/analytics.module.js +0 -21
- package/dist-server/common/cors.js +0 -15
- package/dist-server/common/decorators/project.decorator.js +0 -21
- package/dist-server/common/decorators/workspace.decorator.js +0 -24
- package/dist-server/common/device-id.js +0 -33
- package/dist-server/common/interceptor/api-id-header.interceptor.js +0 -53
- package/dist-server/common/interceptor/http-logging.interceptor.js +0 -124
- package/dist-server/common/limits/defaults.js +0 -13
- package/dist-server/common/limits/limits-provider.js +0 -6
- package/dist-server/common/limits/limits.controller.js +0 -38
- package/dist-server/common/limits/limits.module.js +0 -24
- package/dist-server/common/limits/local-limits.provider.js +0 -27
- package/dist-server/common/middleware/workspace.middleware.js +0 -210
- package/dist-server/common/repository.exception-filter.js +0 -48
- package/dist-server/common/workspace-context.store.js +0 -25
- package/dist-server/domain/agent/agent.controller.js +0 -96
- package/dist-server/domain/agent/agent.module.js +0 -27
- package/dist-server/domain/agent/agent.service.js +0 -335
- package/dist-server/domain/agent/agent.types.js +0 -3
- package/dist-server/domain/agent/dto/create-agent.dto.js +0 -80
- package/dist-server/domain/agent/dto/preview-prompt.dto.js +0 -77
- package/dist-server/domain/agent/dto/provider-meta.dto.js +0 -2
- package/dist-server/domain/agent/dto/update-agent.dto.js +0 -92
- package/dist-server/domain/agent/provider.controller.js +0 -38
- package/dist-server/domain/agent/template-processor.service.js +0 -71
- package/dist-server/domain/auth/auth.constants.js +0 -4
- package/dist-server/domain/auth/auth.controller.js +0 -123
- package/dist-server/domain/auth/auth.module.js +0 -47
- package/dist-server/domain/auth/decorators/public.decorator.js +0 -7
- package/dist-server/domain/auth/guards/base-auth.guard.js +0 -66
- package/dist-server/domain/auth/session-store.js +0 -26
- package/dist-server/domain/auth/utils/ip.utils.js +0 -31
- package/dist-server/domain/box/box.controller.js +0 -77
- package/dist-server/domain/box/box.module.js +0 -21
- package/dist-server/domain/box/box.service.js +0 -97
- package/dist-server/domain/box/dto/create-box.dto.js +0 -74
- package/dist-server/domain/cli/cli.controller.js +0 -136
- package/dist-server/domain/cli/cli.module.js +0 -21
- package/dist-server/domain/cli/cli.service.js +0 -598
- package/dist-server/domain/cli/dto/login-cli.dto.js +0 -25
- package/dist-server/domain/cli/dto/update-cli.dto.js +0 -31
- package/dist-server/domain/doc/doc.controller.js +0 -95
- package/dist-server/domain/doc/doc.module.js +0 -21
- package/dist-server/domain/doc/doc.service.js +0 -368
- package/dist-server/domain/doc/dto/doc.dto.js +0 -64
- package/dist-server/domain/document/document.controller.js +0 -57
- package/dist-server/domain/document/document.module.js +0 -22
- package/dist-server/domain/document/document.service.js +0 -274
- package/dist-server/domain/document/dto/register-document.dto.js +0 -24
- package/dist-server/domain/fs/dto/mkdir-request.dto.js +0 -47
- package/dist-server/domain/fs/dto/mkdir-response.dto.js +0 -2
- package/dist-server/domain/fs/dto/quick-access.dto.js +0 -2
- package/dist-server/domain/fs/dto/tree-node.dto.js +0 -2
- package/dist-server/domain/fs/dto/tree-query.dto.js +0 -35
- package/dist-server/domain/fs/fs.controller.js +0 -63
- package/dist-server/domain/fs/fs.module.js +0 -22
- package/dist-server/domain/fs/fs.service.js +0 -303
- package/dist-server/domain/git/git.module.js +0 -20
- package/dist-server/domain/git/git.service.js +0 -222
- package/dist-server/domain/goal/dto/goal.dto.js +0 -49
- package/dist-server/domain/goal/goal.controller.js +0 -86
- package/dist-server/domain/goal/goal.module.js +0 -22
- package/dist-server/domain/goal/goal.service.js +0 -197
- package/dist-server/domain/goal/parser/goal-md-parser.js +0 -166
- package/dist-server/domain/goal/parser/goal-md-serializer.js +0 -110
- package/dist-server/domain/health/health.controller.js +0 -57
- package/dist-server/domain/health/health.module.js +0 -19
- package/dist-server/domain/knowledge/dto/graph-knowledge.dto.js +0 -27
- package/dist-server/domain/knowledge/dto/knowledge.dto.js +0 -71
- package/dist-server/domain/knowledge/knowledge.controller.js +0 -122
- package/dist-server/domain/knowledge/knowledge.module.js +0 -21
- package/dist-server/domain/knowledge/knowledge.service.js +0 -538
- package/dist-server/domain/market/dto/add-registry.dto.js +0 -32
- package/dist-server/domain/market/dto/install-market.dto.js +0 -30
- package/dist-server/domain/market/market.controller.js +0 -163
- package/dist-server/domain/market/market.module.js +0 -22
- package/dist-server/domain/market/market.service.js +0 -1447
- package/dist-server/domain/mcp/browser-session.store.js +0 -247
- package/dist-server/domain/mcp/crewx-tool.factory.js +0 -119
- package/dist-server/domain/mcp/mcp-auth.guard.js +0 -61
- package/dist-server/domain/mcp/mcp.constants.js +0 -4
- package/dist-server/domain/mcp/mcp.controller.js +0 -125
- package/dist-server/domain/mcp/mcp.dto.js +0 -12
- package/dist-server/domain/mcp/mcp.module.js +0 -49
- package/dist-server/domain/mcp/mcp.service.js +0 -684
- package/dist-server/domain/mcp/tool-router.service.js +0 -120
- package/dist-server/domain/message/dto/list-messages.dto.js +0 -57
- package/dist-server/domain/message/dto/send-message.dto.js +0 -45
- package/dist-server/domain/message/message.controller.js +0 -73
- package/dist-server/domain/message/message.module.js +0 -25
- package/dist-server/domain/message/message.service.js +0 -160
- package/dist-server/domain/onboarding/onboarding.controller.js +0 -97
- package/dist-server/domain/onboarding/onboarding.module.js +0 -23
- package/dist-server/domain/onboarding/onboarding.service.js +0 -106
- package/dist-server/domain/onboarding/onboarding.types.js +0 -2
- package/dist-server/domain/onboarding/presets/custom.js +0 -10
- package/dist-server/domain/onboarding/presets/dev-team.js +0 -41
- package/dist-server/domain/onboarding/presets/index.js +0 -15
- package/dist-server/domain/onboarding/presets/marketing-team.js +0 -29
- package/dist-server/domain/onboarding/presets/planning-team.js +0 -29
- package/dist-server/domain/onboarding/presets/solo.js +0 -17
- package/dist-server/domain/planner/dto/planner.dto.js +0 -58
- package/dist-server/domain/planner/planner.controller.js +0 -74
- package/dist-server/domain/planner/planner.module.js +0 -22
- package/dist-server/domain/planner/planner.service.js +0 -269
- package/dist-server/domain/project/dto/create-project.dto.js +0 -46
- package/dist-server/domain/project/dto/update-project.dto.js +0 -60
- package/dist-server/domain/project/project.controller.js +0 -129
- package/dist-server/domain/project/project.module.js +0 -22
- package/dist-server/domain/project/project.service.js +0 -173
- package/dist-server/domain/settings/settings.controller.js +0 -52
- package/dist-server/domain/settings/settings.module.js +0 -22
- package/dist-server/domain/settings/settings.service.js +0 -105
- package/dist-server/domain/task/dto/all-tasks.dto.js +0 -55
- package/dist-server/domain/task/dto/list-tasks.dto.js +0 -69
- package/dist-server/domain/task/dto/search-tasks.dto.js +0 -66
- package/dist-server/domain/task/dto/send-message.dto.js +0 -31
- package/dist-server/domain/task/dto/task-status.dto.js +0 -34
- package/dist-server/domain/task/dto/workspace-usage.dto.js +0 -38
- package/dist-server/domain/task/task.constants.js +0 -5
- package/dist-server/domain/task/task.controller.js +0 -169
- package/dist-server/domain/task/task.module.js +0 -23
- package/dist-server/domain/task/task.service.js +0 -775
- package/dist-server/domain/thread/dto/build-context.dto.js +0 -42
- package/dist-server/domain/thread/dto/list-messages.dto.js +0 -49
- package/dist-server/domain/thread/dto/list-threads.dto.js +0 -70
- package/dist-server/domain/thread/dto/search-threads.dto.js +0 -13
- package/dist-server/domain/thread/dto/update-thread.dto.js +0 -23
- package/dist-server/domain/thread/thread.controller.js +0 -160
- package/dist-server/domain/thread/thread.module.js +0 -23
- package/dist-server/domain/thread/thread.service.js +0 -419
- package/dist-server/domain/usage/usage.controller.js +0 -118
- package/dist-server/domain/usage/usage.module.js +0 -21
- package/dist-server/domain/usage/usage.service.js +0 -358
- package/dist-server/domain/usage/usage.types.js +0 -4
- package/dist-server/domain/wbs/wbs.controller.js +0 -51
- package/dist-server/domain/wbs/wbs.module.js +0 -21
- package/dist-server/domain/wbs/wbs.service.js +0 -94
- package/dist-server/domain/workflow/workflow.controller.js +0 -53
- package/dist-server/domain/workflow/workflow.module.js +0 -21
- package/dist-server/domain/workflow/workflow.service.js +0 -209
- package/dist-server/domain/workspace/dto/create-workspace.dto.js +0 -45
- package/dist-server/domain/workspace/dto/switch-workspace.dto.js +0 -32
- package/dist-server/domain/workspace/dto/workspace-info.dto.js +0 -2
- package/dist-server/domain/workspace/workspace.controller.js +0 -98
- package/dist-server/domain/workspace/workspace.module.js +0 -22
- package/dist-server/domain/workspace/workspace.service.js +0 -198
- package/dist-server/main.js +0 -204
- package/dist-server/modules/crewx-pool.service.js +0 -75
- package/dist-server/modules/crewx.module.js +0 -47
- package/dist-server/repository/base-sqlite.repository.js +0 -18
- package/dist-server/repository/box.repository.js +0 -99
- package/dist-server/repository/message.repository.js +0 -22
- package/dist-server/repository/project.repository.js +0 -51
- package/dist-server/repository/repository.module.js +0 -34
- package/dist-server/repository/request-log.repository.js +0 -96
- package/dist-server/repository/task.repository.js +0 -105
- package/dist-server/repository/thread.repository.js +0 -165
- package/dist-server/shared/crewx-home.js +0 -58
- package/dist-server/shared/crewx-state.js +0 -66
- package/dist-server/test-utils/compat-database.js +0 -30
- package/dist-server/utils/tokenizer.js +0 -8
|
@@ -1,1447 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
-
};
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
|
-
};
|
|
44
|
-
var MarketService_1;
|
|
45
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
-
exports.MarketService = void 0;
|
|
47
|
-
exports.boolToTrustLevel = boolToTrustLevel;
|
|
48
|
-
exports.maxTrustLevel = maxTrustLevel;
|
|
49
|
-
exports.trustLevelToIsVerified = trustLevelToIsVerified;
|
|
50
|
-
const common_1 = require("@nestjs/common");
|
|
51
|
-
const fs_1 = require("fs");
|
|
52
|
-
const promises_1 = require("fs/promises");
|
|
53
|
-
const path_1 = require("path");
|
|
54
|
-
const os_1 = require("os");
|
|
55
|
-
const crypto_1 = require("crypto");
|
|
56
|
-
const child_process_1 = require("child_process");
|
|
57
|
-
const yaml = __importStar(require("js-yaml"));
|
|
58
|
-
const zod_1 = require("zod");
|
|
59
|
-
const skill_1 = require("@crewx/skill");
|
|
60
|
-
const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
|
|
61
|
-
const analytics_client_js_1 = require("../../common/analytics.client.js");
|
|
62
|
-
// --- Zod schemas ---
|
|
63
|
-
const MarketPackageTypeSchema = zod_1.z.enum(['skill', 'template', 'workflow', 'plugin']);
|
|
64
|
-
const TemplateEntrySchema = zod_1.z.object({
|
|
65
|
-
name: zod_1.z.string(),
|
|
66
|
-
displayName: zod_1.z.string().optional(),
|
|
67
|
-
description: zod_1.z.string().optional(),
|
|
68
|
-
author: zod_1.z.string().optional(),
|
|
69
|
-
version: zod_1.z.string().optional(),
|
|
70
|
-
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
71
|
-
type: MarketPackageTypeSchema.optional().default('skill'),
|
|
72
|
-
path: zod_1.z.string().optional(),
|
|
73
|
-
features: zod_1.z.array(zod_1.z.string()).optional(),
|
|
74
|
-
crewxVersion: zod_1.z.string().optional(),
|
|
75
|
-
});
|
|
76
|
-
const MarketplaceRegistryEntrySchema = zod_1.z.object({
|
|
77
|
-
id: zod_1.z.string(),
|
|
78
|
-
name: zod_1.z.string().optional(),
|
|
79
|
-
url: zod_1.z.string(),
|
|
80
|
-
type: zod_1.z.string().optional(),
|
|
81
|
-
trust: zod_1.z.enum(['trusted', 'verified', 'unverified']).optional(),
|
|
82
|
-
verified: zod_1.z.boolean().optional(),
|
|
83
|
-
default: zod_1.z.boolean().optional(),
|
|
84
|
-
tokenEnv: zod_1.z.string().optional(),
|
|
85
|
-
allow: zod_1.z.array(zod_1.z.string()).optional(),
|
|
86
|
-
});
|
|
87
|
-
const MarketplaceConfigSchema = zod_1.z.object({
|
|
88
|
-
version: zod_1.z.string().optional(),
|
|
89
|
-
forceDisabled: zod_1.z.array(zod_1.z.string()).optional(),
|
|
90
|
-
forceEnabled: zod_1.z.array(zod_1.z.string()).optional(),
|
|
91
|
-
deny: zod_1.z.array(zod_1.z.string()).optional(),
|
|
92
|
-
registries: zod_1.z.array(MarketplaceRegistryEntrySchema),
|
|
93
|
-
});
|
|
94
|
-
const VALID_REGISTRY_SOURCE_TYPES = new Set(['crewx', 'codex', 'claude']);
|
|
95
|
-
const TRUST_RANK = { trusted: 2, verified: 1, unverified: 0 };
|
|
96
|
-
function boolToTrustLevel(isVerified, isDefault, source) {
|
|
97
|
-
if (source === 'builtin')
|
|
98
|
-
return 'trusted';
|
|
99
|
-
if (isDefault || isVerified)
|
|
100
|
-
return 'verified';
|
|
101
|
-
return 'unverified';
|
|
102
|
-
}
|
|
103
|
-
function maxTrustLevel(a, b) {
|
|
104
|
-
return TRUST_RANK[a] >= TRUST_RANK[b] ? a : b;
|
|
105
|
-
}
|
|
106
|
-
function trustLevelToIsVerified(level) {
|
|
107
|
-
return level !== 'unverified';
|
|
108
|
-
}
|
|
109
|
-
// --- Constants ---
|
|
110
|
-
const DEFAULT_REGISTRY_URL = 'https://github.com/sowonlabs/crewx-templates';
|
|
111
|
-
const DEFAULT_REGISTRY_NAME = 'crewx-templates';
|
|
112
|
-
const MARKETPLACE_REMOTE_URL = 'https://raw.githubusercontent.com/sowonlabs/crewx-templates/main/marketplace.json';
|
|
113
|
-
const MARKETPLACE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
114
|
-
function loadBundledMarketplace() {
|
|
115
|
-
try {
|
|
116
|
-
const bundlePath = (0, path_1.join)(__dirname, 'default-marketplace.json');
|
|
117
|
-
const raw = JSON.parse((0, fs_1.readFileSync)(bundlePath, 'utf-8'));
|
|
118
|
-
const result = MarketplaceConfigSchema.safeParse(raw);
|
|
119
|
-
return result.success ? result.data : null;
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Cache TTL: 15 minutes (§4.3 rate limit)
|
|
126
|
-
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
127
|
-
let MarketService = MarketService_1 = class MarketService {
|
|
128
|
-
logger = new common_1.Logger(MarketService_1.name);
|
|
129
|
-
analytics;
|
|
130
|
-
constructor(analytics) {
|
|
131
|
-
this.analytics = analytics;
|
|
132
|
-
}
|
|
133
|
-
// In-memory cache for templates.json fetches
|
|
134
|
-
templateCache = new Map();
|
|
135
|
-
marketplaceMemoryCache = null;
|
|
136
|
-
marketplaceRefreshPending = false;
|
|
137
|
-
get skillsDir() {
|
|
138
|
-
return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), 'skills');
|
|
139
|
-
}
|
|
140
|
-
get configPath() {
|
|
141
|
-
return process.env.CREWX_CONFIG || (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), 'crewx.yaml');
|
|
142
|
-
}
|
|
143
|
-
get registryCachePath() {
|
|
144
|
-
return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), '.crewx', 'registry.json');
|
|
145
|
-
}
|
|
146
|
-
get lockFilePath() {
|
|
147
|
-
return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), '.crewx', 'skills.lock.json');
|
|
148
|
-
}
|
|
149
|
-
get marketplacePath() {
|
|
150
|
-
return (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'marketplace.json');
|
|
151
|
-
}
|
|
152
|
-
get marketplaceCachePath() {
|
|
153
|
-
return (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'marketplace.cache.json');
|
|
154
|
-
}
|
|
155
|
-
get workspaceMarketplacePath() {
|
|
156
|
-
return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), '.crewx', 'marketplace.json');
|
|
157
|
-
}
|
|
158
|
-
// --- Path traversal guard ---
|
|
159
|
-
validateItemName(name) {
|
|
160
|
-
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
161
|
-
throw new common_1.BadRequestException('Invalid skill name');
|
|
162
|
-
}
|
|
163
|
-
const resolved = (0, path_1.resolve)(this.skillsDir, name);
|
|
164
|
-
if (!resolved.startsWith((0, path_1.resolve)(this.skillsDir))) {
|
|
165
|
-
throw new common_1.BadRequestException('Path traversal detected');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// --- Registry CRUD ---
|
|
169
|
-
listRegistries() {
|
|
170
|
-
const entries = this.loadRegistries();
|
|
171
|
-
const registries = entries.map((r) => ({
|
|
172
|
-
...r,
|
|
173
|
-
isVerified: trustLevelToIsVerified(r.trustLevel),
|
|
174
|
-
itemCount: this.getCachedItemCount(r.id),
|
|
175
|
-
}));
|
|
176
|
-
return { registries };
|
|
177
|
-
}
|
|
178
|
-
addRegistry(url, tokenEnv) {
|
|
179
|
-
if (!url || typeof url !== 'string') {
|
|
180
|
-
throw new common_1.BadRequestException('url is required');
|
|
181
|
-
}
|
|
182
|
-
const parsed = this.parseGitHubUrl(url);
|
|
183
|
-
if (!parsed) {
|
|
184
|
-
throw new common_1.BadRequestException('Invalid GitHub repository URL. Expected format: https://github.com/{owner}/{repo}');
|
|
185
|
-
}
|
|
186
|
-
if (tokenEnv !== undefined) {
|
|
187
|
-
this.validateTokenEnv(tokenEnv);
|
|
188
|
-
}
|
|
189
|
-
const mpConfig = this.loadMarketplaceJson();
|
|
190
|
-
if (mpConfig?.forceDisabled?.length) {
|
|
191
|
-
const disabledSet = new Set(mpConfig.forceDisabled);
|
|
192
|
-
const parsedInput = this.parseGitHubUrl(url);
|
|
193
|
-
const blockedByPolicy = mpConfig.registries.some((r) => {
|
|
194
|
-
if (!disabledSet.has(r.id))
|
|
195
|
-
return false;
|
|
196
|
-
const parsedBlocked = this.parseGitHubUrl(r.url);
|
|
197
|
-
if (!parsedInput || !parsedBlocked)
|
|
198
|
-
return false;
|
|
199
|
-
return (parsedInput.owner.toLowerCase() === parsedBlocked.owner.toLowerCase() &&
|
|
200
|
-
parsedInput.repo.toLowerCase() === parsedBlocked.repo.toLowerCase());
|
|
201
|
-
});
|
|
202
|
-
if (blockedByPolicy) {
|
|
203
|
-
throw new common_1.BadRequestException('This registry is blocked by marketplace policy (forceDisabled)');
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const entries = this.loadRegistries();
|
|
207
|
-
// Check duplicate URL
|
|
208
|
-
const normalized = this.normalizeUrl(url);
|
|
209
|
-
const exists = entries.some((r) => this.normalizeUrl(r.url) === normalized);
|
|
210
|
-
if (exists) {
|
|
211
|
-
throw new common_1.BadRequestException('Registry with this URL already exists');
|
|
212
|
-
}
|
|
213
|
-
const entry = {
|
|
214
|
-
id: (0, crypto_1.randomUUID)(),
|
|
215
|
-
name: parsed.repo,
|
|
216
|
-
url: normalized,
|
|
217
|
-
type: 'crewx',
|
|
218
|
-
trustLevel: 'unverified',
|
|
219
|
-
isDefault: false,
|
|
220
|
-
addedAt: new Date().toISOString(),
|
|
221
|
-
source: 'workspace',
|
|
222
|
-
...(tokenEnv ? { tokenEnv } : {}),
|
|
223
|
-
};
|
|
224
|
-
entries.push(entry);
|
|
225
|
-
this.saveRegistries(entries);
|
|
226
|
-
return { ...entry, isVerified: trustLevelToIsVerified(entry.trustLevel), itemCount: 0 };
|
|
227
|
-
}
|
|
228
|
-
deleteRegistry(id) {
|
|
229
|
-
const entries = this.loadRegistries();
|
|
230
|
-
const idx = entries.findIndex((r) => r.id === id);
|
|
231
|
-
if (idx === -1) {
|
|
232
|
-
throw new common_1.NotFoundException(`Registry '${id}' not found`);
|
|
233
|
-
}
|
|
234
|
-
if (entries[idx].isDefault) {
|
|
235
|
-
throw new common_1.BadRequestException('Cannot delete the default registry');
|
|
236
|
-
}
|
|
237
|
-
if (entries[idx].source === 'global') {
|
|
238
|
-
throw new common_1.BadRequestException('Cannot delete official registry from marketplace policy');
|
|
239
|
-
}
|
|
240
|
-
entries.splice(idx, 1);
|
|
241
|
-
this.saveRegistries(entries);
|
|
242
|
-
this.templateCache.delete(id);
|
|
243
|
-
}
|
|
244
|
-
// --- Market item listing ---
|
|
245
|
-
async listMarketItems(registryId) {
|
|
246
|
-
const entries = this.loadRegistries();
|
|
247
|
-
const registry = entries.find((r) => r.id === registryId);
|
|
248
|
-
if (!registry) {
|
|
249
|
-
throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
|
|
250
|
-
}
|
|
251
|
-
const templates = await this.fetchTemplates(registry);
|
|
252
|
-
const [installedNames, installStats] = await Promise.all([
|
|
253
|
-
this.getInstalledItemNames(),
|
|
254
|
-
this.analytics.fetchInstallStats(templates.map((t) => t.name)),
|
|
255
|
-
]);
|
|
256
|
-
const installedSlugs = new Set([...installedNames].map((n) => n.toLowerCase().replace(/[\s_]+/g, '-')));
|
|
257
|
-
const items = templates.map((t) => ({
|
|
258
|
-
name: t.name,
|
|
259
|
-
displayName: t.displayName || t.name,
|
|
260
|
-
description: t.description || '',
|
|
261
|
-
author: t.author || '',
|
|
262
|
-
version: t.version || '0.0.0',
|
|
263
|
-
tags: t.tags || [],
|
|
264
|
-
type: t.type || 'template',
|
|
265
|
-
trustLevel: registry.trustLevel,
|
|
266
|
-
isVerified: trustLevelToIsVerified(registry.trustLevel),
|
|
267
|
-
isInstalled: installedNames.has(t.name) ||
|
|
268
|
-
installedSlugs.has(t.name.toLowerCase().replace(/[\s_]+/g, '-')),
|
|
269
|
-
registryId: registry.id,
|
|
270
|
-
registryName: registry.name,
|
|
271
|
-
installCount: installStats[t.name]?.unique ?? 0,
|
|
272
|
-
}));
|
|
273
|
-
return { items };
|
|
274
|
-
}
|
|
275
|
-
async listAllMarketItems() {
|
|
276
|
-
const registries = this.loadRegistries();
|
|
277
|
-
const [installedNames, ...templateResults] = await Promise.all([
|
|
278
|
-
this.getInstalledItemNames(),
|
|
279
|
-
...registries.map((registry) => this.fetchTemplates(registry)
|
|
280
|
-
.then((templates) => ({ registry, templates }))
|
|
281
|
-
.catch(() => ({ registry, templates: [] }))),
|
|
282
|
-
]);
|
|
283
|
-
const installedSlugs = new Set([...installedNames].map((n) => n.toLowerCase().replace(/[\s_]+/g, '-')));
|
|
284
|
-
const allNames = templateResults.flatMap(({ templates }) => templates.map((t) => t.name));
|
|
285
|
-
const installStats = await this.analytics.fetchInstallStats(allNames);
|
|
286
|
-
const items = [];
|
|
287
|
-
for (const { registry, templates } of templateResults) {
|
|
288
|
-
for (const t of templates) {
|
|
289
|
-
items.push({
|
|
290
|
-
name: t.name,
|
|
291
|
-
displayName: t.displayName || t.name,
|
|
292
|
-
description: t.description || '',
|
|
293
|
-
author: t.author || '',
|
|
294
|
-
version: t.version || '0.0.0',
|
|
295
|
-
tags: t.tags || [],
|
|
296
|
-
type: t.type || 'template',
|
|
297
|
-
trustLevel: registry.trustLevel,
|
|
298
|
-
isVerified: trustLevelToIsVerified(registry.trustLevel),
|
|
299
|
-
isInstalled: installedNames.has(t.name) ||
|
|
300
|
-
installedSlugs.has(t.name.toLowerCase().replace(/[\s_]+/g, '-')),
|
|
301
|
-
registryId: registry.id,
|
|
302
|
-
registryName: registry.name,
|
|
303
|
-
installCount: installStats[t.name]?.unique ?? 0,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return { items };
|
|
308
|
-
}
|
|
309
|
-
// --- Install / Uninstall ---
|
|
310
|
-
async installMarketItem(registryId, marketName) {
|
|
311
|
-
this.validateItemName(marketName);
|
|
312
|
-
const entries = this.loadRegistries();
|
|
313
|
-
const registry = entries.find((r) => r.id === registryId);
|
|
314
|
-
if (!registry) {
|
|
315
|
-
throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
|
|
316
|
-
}
|
|
317
|
-
// Deny list: block packages listed in marketplace.json deny[]
|
|
318
|
-
const mpConfig = this.loadMarketplaceJson();
|
|
319
|
-
if (mpConfig?.deny?.length) {
|
|
320
|
-
const denySet = new Set(mpConfig.deny.map((d) => d.toLowerCase()));
|
|
321
|
-
if (denySet.has(marketName.toLowerCase())) {
|
|
322
|
-
throw new common_1.BadRequestException(`Package '${marketName}' is blocked by marketplace policy (deny list).`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const templates = await this.fetchTemplates(registry);
|
|
326
|
-
const template = templates.find((t) => t.name === marketName);
|
|
327
|
-
if (!template) {
|
|
328
|
-
throw new common_1.NotFoundException(`Skill '${marketName}' not found in registry '${registry.name}'`);
|
|
329
|
-
}
|
|
330
|
-
// Block exec/plugin from unverified registries (security policy — 마켓-설계서 6.1)
|
|
331
|
-
if (registry.trustLevel === 'unverified') {
|
|
332
|
-
if (template.type === 'exec') {
|
|
333
|
-
throw new common_1.BadRequestException('Unverified registry exec skill installation is blocked for security.\n' +
|
|
334
|
-
'Use a verified registry or manually copy the skill files.');
|
|
335
|
-
}
|
|
336
|
-
if (template.type === 'plugin') {
|
|
337
|
-
throw new common_1.BadRequestException('Unverified registry plugin installation is blocked for security.\n' +
|
|
338
|
-
'Use a verified registry or manually copy the plugin files.');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
342
|
-
if (!parsed) {
|
|
343
|
-
throw new common_1.BadRequestException('Invalid registry URL');
|
|
344
|
-
}
|
|
345
|
-
const skillPath = template.path || marketName;
|
|
346
|
-
const targetDir = (0, path_1.join)(this.skillsDir, marketName);
|
|
347
|
-
const tempDir = (0, path_1.join)(this.skillsDir, `.tmp-${marketName}-${Date.now()}`);
|
|
348
|
-
try {
|
|
349
|
-
// 1. Fetch GitHub Trees API to get full file list
|
|
350
|
-
const treesUrl = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/git/trees/main?recursive=1`;
|
|
351
|
-
const treesRes = await fetch(treesUrl, { headers: this.getGitHubHeaders(registry) });
|
|
352
|
-
if (!treesRes.ok) {
|
|
353
|
-
throw new Error(`Trees API: HTTP ${treesRes.status} ${treesRes.statusText}`);
|
|
354
|
-
}
|
|
355
|
-
const treesData = await treesRes.json();
|
|
356
|
-
// 2. Filter: blobs under skillPath/ with safety exclusions
|
|
357
|
-
const BLOCKED_PREFIXES = ['node_modules/', '.git/'];
|
|
358
|
-
const skillPrefix = `${skillPath}/`;
|
|
359
|
-
const files = treesData.tree.filter((item) => {
|
|
360
|
-
if (item.type !== 'blob')
|
|
361
|
-
return false;
|
|
362
|
-
if (!item.path.startsWith(skillPrefix))
|
|
363
|
-
return false;
|
|
364
|
-
const relPath = item.path.slice(skillPrefix.length);
|
|
365
|
-
if (!relPath)
|
|
366
|
-
return false;
|
|
367
|
-
// Safety filter: block node_modules/, .git/, and .env files
|
|
368
|
-
if (BLOCKED_PREFIXES.some((p) => relPath.startsWith(p)))
|
|
369
|
-
return false;
|
|
370
|
-
if (relPath === '.env' || relPath.startsWith('.env/'))
|
|
371
|
-
return false;
|
|
372
|
-
return true;
|
|
373
|
-
});
|
|
374
|
-
if (files.length === 0) {
|
|
375
|
-
throw new Error(`No files found in skill directory '${skillPath}'`);
|
|
376
|
-
}
|
|
377
|
-
// 3. Download all files to tempDir
|
|
378
|
-
if (!(0, fs_1.existsSync)(this.skillsDir)) {
|
|
379
|
-
(0, fs_1.mkdirSync)(this.skillsDir, { recursive: true });
|
|
380
|
-
}
|
|
381
|
-
(0, fs_1.mkdirSync)(tempDir, { recursive: true });
|
|
382
|
-
const resolvedTempDir = (0, path_1.resolve)(tempDir);
|
|
383
|
-
for (const file of files) {
|
|
384
|
-
const relPath = file.path.slice(skillPrefix.length);
|
|
385
|
-
// Path traversal guard
|
|
386
|
-
const destPath = (0, path_1.resolve)((0, path_1.join)(tempDir, relPath));
|
|
387
|
-
if (!destPath.startsWith(resolvedTempDir + path_1.sep) && destPath !== resolvedTempDir) {
|
|
388
|
-
throw new Error(`Path traversal detected: ${relPath}`);
|
|
389
|
-
}
|
|
390
|
-
const destFileDir = (0, path_1.dirname)(destPath);
|
|
391
|
-
if (!(0, fs_1.existsSync)(destFileDir)) {
|
|
392
|
-
(0, fs_1.mkdirSync)(destFileDir, { recursive: true });
|
|
393
|
-
}
|
|
394
|
-
const rawUrl = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/${file.path}`;
|
|
395
|
-
const fileContent = await this.fetchUrlBytes(rawUrl, this.getGitHubHeaders(registry));
|
|
396
|
-
(0, fs_1.writeFileSync)(destPath, fileContent);
|
|
397
|
-
}
|
|
398
|
-
// 3.5. Compute treeSha early (needed for Step 5.5 and Step 8)
|
|
399
|
-
const treeSha = treesData.tree.find((item) => item.type === 'tree' && item.path === skillPath)?.sha ?? '';
|
|
400
|
-
// 4. Trust check: exec skill from unverified registry → block
|
|
401
|
-
if (registry.trustLevel === 'unverified' && this.isExecItem(template, tempDir)) {
|
|
402
|
-
throw new common_1.BadRequestException('Unverified registry exec skill installation is blocked for security. ' +
|
|
403
|
-
'Use a verified registry or manually copy the skill files.');
|
|
404
|
-
}
|
|
405
|
-
// 5. Atomic rename: backup-rename pattern for safe rollback
|
|
406
|
-
const backupDir = (0, fs_1.existsSync)(targetDir)
|
|
407
|
-
? (0, path_1.join)(this.skillsDir, `.backup-${marketName}-${Date.now()}`)
|
|
408
|
-
: null;
|
|
409
|
-
if (backupDir) {
|
|
410
|
-
(0, fs_1.renameSync)(targetDir, backupDir);
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
(0, fs_1.renameSync)(tempDir, targetDir);
|
|
414
|
-
if (backupDir && (0, fs_1.existsSync)(backupDir)) {
|
|
415
|
-
(0, fs_1.rmSync)(backupDir, { recursive: true, force: true });
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
catch (renameErr) {
|
|
419
|
-
if (backupDir && (0, fs_1.existsSync)(backupDir)) {
|
|
420
|
-
(0, fs_1.renameSync)(backupDir, targetDir);
|
|
421
|
-
}
|
|
422
|
-
throw renameErr;
|
|
423
|
-
}
|
|
424
|
-
// 5.5. Ensure SKILL.md exists and inject metadata.source (§3.3)
|
|
425
|
-
const skillMdPath = (0, path_1.join)(targetDir, 'SKILL.md');
|
|
426
|
-
if (!(0, fs_1.existsSync)(skillMdPath)) {
|
|
427
|
-
const defaultContent = [
|
|
428
|
-
'---',
|
|
429
|
-
`name: ${template.displayName || marketName}`,
|
|
430
|
-
`description: "${template.description || ''}"`,
|
|
431
|
-
`version: ${template.version || '0.0.0'}`,
|
|
432
|
-
'---',
|
|
433
|
-
'',
|
|
434
|
-
`# ${template.displayName || marketName}`,
|
|
435
|
-
'',
|
|
436
|
-
template.description || '',
|
|
437
|
-
'',
|
|
438
|
-
].join('\n');
|
|
439
|
-
(0, fs_1.writeFileSync)(skillMdPath, defaultContent, 'utf-8');
|
|
440
|
-
}
|
|
441
|
-
const installedAt = new Date().toISOString();
|
|
442
|
-
this.injectSourceMetadata(skillMdPath, {
|
|
443
|
-
registry: `${parsed.owner}/${parsed.repo}`,
|
|
444
|
-
type: registry.type,
|
|
445
|
-
path: template.path ?? marketName,
|
|
446
|
-
version: template.version ?? '0.0.0',
|
|
447
|
-
treeSha,
|
|
448
|
-
installedAt,
|
|
449
|
-
});
|
|
450
|
-
// 6. Install npm dependencies if package.json exists
|
|
451
|
-
if ((0, fs_1.existsSync)((0, path_1.join)(targetDir, 'package.json'))) {
|
|
452
|
-
this.logger.log(`Installing npm dependencies for '${marketName}'`);
|
|
453
|
-
(0, child_process_1.execSync)('npm install --production --ignore-scripts', { cwd: targetDir });
|
|
454
|
-
}
|
|
455
|
-
// 7. Record install tracking in registry.json
|
|
456
|
-
this.updateRegistryInstallRecord(marketName, {
|
|
457
|
-
version: template.version,
|
|
458
|
-
registryId: registry.id,
|
|
459
|
-
installedAt,
|
|
460
|
-
});
|
|
461
|
-
// 8. Write lockfile entry (treeSha reused from Step 3.5)
|
|
462
|
-
const fileShas = {};
|
|
463
|
-
for (const item of treesData.tree) {
|
|
464
|
-
if (item.type === 'blob' && item.path.startsWith(skillPrefix)) {
|
|
465
|
-
const relPath = item.path.slice(skillPrefix.length);
|
|
466
|
-
if (relPath)
|
|
467
|
-
fileShas[relPath] = item.sha;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
const lockEntry = {
|
|
471
|
-
name: marketName,
|
|
472
|
-
version: template.version ?? '0.0.0',
|
|
473
|
-
registryId: registry.id,
|
|
474
|
-
registryUrl: registry.url,
|
|
475
|
-
treeSha,
|
|
476
|
-
fileShas,
|
|
477
|
-
installedAt,
|
|
478
|
-
};
|
|
479
|
-
const lock = this.loadLockFile();
|
|
480
|
-
lock.skills[marketName] = lockEntry;
|
|
481
|
-
this.saveLockFile(lock);
|
|
482
|
-
// Cache GitHub metadata (non-blocking — best-effort)
|
|
483
|
-
this.cacheGitHubMetadata(marketName, registry).catch((err) => {
|
|
484
|
-
this.logger.warn(`Failed to cache GitHub metadata for '${marketName}': ${err}`);
|
|
485
|
-
});
|
|
486
|
-
this.logger.log(`Installed skill '${marketName}' to ${targetDir}`);
|
|
487
|
-
this.analytics.track('market.install', {
|
|
488
|
-
item_name: marketName,
|
|
489
|
-
item_type: template.type || 'template',
|
|
490
|
-
item_version: template.version || '0.0.0',
|
|
491
|
-
registry_id: registryId,
|
|
492
|
-
});
|
|
493
|
-
return { installed: true, path: targetDir };
|
|
494
|
-
}
|
|
495
|
-
catch (err) {
|
|
496
|
-
// Cleanup temp dir on any failure
|
|
497
|
-
if ((0, fs_1.existsSync)(tempDir)) {
|
|
498
|
-
(0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
499
|
-
}
|
|
500
|
-
if (err instanceof common_1.BadRequestException || err instanceof common_1.NotFoundException) {
|
|
501
|
-
throw err;
|
|
502
|
-
}
|
|
503
|
-
this.logger.error(`Failed to install skill '${marketName}': ${err}`);
|
|
504
|
-
throw new common_1.BadRequestException(`Failed to install skill '${marketName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
isExecItem(template, tempDir) {
|
|
508
|
-
if (template.type === 'exec')
|
|
509
|
-
return true;
|
|
510
|
-
const pkgPath = (0, path_1.join)(tempDir, 'package.json');
|
|
511
|
-
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
512
|
-
try {
|
|
513
|
-
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
|
|
514
|
-
if (pkg.bin || pkg.main)
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
// ignore malformed package.json
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
injectSourceMetadata(skillMdPath, source) {
|
|
524
|
-
const raw = (0, fs_1.readFileSync)(skillMdPath, 'utf-8');
|
|
525
|
-
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)/);
|
|
526
|
-
let frontmatter;
|
|
527
|
-
let body;
|
|
528
|
-
if (fmMatch) {
|
|
529
|
-
frontmatter = yaml.load(fmMatch[1]) ?? {};
|
|
530
|
-
body = fmMatch[2] ?? '';
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
frontmatter = {};
|
|
534
|
-
body = raw;
|
|
535
|
-
}
|
|
536
|
-
const existingMetadata = frontmatter.metadata != null && typeof frontmatter.metadata === 'object'
|
|
537
|
-
? frontmatter.metadata
|
|
538
|
-
: {};
|
|
539
|
-
frontmatter.metadata = {
|
|
540
|
-
...existingMetadata,
|
|
541
|
-
version: frontmatter.version ?? source.version,
|
|
542
|
-
source,
|
|
543
|
-
};
|
|
544
|
-
const yamlStr = yaml.dump(frontmatter, { lineWidth: -1 });
|
|
545
|
-
(0, fs_1.writeFileSync)(skillMdPath, `---\n${yamlStr}---\n${body}`, 'utf-8');
|
|
546
|
-
}
|
|
547
|
-
updateRegistryInstallRecord(itemName, info) {
|
|
548
|
-
const cache = this.loadRegistryCache();
|
|
549
|
-
const existing = cache[itemName];
|
|
550
|
-
cache[itemName] = {
|
|
551
|
-
authors: existing?.authors ?? [],
|
|
552
|
-
stars: existing?.stars ?? 0,
|
|
553
|
-
license: existing?.license ?? '',
|
|
554
|
-
updatedAt: existing?.updatedAt ?? '',
|
|
555
|
-
fetchedAt: existing?.fetchedAt ?? '',
|
|
556
|
-
version: info.version,
|
|
557
|
-
registryId: info.registryId,
|
|
558
|
-
installedAt: info.installedAt,
|
|
559
|
-
};
|
|
560
|
-
this.saveRegistryCache(cache);
|
|
561
|
-
}
|
|
562
|
-
async fetchUrlBytes(url, headers) {
|
|
563
|
-
const response = await fetch(url, headers ? { headers } : undefined);
|
|
564
|
-
if (!response.ok) {
|
|
565
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
566
|
-
}
|
|
567
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
568
|
-
}
|
|
569
|
-
uninstallMarketItem(name) {
|
|
570
|
-
this.validateItemName(name);
|
|
571
|
-
const targetDir = (0, path_1.join)(this.skillsDir, name);
|
|
572
|
-
if ((0, fs_1.existsSync)(targetDir)) {
|
|
573
|
-
(0, fs_1.rmSync)(targetDir, { recursive: true, force: true });
|
|
574
|
-
}
|
|
575
|
-
const lock = this.loadLockFile();
|
|
576
|
-
delete lock.skills[name];
|
|
577
|
-
this.saveLockFile(lock);
|
|
578
|
-
this.logger.log(`Uninstalled skill '${name}'`);
|
|
579
|
-
this.analytics.track('market.uninstall', {
|
|
580
|
-
item_name: name,
|
|
581
|
-
item_type: 'skill',
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
// --- Installed items ---
|
|
585
|
-
resolveItemSource(dir, projectRoot) {
|
|
586
|
-
const nmDir = (0, path_1.join)(projectRoot, 'node_modules') + path_1.sep;
|
|
587
|
-
if (!dir.startsWith(nmDir) && dir !== (0, path_1.join)(projectRoot, 'node_modules')) {
|
|
588
|
-
return 'local';
|
|
589
|
-
}
|
|
590
|
-
const relFromNm = dir.slice(nmDir.length);
|
|
591
|
-
return relFromNm.startsWith(`@crewx${path_1.sep}`) ? 'builtin' : 'node_modules';
|
|
592
|
-
}
|
|
593
|
-
async listInstalledMarkets() {
|
|
594
|
-
const projectRoot = (0, path_1.dirname)(this.skillsDir);
|
|
595
|
-
const nmDir = (0, path_1.join)(projectRoot, 'node_modules');
|
|
596
|
-
const engine = new skill_1.SkillEngine(projectRoot, (0, fs_1.existsSync)(nmDir) ? [nmDir] : []);
|
|
597
|
-
const discovered = engine.discover();
|
|
598
|
-
const items = [];
|
|
599
|
-
for (const item of discovered) {
|
|
600
|
-
const source = this.resolveItemSource(item.dir, projectRoot);
|
|
601
|
-
if (source === 'local') {
|
|
602
|
-
const skillMdPath = item.skillMdPath ?? (0, path_1.join)(item.dir, 'SKILL.md');
|
|
603
|
-
const { frontmatter } = (0, fs_1.existsSync)(skillMdPath)
|
|
604
|
-
? await this.parseSkillMdAsync(skillMdPath)
|
|
605
|
-
: { frontmatter: {} };
|
|
606
|
-
const installSource = this.detectInstallSource(item.name);
|
|
607
|
-
const cached = this.getGitHubCacheEntry(item.name);
|
|
608
|
-
let updatedAt = cached?.updatedAt;
|
|
609
|
-
if (!updatedAt) {
|
|
610
|
-
try {
|
|
611
|
-
updatedAt = (await (0, promises_1.stat)(skillMdPath)).mtime.toISOString();
|
|
612
|
-
}
|
|
613
|
-
catch {
|
|
614
|
-
// ignore stat errors
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
const localTrust = installSource === 'builtin' ? 'trusted' : 'unverified';
|
|
618
|
-
items.push({
|
|
619
|
-
name: item.name,
|
|
620
|
-
displayName: item.name,
|
|
621
|
-
description: item.description || frontmatter.description || '',
|
|
622
|
-
author: frontmatter.author || '',
|
|
623
|
-
version: item.version || frontmatter.version || '0.0.0',
|
|
624
|
-
tags: frontmatter.tags ? String(frontmatter.tags).split(',').map((t) => t.trim()) : [],
|
|
625
|
-
type: item.type,
|
|
626
|
-
trustLevel: localTrust,
|
|
627
|
-
isVerified: trustLevelToIsVerified(localTrust),
|
|
628
|
-
isInstalled: true,
|
|
629
|
-
source,
|
|
630
|
-
installSource,
|
|
631
|
-
registryId: cached?.registryId,
|
|
632
|
-
authors: cached?.authors,
|
|
633
|
-
updatedAt,
|
|
634
|
-
stars: cached?.stars,
|
|
635
|
-
license: cached?.license,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
const nmTrust = source === 'builtin' ? 'trusted' : 'unverified';
|
|
640
|
-
items.push({
|
|
641
|
-
name: item.name,
|
|
642
|
-
displayName: item.name,
|
|
643
|
-
description: item.description || '',
|
|
644
|
-
author: '',
|
|
645
|
-
version: item.version || '0.0.0',
|
|
646
|
-
tags: [],
|
|
647
|
-
type: item.type,
|
|
648
|
-
trustLevel: nmTrust,
|
|
649
|
-
isVerified: trustLevelToIsVerified(nmTrust),
|
|
650
|
-
isInstalled: false,
|
|
651
|
-
source,
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return { items };
|
|
656
|
-
}
|
|
657
|
-
async checkUpdates() {
|
|
658
|
-
const registries = this.loadRegistries();
|
|
659
|
-
const installed = await this.listInstalledMarkets();
|
|
660
|
-
const cache = this.loadRegistryCache();
|
|
661
|
-
const updates = [];
|
|
662
|
-
const registryTemplates = new Map();
|
|
663
|
-
for (const item of installed.items) {
|
|
664
|
-
if (item.installSource !== 'registry')
|
|
665
|
-
continue;
|
|
666
|
-
const cached = cache[item.name];
|
|
667
|
-
if (!cached?.registryId)
|
|
668
|
-
continue;
|
|
669
|
-
const registry = registries.find((r) => r.id === cached.registryId);
|
|
670
|
-
if (!registry)
|
|
671
|
-
continue;
|
|
672
|
-
let templates = registryTemplates.get(registry.id);
|
|
673
|
-
if (!templates) {
|
|
674
|
-
try {
|
|
675
|
-
templates = await this.fetchTemplates(registry);
|
|
676
|
-
registryTemplates.set(registry.id, templates);
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const remote = templates.find((t) => t.name === item.name);
|
|
683
|
-
if (!remote?.version || !cached.version)
|
|
684
|
-
continue;
|
|
685
|
-
const hasUpdate = remote.version !== cached.version;
|
|
686
|
-
const isCustomized = await this.checkIsCustomized(item.name);
|
|
687
|
-
updates.push({
|
|
688
|
-
name: item.name,
|
|
689
|
-
hasUpdate,
|
|
690
|
-
installedVersion: cached.version,
|
|
691
|
-
remoteVersion: remote.version,
|
|
692
|
-
isCustomized,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
return { updates };
|
|
696
|
-
}
|
|
697
|
-
async checkIsCustomized(_skillName) {
|
|
698
|
-
// Disabled: GitHub tree SHA vs local SHA-256 mismatch (SQA #9)
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
async getInstalledMarketDetail(name) {
|
|
702
|
-
this.validateItemName(name);
|
|
703
|
-
const entryPath = (0, path_1.join)(this.skillsDir, name);
|
|
704
|
-
if (!(0, fs_1.existsSync)(entryPath) || !(0, fs_1.statSync)(entryPath).isDirectory()) {
|
|
705
|
-
throw new common_1.NotFoundException(`Installed skill '${name}' not found`);
|
|
706
|
-
}
|
|
707
|
-
const skillMdPath = (0, path_1.join)(entryPath, 'SKILL.md');
|
|
708
|
-
if (!(0, fs_1.existsSync)(skillMdPath)) {
|
|
709
|
-
throw new common_1.NotFoundException(`Installed skill '${name}' not found`);
|
|
710
|
-
}
|
|
711
|
-
const { frontmatter, body } = this.parseSkillMd(skillMdPath);
|
|
712
|
-
const installSource = this.detectInstallSource(name);
|
|
713
|
-
const cached = this.getGitHubCacheEntry(name);
|
|
714
|
-
let updatedAt = cached?.updatedAt;
|
|
715
|
-
if (!updatedAt) {
|
|
716
|
-
try {
|
|
717
|
-
updatedAt = (0, fs_1.statSync)(skillMdPath).mtime.toISOString();
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
// ignore stat errors
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const detailTrust = installSource === 'builtin' ? 'trusted' : 'unverified';
|
|
724
|
-
return {
|
|
725
|
-
name: frontmatter.name || name,
|
|
726
|
-
displayName: frontmatter.name || name,
|
|
727
|
-
description: frontmatter.description || '',
|
|
728
|
-
author: frontmatter.author || '',
|
|
729
|
-
version: frontmatter.version || '0.0.0',
|
|
730
|
-
tags: frontmatter.tags ? frontmatter.tags.split(',').map((t) => t.trim()) : [],
|
|
731
|
-
type: 'skill',
|
|
732
|
-
trustLevel: detailTrust,
|
|
733
|
-
isVerified: trustLevelToIsVerified(detailTrust),
|
|
734
|
-
isInstalled: true,
|
|
735
|
-
installSource,
|
|
736
|
-
authors: cached?.authors,
|
|
737
|
-
updatedAt,
|
|
738
|
-
stars: cached?.stars,
|
|
739
|
-
license: cached?.license,
|
|
740
|
-
body,
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
parseSkillMd(filePath) {
|
|
744
|
-
try {
|
|
745
|
-
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
746
|
-
return this.parseFrontmatter(content);
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
return { frontmatter: {}, body: '' };
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
async parseSkillMdAsync(filePath) {
|
|
753
|
-
try {
|
|
754
|
-
const content = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
755
|
-
return this.parseFrontmatter(content);
|
|
756
|
-
}
|
|
757
|
-
catch {
|
|
758
|
-
return { frontmatter: {}, body: '' };
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
parseFrontmatter(content) {
|
|
762
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)/);
|
|
763
|
-
if (!match)
|
|
764
|
-
return { frontmatter: {}, body: content };
|
|
765
|
-
const result = {};
|
|
766
|
-
const lines = match[1].split('\n');
|
|
767
|
-
for (const line of lines) {
|
|
768
|
-
const colonIdx = line.indexOf(':');
|
|
769
|
-
if (colonIdx === -1)
|
|
770
|
-
continue;
|
|
771
|
-
const key = line.slice(0, colonIdx).trim();
|
|
772
|
-
let value = line.slice(colonIdx + 1).trim();
|
|
773
|
-
// Strip surrounding quotes
|
|
774
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
775
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
776
|
-
value = value.slice(1, -1);
|
|
777
|
-
}
|
|
778
|
-
if (key)
|
|
779
|
-
result[key] = value;
|
|
780
|
-
}
|
|
781
|
-
return { frontmatter: result, body: match[2] || '' };
|
|
782
|
-
}
|
|
783
|
-
async getRemoteSkillDetail(registryId, skillName) {
|
|
784
|
-
this.validateItemName(skillName);
|
|
785
|
-
const entries = this.loadRegistries();
|
|
786
|
-
const registry = entries.find((r) => r.id === registryId);
|
|
787
|
-
if (!registry) {
|
|
788
|
-
throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
|
|
789
|
-
}
|
|
790
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
791
|
-
if (!parsed) {
|
|
792
|
-
return { body: '' };
|
|
793
|
-
}
|
|
794
|
-
const templates = await this.fetchTemplates(registry);
|
|
795
|
-
const template = templates.find((t) => t.name === skillName);
|
|
796
|
-
const skillPath = template?.path || skillName;
|
|
797
|
-
const candidates = registry.type === 'claude'
|
|
798
|
-
? [`${skillPath}/SKILL.md`, `${skillPath}/README.md`]
|
|
799
|
-
: [`skills/${skillPath}/SKILL.md`, `${skillPath}/SKILL.md`, `skills/${skillPath}/README.md`, `${skillPath}/README.md`];
|
|
800
|
-
const headers = this.getGitHubHeaders(registry);
|
|
801
|
-
for (const candidate of candidates) {
|
|
802
|
-
const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/${candidate}`;
|
|
803
|
-
try {
|
|
804
|
-
const content = await this.fetchUrl(url, headers);
|
|
805
|
-
const { body } = this.parseFrontmatter(content);
|
|
806
|
-
return { body: body.trim() ? body : content };
|
|
807
|
-
}
|
|
808
|
-
catch {
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return { body: '' };
|
|
813
|
-
}
|
|
814
|
-
detectInstallSource(itemName) {
|
|
815
|
-
const builtinDir = (0, path_1.join)(process.cwd(), 'packages', 'built-in', 'skill', 'skills', itemName);
|
|
816
|
-
if ((0, fs_1.existsSync)(builtinDir)) {
|
|
817
|
-
return 'builtin';
|
|
818
|
-
}
|
|
819
|
-
const cache = this.loadRegistryCache();
|
|
820
|
-
if (cache[itemName]?.installedAt || cache[itemName]?.fetchedAt) {
|
|
821
|
-
return 'registry';
|
|
822
|
-
}
|
|
823
|
-
return 'custom';
|
|
824
|
-
}
|
|
825
|
-
// --- Lockfile helpers ---
|
|
826
|
-
loadLockFile() {
|
|
827
|
-
if (!(0, fs_1.existsSync)(this.lockFilePath)) {
|
|
828
|
-
return { lockVersion: 1, generatedAt: new Date().toISOString(), skills: {} };
|
|
829
|
-
}
|
|
830
|
-
const data = JSON.parse((0, fs_1.readFileSync)(this.lockFilePath, 'utf-8'));
|
|
831
|
-
if (data.lockVersion !== 1) {
|
|
832
|
-
throw new Error(`Unsupported lock version: ${data.lockVersion}`);
|
|
833
|
-
}
|
|
834
|
-
return data;
|
|
835
|
-
}
|
|
836
|
-
saveLockFile(lock) {
|
|
837
|
-
lock.generatedAt = new Date().toISOString();
|
|
838
|
-
const dir = (0, path_1.dirname)(this.lockFilePath);
|
|
839
|
-
if (!(0, fs_1.existsSync)(dir)) {
|
|
840
|
-
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
841
|
-
}
|
|
842
|
-
(0, fs_1.writeFileSync)(this.lockFilePath, JSON.stringify(lock, null, 2) + '\n', 'utf-8');
|
|
843
|
-
}
|
|
844
|
-
// --- GitHub cache helpers ---
|
|
845
|
-
loadRegistryCache() {
|
|
846
|
-
if (!(0, fs_1.existsSync)(this.registryCachePath))
|
|
847
|
-
return {};
|
|
848
|
-
try {
|
|
849
|
-
return JSON.parse((0, fs_1.readFileSync)(this.registryCachePath, 'utf-8'));
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
return {};
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
saveRegistryCache(cache) {
|
|
856
|
-
const dir = (0, path_1.join)(this.registryCachePath, '..');
|
|
857
|
-
if (!(0, fs_1.existsSync)(dir)) {
|
|
858
|
-
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
859
|
-
}
|
|
860
|
-
(0, fs_1.writeFileSync)(this.registryCachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
861
|
-
}
|
|
862
|
-
getGitHubCacheEntry(itemName) {
|
|
863
|
-
const cache = this.loadRegistryCache();
|
|
864
|
-
return cache[itemName];
|
|
865
|
-
}
|
|
866
|
-
async fetchGitHubAuthors(registry) {
|
|
867
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
868
|
-
if (!parsed)
|
|
869
|
-
return [];
|
|
870
|
-
try {
|
|
871
|
-
const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/contributors?per_page=5`, { headers: this.getGitHubHeaders(registry) });
|
|
872
|
-
if (!res.ok)
|
|
873
|
-
return [];
|
|
874
|
-
const contributors = await res.json();
|
|
875
|
-
return contributors.map((c) => ({
|
|
876
|
-
login: c.login,
|
|
877
|
-
avatarUrl: c.avatar_url,
|
|
878
|
-
profileUrl: c.html_url,
|
|
879
|
-
contributions: c.contributions,
|
|
880
|
-
}));
|
|
881
|
-
}
|
|
882
|
-
catch {
|
|
883
|
-
return [];
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
async fetchGitHubRepoMeta(registry) {
|
|
887
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
888
|
-
if (!parsed)
|
|
889
|
-
return null;
|
|
890
|
-
try {
|
|
891
|
-
const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, { headers: this.getGitHubHeaders(registry) });
|
|
892
|
-
if (!res.ok)
|
|
893
|
-
return null;
|
|
894
|
-
const data = await res.json();
|
|
895
|
-
return {
|
|
896
|
-
stars: data.stargazers_count ?? 0,
|
|
897
|
-
license: data.license?.spdx_id ?? '',
|
|
898
|
-
updatedAt: data.pushed_at ?? '',
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
catch {
|
|
902
|
-
return null;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
async cacheGitHubMetadata(itemName, registry) {
|
|
906
|
-
const [authors, meta] = await Promise.all([
|
|
907
|
-
this.fetchGitHubAuthors(registry),
|
|
908
|
-
this.fetchGitHubRepoMeta(registry),
|
|
909
|
-
]);
|
|
910
|
-
const cache = this.loadRegistryCache();
|
|
911
|
-
cache[itemName] = {
|
|
912
|
-
...(cache[itemName] ?? {}),
|
|
913
|
-
authors,
|
|
914
|
-
stars: meta?.stars ?? 0,
|
|
915
|
-
license: meta?.license ?? '',
|
|
916
|
-
updatedAt: meta?.updatedAt ?? '',
|
|
917
|
-
fetchedAt: new Date().toISOString(),
|
|
918
|
-
};
|
|
919
|
-
this.saveRegistryCache(cache);
|
|
920
|
-
}
|
|
921
|
-
getGitHubHeaders(registry) {
|
|
922
|
-
const headers = {
|
|
923
|
-
Accept: 'application/vnd.github.v3+json',
|
|
924
|
-
'User-Agent': 'crewx-ui',
|
|
925
|
-
};
|
|
926
|
-
const tokenEnvName = registry?.tokenEnv;
|
|
927
|
-
const token = tokenEnvName ? process.env[tokenEnvName] : process.env.GITHUB_TOKEN;
|
|
928
|
-
if (token) {
|
|
929
|
-
headers['Authorization'] = `token ${token}`;
|
|
930
|
-
}
|
|
931
|
-
return headers;
|
|
932
|
-
}
|
|
933
|
-
validateTokenEnv(tokenEnv) {
|
|
934
|
-
if (!/^[A-Z_][A-Z0-9_]*$/.test(tokenEnv)) {
|
|
935
|
-
throw new common_1.BadRequestException('tokenEnv must match pattern ^[A-Z_][A-Z0-9_]*$ (uppercase letters, digits, underscores only)');
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
// --- Internal helpers ---
|
|
939
|
-
isPlainObject(val) {
|
|
940
|
-
return Object.prototype.toString.call(val) === '[object Object]';
|
|
941
|
-
}
|
|
942
|
-
loadConfig() {
|
|
943
|
-
if (!(0, fs_1.existsSync)(this.configPath)) {
|
|
944
|
-
return {};
|
|
945
|
-
}
|
|
946
|
-
try {
|
|
947
|
-
const content = (0, fs_1.readFileSync)(this.configPath, 'utf-8');
|
|
948
|
-
const parsed = yaml.load(content);
|
|
949
|
-
if (!this.isPlainObject(parsed)) {
|
|
950
|
-
return {};
|
|
951
|
-
}
|
|
952
|
-
return parsed;
|
|
953
|
-
}
|
|
954
|
-
catch {
|
|
955
|
-
return {};
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
saveConfig(config) {
|
|
959
|
-
(0, fs_1.writeFileSync)(this.configPath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
|
|
960
|
-
}
|
|
961
|
-
parseRegistrySourceType(raw) {
|
|
962
|
-
return typeof raw === 'string' && VALID_REGISTRY_SOURCE_TYPES.has(raw)
|
|
963
|
-
? raw
|
|
964
|
-
: 'crewx';
|
|
965
|
-
}
|
|
966
|
-
yamlToRegistryEntry(item) {
|
|
967
|
-
return {
|
|
968
|
-
id: item.id || `reg-${(0, crypto_1.randomUUID)()}`,
|
|
969
|
-
name: item.name || '',
|
|
970
|
-
url: item.url || '',
|
|
971
|
-
type: this.parseRegistrySourceType(item.type),
|
|
972
|
-
trustLevel: boolToTrustLevel(!!item.verified, !!item.default),
|
|
973
|
-
isDefault: !!item.default,
|
|
974
|
-
addedAt: item.addedAt || new Date().toISOString(),
|
|
975
|
-
...(item.tokenEnv ? { tokenEnv: item.tokenEnv } : {}),
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
registryEntryToYaml(entry) {
|
|
979
|
-
return {
|
|
980
|
-
id: entry.id,
|
|
981
|
-
name: entry.name,
|
|
982
|
-
url: entry.url,
|
|
983
|
-
type: entry.type,
|
|
984
|
-
verified: trustLevelToIsVerified(entry.trustLevel),
|
|
985
|
-
default: entry.isDefault,
|
|
986
|
-
addedAt: entry.addedAt,
|
|
987
|
-
...(entry.tokenEnv ? { tokenEnv: entry.tokenEnv } : {}),
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
loadMarketplaceJson() {
|
|
991
|
-
// 1. Memory cache (process lifetime)
|
|
992
|
-
if (this.marketplaceMemoryCache) {
|
|
993
|
-
const age = Date.now() - this.marketplaceMemoryCache.fetchedAt;
|
|
994
|
-
if (age >= MARKETPLACE_CACHE_TTL_MS && !this.marketplaceRefreshPending) {
|
|
995
|
-
this.marketplaceRefreshPending = true;
|
|
996
|
-
this.refreshMarketplaceRemote();
|
|
997
|
-
}
|
|
998
|
-
return this.marketplaceMemoryCache.config;
|
|
999
|
-
}
|
|
1000
|
-
// 2. User-managed override (~/.crewx/marketplace.json)
|
|
1001
|
-
if ((0, fs_1.existsSync)(this.marketplacePath)) {
|
|
1002
|
-
try {
|
|
1003
|
-
const raw = JSON.parse((0, fs_1.readFileSync)(this.marketplacePath, 'utf-8'));
|
|
1004
|
-
const result = MarketplaceConfigSchema.safeParse(raw);
|
|
1005
|
-
if (result.success) {
|
|
1006
|
-
const config = result.data;
|
|
1007
|
-
this.marketplaceMemoryCache = { config, fetchedAt: Date.now() };
|
|
1008
|
-
return config;
|
|
1009
|
-
}
|
|
1010
|
-
this.logger.warn(`Invalid marketplace.json schema: ${result.error.message}`);
|
|
1011
|
-
}
|
|
1012
|
-
catch {
|
|
1013
|
-
this.logger.warn('Failed to parse marketplace.json');
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
// 3. Disk cache (~/.crewx/marketplace.cache.json)
|
|
1017
|
-
const diskCache = this.loadMarketplaceDiskCache();
|
|
1018
|
-
if (diskCache) {
|
|
1019
|
-
this.marketplaceMemoryCache = diskCache;
|
|
1020
|
-
if (Date.now() - diskCache.fetchedAt >= MARKETPLACE_CACHE_TTL_MS && !this.marketplaceRefreshPending) {
|
|
1021
|
-
this.marketplaceRefreshPending = true;
|
|
1022
|
-
this.refreshMarketplaceRemote();
|
|
1023
|
-
}
|
|
1024
|
-
return diskCache.config;
|
|
1025
|
-
}
|
|
1026
|
-
// 4. No local data — return null, let loadRegistries() handle fallback.
|
|
1027
|
-
return null;
|
|
1028
|
-
}
|
|
1029
|
-
loadMarketplaceDiskCache() {
|
|
1030
|
-
if (!(0, fs_1.existsSync)(this.marketplaceCachePath))
|
|
1031
|
-
return null;
|
|
1032
|
-
try {
|
|
1033
|
-
const raw = JSON.parse((0, fs_1.readFileSync)(this.marketplaceCachePath, 'utf-8'));
|
|
1034
|
-
if (!raw || typeof raw.fetchedAt !== 'number' || !raw.config)
|
|
1035
|
-
return null;
|
|
1036
|
-
const result = MarketplaceConfigSchema.safeParse(raw.config);
|
|
1037
|
-
if (!result.success)
|
|
1038
|
-
return null;
|
|
1039
|
-
return {
|
|
1040
|
-
config: result.data,
|
|
1041
|
-
etag: raw.etag,
|
|
1042
|
-
lastModified: raw.lastModified,
|
|
1043
|
-
fetchedAt: raw.fetchedAt,
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
catch {
|
|
1047
|
-
return null;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
saveMarketplaceDiskCache(cache) {
|
|
1051
|
-
try {
|
|
1052
|
-
const dir = (0, path_1.dirname)(this.marketplaceCachePath);
|
|
1053
|
-
if (!(0, fs_1.existsSync)(dir))
|
|
1054
|
-
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
1055
|
-
(0, fs_1.writeFileSync)(this.marketplaceCachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1056
|
-
}
|
|
1057
|
-
catch {
|
|
1058
|
-
this.logger.warn('Failed to save marketplace disk cache');
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
refreshMarketplaceRemote() {
|
|
1062
|
-
const existing = this.marketplaceMemoryCache;
|
|
1063
|
-
const headers = {};
|
|
1064
|
-
if (existing?.etag)
|
|
1065
|
-
headers['If-None-Match'] = existing.etag;
|
|
1066
|
-
if (existing?.lastModified)
|
|
1067
|
-
headers['If-Modified-Since'] = existing.lastModified;
|
|
1068
|
-
fetch(MARKETPLACE_REMOTE_URL, { headers, signal: AbortSignal.timeout(10_000) })
|
|
1069
|
-
.then(async (res) => {
|
|
1070
|
-
if (res.status === 304) {
|
|
1071
|
-
if (existing) {
|
|
1072
|
-
existing.fetchedAt = Date.now();
|
|
1073
|
-
this.saveMarketplaceDiskCache(existing);
|
|
1074
|
-
}
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
if (!res.ok)
|
|
1078
|
-
return;
|
|
1079
|
-
const text = await res.text();
|
|
1080
|
-
const parsed = MarketplaceConfigSchema.safeParse(JSON.parse(text));
|
|
1081
|
-
if (!parsed.success)
|
|
1082
|
-
return;
|
|
1083
|
-
const cache = {
|
|
1084
|
-
config: parsed.data,
|
|
1085
|
-
etag: res.headers.get('etag') ?? undefined,
|
|
1086
|
-
lastModified: res.headers.get('last-modified') ?? undefined,
|
|
1087
|
-
fetchedAt: Date.now(),
|
|
1088
|
-
};
|
|
1089
|
-
this.marketplaceMemoryCache = cache;
|
|
1090
|
-
this.saveMarketplaceDiskCache(cache);
|
|
1091
|
-
})
|
|
1092
|
-
.catch(() => {
|
|
1093
|
-
// Network failure — seed disk cache from bundled default if nothing cached yet
|
|
1094
|
-
if (!this.marketplaceMemoryCache && !(0, fs_1.existsSync)(this.marketplaceCachePath)) {
|
|
1095
|
-
const bundled = loadBundledMarketplace();
|
|
1096
|
-
if (bundled) {
|
|
1097
|
-
const cache = { config: bundled, fetchedAt: Date.now() };
|
|
1098
|
-
this.marketplaceMemoryCache = cache;
|
|
1099
|
-
this.saveMarketplaceDiskCache(cache);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
})
|
|
1103
|
-
.finally(() => {
|
|
1104
|
-
this.marketplaceRefreshPending = false;
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
loadWorkspaceMarketplaceJson() {
|
|
1108
|
-
if (!(0, fs_1.existsSync)(this.workspaceMarketplacePath))
|
|
1109
|
-
return null;
|
|
1110
|
-
try {
|
|
1111
|
-
const raw = JSON.parse((0, fs_1.readFileSync)(this.workspaceMarketplacePath, 'utf-8'));
|
|
1112
|
-
const result = MarketplaceConfigSchema.safeParse(raw);
|
|
1113
|
-
if (!result.success) {
|
|
1114
|
-
this.logger.warn(`Invalid workspace marketplace.json schema: ${result.error.message}`);
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
return result.data;
|
|
1118
|
-
}
|
|
1119
|
-
catch {
|
|
1120
|
-
this.logger.warn('Failed to parse workspace marketplace.json');
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
marketplaceEntryToRegistry(item) {
|
|
1125
|
-
const trust = item.trust ?? boolToTrustLevel(item.verified ?? true, !!item.default);
|
|
1126
|
-
return {
|
|
1127
|
-
id: item.id,
|
|
1128
|
-
name: item.name || '',
|
|
1129
|
-
url: item.url,
|
|
1130
|
-
type: this.parseRegistrySourceType(item.type),
|
|
1131
|
-
trustLevel: trust,
|
|
1132
|
-
isDefault: !!item.default,
|
|
1133
|
-
addedAt: new Date().toISOString(),
|
|
1134
|
-
...(item.tokenEnv ? { tokenEnv: item.tokenEnv } : {}),
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
isBuiltinExempt(reg) {
|
|
1138
|
-
return (reg.name.startsWith('@crewx/') ||
|
|
1139
|
-
this.normalizeUrl(reg.url) === this.normalizeUrl(DEFAULT_REGISTRY_URL));
|
|
1140
|
-
}
|
|
1141
|
-
mergeRegistries(global, workspace, forceDisabled) {
|
|
1142
|
-
const disabledSet = new Set(forceDisabled);
|
|
1143
|
-
const merged = new Map();
|
|
1144
|
-
for (const reg of global) {
|
|
1145
|
-
if (!disabledSet.has(reg.id) || this.isBuiltinExempt(reg)) {
|
|
1146
|
-
merged.set(this.normalizeUrl(reg.url), { ...reg, source: 'global' });
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
for (const reg of workspace) {
|
|
1150
|
-
if (disabledSet.has(reg.id) && !this.isBuiltinExempt(reg))
|
|
1151
|
-
continue;
|
|
1152
|
-
const key = this.normalizeUrl(reg.url);
|
|
1153
|
-
if (merged.has(key))
|
|
1154
|
-
continue;
|
|
1155
|
-
merged.set(key, { ...reg, source: 'workspace', trustLevel: 'unverified' });
|
|
1156
|
-
}
|
|
1157
|
-
return Array.from(merged.values());
|
|
1158
|
-
}
|
|
1159
|
-
loadRegistries() {
|
|
1160
|
-
const mpConfig = this.loadMarketplaceJson();
|
|
1161
|
-
const wsMpConfig = this.loadWorkspaceMarketplaceJson();
|
|
1162
|
-
const config = this.loadConfig();
|
|
1163
|
-
const registriesYaml = config?.skills?.registries;
|
|
1164
|
-
if (!mpConfig && !wsMpConfig) {
|
|
1165
|
-
// No marketplace.json anywhere — seed disk cache from remote for next restart
|
|
1166
|
-
if (!this.marketplaceRefreshPending) {
|
|
1167
|
-
this.marketplaceRefreshPending = true;
|
|
1168
|
-
this.refreshMarketplaceRemote();
|
|
1169
|
-
}
|
|
1170
|
-
if (!registriesYaml || registriesYaml.length === 0) {
|
|
1171
|
-
const defaults = this.getDefaultRegistries();
|
|
1172
|
-
this.saveRegistries(defaults);
|
|
1173
|
-
return defaults;
|
|
1174
|
-
}
|
|
1175
|
-
try {
|
|
1176
|
-
const entries = registriesYaml.map((item) => this.yamlToRegistryEntry(item));
|
|
1177
|
-
return this.ensureDefaultRegistry(entries);
|
|
1178
|
-
}
|
|
1179
|
-
catch {
|
|
1180
|
-
const defaults = this.getDefaultRegistries();
|
|
1181
|
-
this.saveRegistries(defaults);
|
|
1182
|
-
return defaults;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
const globalRegistries = mpConfig
|
|
1186
|
-
? mpConfig.registries.map((r) => this.marketplaceEntryToRegistry(r))
|
|
1187
|
-
: [];
|
|
1188
|
-
const forceDisabled = mpConfig?.forceDisabled ?? [];
|
|
1189
|
-
let workspaceRegistries = [];
|
|
1190
|
-
if (wsMpConfig) {
|
|
1191
|
-
workspaceRegistries = wsMpConfig.registries.map((r) => this.marketplaceEntryToRegistry(r));
|
|
1192
|
-
}
|
|
1193
|
-
if (registriesYaml && registriesYaml.length > 0) {
|
|
1194
|
-
try {
|
|
1195
|
-
const yamlEntries = registriesYaml.map((item) => this.yamlToRegistryEntry(item));
|
|
1196
|
-
workspaceRegistries = [...workspaceRegistries, ...yamlEntries];
|
|
1197
|
-
}
|
|
1198
|
-
catch {
|
|
1199
|
-
// ignore workspace parse errors
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
const merged = this.mergeRegistries(globalRegistries, workspaceRegistries, forceDisabled);
|
|
1203
|
-
return this.ensureDefaultRegistry(merged);
|
|
1204
|
-
}
|
|
1205
|
-
saveRegistries(entries) {
|
|
1206
|
-
const config = this.loadConfig();
|
|
1207
|
-
if (!this.isPlainObject(config.skills)) {
|
|
1208
|
-
config.skills = {};
|
|
1209
|
-
}
|
|
1210
|
-
const persistable = entries.filter((e) => e.source !== 'global');
|
|
1211
|
-
config.skills.registries = persistable.map((e) => this.registryEntryToYaml(e));
|
|
1212
|
-
this.saveConfig(config);
|
|
1213
|
-
}
|
|
1214
|
-
getDefaultRegistries() {
|
|
1215
|
-
return [
|
|
1216
|
-
{
|
|
1217
|
-
id: 'default-crewx-templates',
|
|
1218
|
-
name: DEFAULT_REGISTRY_NAME,
|
|
1219
|
-
url: DEFAULT_REGISTRY_URL,
|
|
1220
|
-
type: 'crewx',
|
|
1221
|
-
trustLevel: 'verified',
|
|
1222
|
-
isDefault: true,
|
|
1223
|
-
addedAt: new Date().toISOString(),
|
|
1224
|
-
},
|
|
1225
|
-
];
|
|
1226
|
-
}
|
|
1227
|
-
ensureDefaultRegistry(entries) {
|
|
1228
|
-
const defaultUrl = this.normalizeUrl(DEFAULT_REGISTRY_URL);
|
|
1229
|
-
const officialEntry = entries.find((r) => this.normalizeUrl(r.url) === defaultUrl);
|
|
1230
|
-
if (!officialEntry) {
|
|
1231
|
-
entries.unshift(this.getDefaultRegistries()[0]);
|
|
1232
|
-
this.saveRegistries(entries);
|
|
1233
|
-
}
|
|
1234
|
-
else if (!officialEntry.isDefault || officialEntry.trustLevel === 'unverified') {
|
|
1235
|
-
officialEntry.isDefault = true;
|
|
1236
|
-
officialEntry.trustLevel = 'verified';
|
|
1237
|
-
this.saveRegistries(entries);
|
|
1238
|
-
}
|
|
1239
|
-
return entries;
|
|
1240
|
-
}
|
|
1241
|
-
async fetchTemplates(registry) {
|
|
1242
|
-
const cached = this.templateCache.get(registry.id);
|
|
1243
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
1244
|
-
return cached.data;
|
|
1245
|
-
}
|
|
1246
|
-
if (registry.type === 'claude') {
|
|
1247
|
-
return this.fetchClaudeTemplates(registry);
|
|
1248
|
-
}
|
|
1249
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
1250
|
-
if (!parsed) {
|
|
1251
|
-
this.logger.warn(`Invalid registry URL: ${registry.url}`);
|
|
1252
|
-
return [];
|
|
1253
|
-
}
|
|
1254
|
-
const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/templates.json`;
|
|
1255
|
-
try {
|
|
1256
|
-
const content = await this.fetchUrl(url, this.getGitHubHeaders(registry));
|
|
1257
|
-
const json = JSON.parse(content);
|
|
1258
|
-
const rawTemplates = json.templates || [];
|
|
1259
|
-
const templates = [];
|
|
1260
|
-
for (const raw of rawTemplates) {
|
|
1261
|
-
const result = TemplateEntrySchema.safeParse(raw);
|
|
1262
|
-
if (result.success) {
|
|
1263
|
-
templates.push(result.data);
|
|
1264
|
-
}
|
|
1265
|
-
else {
|
|
1266
|
-
this.logger.warn(`Skipping invalid template entry: ${JSON.stringify(result.error.flatten().fieldErrors)}`);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
this.templateCache.set(registry.id, {
|
|
1270
|
-
data: templates,
|
|
1271
|
-
fetchedAt: Date.now(),
|
|
1272
|
-
});
|
|
1273
|
-
return templates;
|
|
1274
|
-
}
|
|
1275
|
-
catch (err) {
|
|
1276
|
-
this.logger.warn(`Failed to fetch templates.json from ${registry.url}: ${err}`);
|
|
1277
|
-
return [];
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
async fetchClaudeTemplates(registry) {
|
|
1281
|
-
const parsed = this.parseGitHubUrl(registry.url);
|
|
1282
|
-
if (!parsed) {
|
|
1283
|
-
this.logger.warn(`Invalid registry URL: ${registry.url}`);
|
|
1284
|
-
return [];
|
|
1285
|
-
}
|
|
1286
|
-
const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/.claude-plugin/marketplace.json`;
|
|
1287
|
-
try {
|
|
1288
|
-
const content = await this.fetchUrl(url, this.getGitHubHeaders(registry));
|
|
1289
|
-
const json = JSON.parse(content);
|
|
1290
|
-
const author = json.owner?.name ?? parsed.owner;
|
|
1291
|
-
const version = json.metadata?.version ?? '1.0.0';
|
|
1292
|
-
const templates = [];
|
|
1293
|
-
for (const plugin of json.plugins ?? []) {
|
|
1294
|
-
if (plugin.skills && plugin.skills.length > 0) {
|
|
1295
|
-
for (const skillRef of plugin.skills) {
|
|
1296
|
-
const skillPath = skillRef.replace(/^\.\//, '');
|
|
1297
|
-
const skillName = skillPath.split('/').pop() ?? skillPath;
|
|
1298
|
-
const displayName = skillName
|
|
1299
|
-
.split('-')
|
|
1300
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1301
|
-
.join(' ');
|
|
1302
|
-
templates.push({
|
|
1303
|
-
name: skillName,
|
|
1304
|
-
displayName,
|
|
1305
|
-
description: plugin.description,
|
|
1306
|
-
path: skillPath,
|
|
1307
|
-
type: 'skill',
|
|
1308
|
-
author,
|
|
1309
|
-
version,
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
else if (plugin.source != null) {
|
|
1314
|
-
const discovered = await this.discoverSkillsViaSource(plugin.source, parsed, registry);
|
|
1315
|
-
for (const skillName of discovered) {
|
|
1316
|
-
const displayName = skillName
|
|
1317
|
-
.split('-')
|
|
1318
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1319
|
-
.join(' ');
|
|
1320
|
-
templates.push({
|
|
1321
|
-
name: skillName,
|
|
1322
|
-
displayName,
|
|
1323
|
-
description: plugin.description,
|
|
1324
|
-
path: `skills/${skillName}`,
|
|
1325
|
-
type: 'skill',
|
|
1326
|
-
author,
|
|
1327
|
-
version,
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
this.templateCache.set(registry.id, {
|
|
1333
|
-
data: templates,
|
|
1334
|
-
fetchedAt: Date.now(),
|
|
1335
|
-
});
|
|
1336
|
-
return templates;
|
|
1337
|
-
}
|
|
1338
|
-
catch (err) {
|
|
1339
|
-
this.logger.warn(`Failed to fetch .claude-plugin/marketplace.json from ${registry.url}: ${err}`);
|
|
1340
|
-
return [];
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
async discoverSkillsViaSource(source, registryParsed, registry) {
|
|
1344
|
-
let owner;
|
|
1345
|
-
let repo;
|
|
1346
|
-
let ref = 'main';
|
|
1347
|
-
let basePath = '';
|
|
1348
|
-
if (typeof source === 'string') {
|
|
1349
|
-
owner = registryParsed.owner;
|
|
1350
|
-
repo = registryParsed.repo;
|
|
1351
|
-
basePath = source.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
1352
|
-
}
|
|
1353
|
-
else {
|
|
1354
|
-
if (source.url) {
|
|
1355
|
-
const srcParsed = this.parseGitHubUrl(source.url);
|
|
1356
|
-
if (!srcParsed)
|
|
1357
|
-
return [];
|
|
1358
|
-
owner = srcParsed.owner;
|
|
1359
|
-
repo = srcParsed.repo;
|
|
1360
|
-
}
|
|
1361
|
-
else {
|
|
1362
|
-
owner = registryParsed.owner;
|
|
1363
|
-
repo = registryParsed.repo;
|
|
1364
|
-
}
|
|
1365
|
-
if (source.ref)
|
|
1366
|
-
ref = source.ref;
|
|
1367
|
-
basePath = (source.path ?? '').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
1368
|
-
}
|
|
1369
|
-
try {
|
|
1370
|
-
const treesUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`;
|
|
1371
|
-
const treesRes = await fetch(treesUrl, { headers: this.getGitHubHeaders(registry) });
|
|
1372
|
-
if (!treesRes.ok)
|
|
1373
|
-
return [];
|
|
1374
|
-
const treesData = await treesRes.json();
|
|
1375
|
-
const skillsPrefix = basePath ? `${basePath}/skills/` : 'skills/';
|
|
1376
|
-
const skillNames = new Set();
|
|
1377
|
-
for (const item of treesData.tree) {
|
|
1378
|
-
if (item.type !== 'blob')
|
|
1379
|
-
continue;
|
|
1380
|
-
if (!item.path.startsWith(skillsPrefix))
|
|
1381
|
-
continue;
|
|
1382
|
-
const rel = item.path.slice(skillsPrefix.length);
|
|
1383
|
-
const parts = rel.split('/');
|
|
1384
|
-
if (parts.length === 2 && parts[1] === 'SKILL.md') {
|
|
1385
|
-
skillNames.add(parts[0]);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
return [...skillNames];
|
|
1389
|
-
}
|
|
1390
|
-
catch {
|
|
1391
|
-
return [];
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
async fetchUrl(url, headers) {
|
|
1395
|
-
const response = await fetch(url, headers ? { headers } : undefined);
|
|
1396
|
-
if (!response.ok) {
|
|
1397
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1398
|
-
}
|
|
1399
|
-
return response.text();
|
|
1400
|
-
}
|
|
1401
|
-
parseGitHubUrl(url) {
|
|
1402
|
-
const match = url.match(/(?:https?:\/\/)?github\.com\/([^/]+)\/([^/.]+)/);
|
|
1403
|
-
if (!match)
|
|
1404
|
-
return null;
|
|
1405
|
-
return { owner: match[1], repo: match[2] };
|
|
1406
|
-
}
|
|
1407
|
-
normalizeUrl(url) {
|
|
1408
|
-
return url
|
|
1409
|
-
.replace(/\.git$/, '')
|
|
1410
|
-
.replace(/\/+$/, '')
|
|
1411
|
-
.toLowerCase();
|
|
1412
|
-
}
|
|
1413
|
-
async getInstalledItemNames() {
|
|
1414
|
-
if (!(0, fs_1.existsSync)(this.skillsDir))
|
|
1415
|
-
return new Set();
|
|
1416
|
-
try {
|
|
1417
|
-
const entries = await (0, promises_1.readdir)(this.skillsDir);
|
|
1418
|
-
const names = new Set();
|
|
1419
|
-
for (const entry of entries) {
|
|
1420
|
-
const entryPath = (0, path_1.join)(this.skillsDir, entry);
|
|
1421
|
-
try {
|
|
1422
|
-
if ((await (0, promises_1.stat)(entryPath)).isDirectory()) {
|
|
1423
|
-
if ((0, fs_1.existsSync)((0, path_1.join)(entryPath, 'SKILL.md'))) {
|
|
1424
|
-
names.add(entry);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
catch {
|
|
1429
|
-
// skip inaccessible entries
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return names;
|
|
1433
|
-
}
|
|
1434
|
-
catch {
|
|
1435
|
-
return new Set();
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
getCachedItemCount(registryId) {
|
|
1439
|
-
const cached = this.templateCache.get(registryId);
|
|
1440
|
-
return cached ? cached.data.length : 0;
|
|
1441
|
-
}
|
|
1442
|
-
};
|
|
1443
|
-
exports.MarketService = MarketService;
|
|
1444
|
-
exports.MarketService = MarketService = MarketService_1 = __decorate([
|
|
1445
|
-
(0, common_1.Injectable)(),
|
|
1446
|
-
__metadata("design:paramtypes", [analytics_client_js_1.AnalyticsService])
|
|
1447
|
-
], MarketService);
|