crewx 0.8.9-rc.2 → 0.8.9-rc.21
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-BpajB_Ex.js → MarketPage-Cl9KvLAo.js} +1 -1
- package/dist/assets/PromptTab-gj_BvClc.js +27 -0
- package/dist/assets/{arc-B0fFLEOc.js → arc-DDzVSXZh.js} +1 -1
- package/dist/assets/{architectureDiagram-UYN6MBPD-CrC7MKTM.js → architectureDiagram-UYN6MBPD-p8OOXe5w.js} +1 -1
- package/dist/assets/{blockDiagram-ZHA2E4KO-DJsG-9x2.js → blockDiagram-ZHA2E4KO-BTlfXGnf.js} +7 -7
- package/dist/assets/{c4Diagram-6F5ED5ID-DlkyjvkS.js → c4Diagram-6F5ED5ID-CKbrDPWE.js} +1 -1
- package/dist/assets/channel-Hzz-cXd0.js +1 -0
- package/dist/assets/{chunk-5HRBRIJM-B_Csqjvw.js → chunk-5HRBRIJM-otR_A_1G.js} +1 -1
- package/dist/assets/{chunk-7U56Z5CX-BlNdzxbh.js → chunk-7U56Z5CX-BliJIH4-.js} +1 -1
- package/dist/assets/{chunk-ASOPGD6M-BiFWfa3w.js → chunk-ASOPGD6M-CmzypD4Z.js} +1 -1
- package/dist/assets/{chunk-KFBOBJHC-ocugjY-9.js → chunk-KFBOBJHC-BW4u3lPL.js} +1 -1
- package/dist/assets/{chunk-T2TOU4HS-kUreJ-bv.js → chunk-T2TOU4HS-BAp6Xscz.js} +1 -1
- package/dist/assets/{chunk-TMUBEWPD-Du19Q8X-.js → chunk-TMUBEWPD-Bv_RKIlz.js} +1 -1
- package/dist/assets/classDiagram-LNE6IOMH-CXR-DJKP.js +1 -0
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-CXR-DJKP.js +1 -0
- package/dist/assets/{dagre-4EVJKHTY-8N5qxubq.js → dagre-4EVJKHTY-sYyP6lkA.js} +1 -1
- package/dist/assets/{diagram-QW4FP2JN-CjLH9uYk.js → diagram-QW4FP2JN-XWldv7Gz.js} +1 -1
- package/dist/assets/{erDiagram-6RL3IURR-CXgt8NtN.js → erDiagram-6RL3IURR-Cd8OVAQ0.js} +1 -1
- package/dist/assets/{flowDiagram-7ASYPVHJ-CdFlmdmT.js → flowDiagram-7ASYPVHJ-Cc3onSB6.js} +1 -1
- package/dist/assets/{ganttDiagram-NTVNEXSI-DDjK4yxy.js → ganttDiagram-NTVNEXSI-Q6HxMUkA.js} +1 -1
- package/dist/assets/{gitGraph-YCYPL57B-Bd9jYZ-C.js → gitGraph-YCYPL57B-BUh0zUco.js} +1 -1
- package/dist/assets/{gitGraphDiagram-NRZ2UAAF-pmwetPkN.js → gitGraphDiagram-NRZ2UAAF-CvPqw-ya.js} +1 -1
- package/dist/assets/{infoDiagram-A4XQUW5V-BoDEaOyd.js → infoDiagram-A4XQUW5V-CbKGdGqb.js} +1 -1
- package/dist/assets/{journeyDiagram-G5WM74LC-D1LRuhB3.js → journeyDiagram-G5WM74LC-BXyAvuxS.js} +1 -1
- package/dist/assets/{kanban-definition-QRCXZQQD-B66SGjA2.js → kanban-definition-QRCXZQQD-CxBFX9if.js} +1 -1
- package/dist/assets/{linear-VMCGabuP.js → linear-Bu0P9vdO.js} +1 -1
- package/dist/assets/main-AYmbxFu4.css +10 -0
- package/dist/assets/main-CcrxU2Tf.js +1454 -0
- package/dist/assets/{mindmap-definition-GWI6TPTV--NljwHD9.js → mindmap-definition-GWI6TPTV-BbXI83yG.js} +1 -1
- package/dist/assets/{pieDiagram-YF2LJOPJ-CP4IPp3Q.js → pieDiagram-YF2LJOPJ-DuXspLPW.js} +1 -1
- package/dist/assets/{quadrantDiagram-OS5C2QUG-baFc-DtQ.js → quadrantDiagram-OS5C2QUG-DnngSA8p.js} +1 -1
- package/dist/assets/{requirementDiagram-MIRIMTAZ-BTWwNcnt.js → requirementDiagram-MIRIMTAZ-ChJE8XYc.js} +1 -1
- package/dist/assets/{sankeyDiagram-Y46BX6SQ-CnrACC1i.js → sankeyDiagram-Y46BX6SQ-BlkQKMk0.js} +1 -1
- package/dist/assets/{sequenceDiagram-G6AWOVSC-Ct5WkHJl.js → sequenceDiagram-G6AWOVSC-BvYCRnWL.js} +1 -1
- package/dist/assets/{stateDiagram-MAYHULR4-DeZHZmIy.js → stateDiagram-MAYHULR4-Cb6BiEq_.js} +1 -1
- package/dist/assets/stateDiagram-v2-4JROLMXI-31Hvt1M0.js +1 -0
- package/dist/assets/{timeline-definition-U7ZMHBDA-BZfUE50X.js → timeline-definition-U7ZMHBDA-w1-dGiEP.js} +1 -1
- package/dist/assets/{xychartDiagram-6QU3TZC5-DV6BFyHM.js → xychartDiagram-6QU3TZC5-B85iIKzT.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/__stubs__/ccusage-codex.js +8 -0
- package/dist-server/app.module.js +2 -0
- package/dist-server/common/limits/defaults.js +3 -3
- package/dist-server/domain/agent/agent.service.js +11 -0
- package/dist-server/domain/cli/cli.service.js +3 -3
- package/dist-server/domain/cron/cron.service.js +5 -1
- package/dist-server/domain/doc/doc.controller.js +22 -0
- package/dist-server/domain/doc/doc.service.js +42 -10
- package/dist-server/domain/git/git.controller.js +180 -0
- package/dist-server/domain/git/git.module.js +2 -0
- package/dist-server/domain/git/git.service.js +327 -34
- package/dist-server/domain/message/message.service.js +18 -8
- package/dist-server/domain/onboarding/onboarding.service.js +1 -1
- package/dist-server/domain/onboarding/presets/custom.js +2 -2
- package/dist-server/domain/onboarding/presets/dev-team.js +7 -7
- package/dist-server/domain/onboarding/presets/marketing-team.js +5 -5
- package/dist-server/domain/onboarding/presets/planning-team.js +5 -5
- package/dist-server/domain/onboarding/presets/solo.js +3 -3
- package/dist-server/domain/settings/settings.module.js +2 -0
- package/dist-server/domain/settings/settings.service.js +75 -8
- package/dist-server/domain/task/task.service.js +7 -7
- package/dist-server/domain/thread/thread.service.js +2 -1
- package/dist-server/domain/usage/usage.controller.js +30 -0
- package/dist-server/domain/usage/usage.service.js +212 -0
- package/dist-server/domain/wi/wi.controller.js +43 -0
- package/dist-server/domain/wi/wi.module.js +22 -0
- package/dist-server/domain/wi/wi.service.js +110 -0
- package/dist-server/domain/wi/wi.types.js +2 -0
- package/dist-server/domain/workspace/workspace.service.js +4 -1
- package/dist-server/modules/crewx-pool.service.js +194 -53
- package/dist-server/repository/task.repository.js +5 -1
- package/package.json +16 -14
- package/packages/cli/dist/commands/agent.js +1 -1
- package/packages/cli/dist/commands/init.d.ts +2 -0
- package/packages/cli/dist/commands/init.js +198 -6
- package/packages/cli/package.json +3 -2
- package/dist/assets/PromptTab-HhD4MQcW.js +0 -37
- package/dist/assets/channel-ClmdPiZd.js +0 -1
- package/dist/assets/classDiagram-LNE6IOMH-Q0hRWLPF.js +0 -1
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-Q0hRWLPF.js +0 -1
- package/dist/assets/main-AqROOc4i.css +0 -10
- package/dist/assets/main-DWCTap27.js +0 -1289
- package/dist/assets/stateDiagram-v2-4JROLMXI-DVsMMyDB.js +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.GitController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const swagger_1 = require("@nestjs/swagger");
|
|
18
|
+
const class_validator_1 = require("class-validator");
|
|
19
|
+
const git_service_js_1 = require("./git.service.js");
|
|
20
|
+
const workspace_decorator_js_1 = require("../../common/decorators/workspace.decorator.js");
|
|
21
|
+
class CommitDto {
|
|
22
|
+
message;
|
|
23
|
+
filepaths;
|
|
24
|
+
}
|
|
25
|
+
__decorate([
|
|
26
|
+
(0, class_validator_1.IsString)(),
|
|
27
|
+
__metadata("design:type", String)
|
|
28
|
+
], CommitDto.prototype, "message", void 0);
|
|
29
|
+
__decorate([
|
|
30
|
+
(0, class_validator_1.IsOptional)(),
|
|
31
|
+
(0, class_validator_1.IsArray)(),
|
|
32
|
+
(0, class_validator_1.IsString)({ each: true }),
|
|
33
|
+
__metadata("design:type", Array)
|
|
34
|
+
], CommitDto.prototype, "filepaths", void 0);
|
|
35
|
+
class PushDto {
|
|
36
|
+
remote;
|
|
37
|
+
branch;
|
|
38
|
+
}
|
|
39
|
+
__decorate([
|
|
40
|
+
(0, class_validator_1.IsOptional)(),
|
|
41
|
+
(0, class_validator_1.IsString)(),
|
|
42
|
+
__metadata("design:type", String)
|
|
43
|
+
], PushDto.prototype, "remote", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
(0, class_validator_1.IsOptional)(),
|
|
46
|
+
(0, class_validator_1.IsString)(),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], PushDto.prototype, "branch", void 0);
|
|
49
|
+
class RevertDto {
|
|
50
|
+
mode;
|
|
51
|
+
filepath;
|
|
52
|
+
commitSha;
|
|
53
|
+
backupSha;
|
|
54
|
+
}
|
|
55
|
+
__decorate([
|
|
56
|
+
(0, class_validator_1.IsIn)(['file', 'commit', 'restore-backup']),
|
|
57
|
+
__metadata("design:type", String)
|
|
58
|
+
], RevertDto.prototype, "mode", void 0);
|
|
59
|
+
__decorate([
|
|
60
|
+
(0, class_validator_1.ValidateIf)((o) => o.mode === 'file'),
|
|
61
|
+
(0, class_validator_1.IsString)(),
|
|
62
|
+
__metadata("design:type", String)
|
|
63
|
+
], RevertDto.prototype, "filepath", void 0);
|
|
64
|
+
__decorate([
|
|
65
|
+
(0, class_validator_1.ValidateIf)((o) => o.mode === 'commit'),
|
|
66
|
+
(0, class_validator_1.IsString)(),
|
|
67
|
+
__metadata("design:type", String)
|
|
68
|
+
], RevertDto.prototype, "commitSha", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
(0, class_validator_1.ValidateIf)((o) => o.mode === 'restore-backup'),
|
|
71
|
+
(0, class_validator_1.IsString)(),
|
|
72
|
+
__metadata("design:type", String)
|
|
73
|
+
], RevertDto.prototype, "backupSha", void 0);
|
|
74
|
+
let GitController = class GitController {
|
|
75
|
+
gitService;
|
|
76
|
+
constructor(gitService) {
|
|
77
|
+
this.gitService = gitService;
|
|
78
|
+
}
|
|
79
|
+
// GIT.STATUS: Get working tree status
|
|
80
|
+
async getStatus(_ws) {
|
|
81
|
+
const data = await this.gitService.getStatus();
|
|
82
|
+
return { success: true, data };
|
|
83
|
+
}
|
|
84
|
+
// GIT.HISTORY: Get commit history
|
|
85
|
+
async getHistory(_ws, filepath, depth) {
|
|
86
|
+
const depthNum = depth ? parseInt(depth, 10) : 50;
|
|
87
|
+
const commits = await this.gitService.getHistory(filepath, depthNum);
|
|
88
|
+
return { success: true, data: { commits } };
|
|
89
|
+
}
|
|
90
|
+
// GIT.DIFF: Get diff (working tree or specific commit)
|
|
91
|
+
async getDiff(_ws, commitSha, filepath) {
|
|
92
|
+
const diffs = await this.gitService.getFileDiff(commitSha, filepath);
|
|
93
|
+
return { success: true, data: { diffs } };
|
|
94
|
+
}
|
|
95
|
+
// GIT.COMMIT: Create a commit (save point)
|
|
96
|
+
async commit(_ws, dto) {
|
|
97
|
+
const sha = await this.gitService.commitFiles(dto.filepaths, dto.message);
|
|
98
|
+
return { success: true, data: { sha } };
|
|
99
|
+
}
|
|
100
|
+
// GIT.PUSH: Push local commits to remote origin
|
|
101
|
+
async push(_ws, dto) {
|
|
102
|
+
const data = await this.gitService.push({ remote: dto.remote, branch: dto.branch });
|
|
103
|
+
return { success: true, data };
|
|
104
|
+
}
|
|
105
|
+
// GIT.REVERT: Revert changes (file, commit, or restore backup)
|
|
106
|
+
async revert(_ws, dto) {
|
|
107
|
+
const data = await this.gitService.revert({
|
|
108
|
+
mode: dto.mode,
|
|
109
|
+
filepath: dto.filepath,
|
|
110
|
+
commitSha: dto.commitSha,
|
|
111
|
+
backupSha: dto.backupSha,
|
|
112
|
+
});
|
|
113
|
+
return { success: true, data };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
exports.GitController = GitController;
|
|
117
|
+
__decorate([
|
|
118
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.STATUS', summary: 'Get workspace git status' }),
|
|
119
|
+
(0, common_1.Get)('status'),
|
|
120
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
121
|
+
__metadata("design:type", Function),
|
|
122
|
+
__metadata("design:paramtypes", [Object]),
|
|
123
|
+
__metadata("design:returntype", Promise)
|
|
124
|
+
], GitController.prototype, "getStatus", null);
|
|
125
|
+
__decorate([
|
|
126
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.HISTORY', summary: 'Get commit history' }),
|
|
127
|
+
(0, swagger_1.ApiQuery)({ name: 'filepath', required: false }),
|
|
128
|
+
(0, swagger_1.ApiQuery)({ name: 'depth', required: false }),
|
|
129
|
+
(0, common_1.Get)('history'),
|
|
130
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
131
|
+
__param(1, (0, common_1.Query)('filepath')),
|
|
132
|
+
__param(2, (0, common_1.Query)('depth')),
|
|
133
|
+
__metadata("design:type", Function),
|
|
134
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
135
|
+
__metadata("design:returntype", Promise)
|
|
136
|
+
], GitController.prototype, "getHistory", null);
|
|
137
|
+
__decorate([
|
|
138
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.DIFF', summary: 'Get file diff (working tree or commit)' }),
|
|
139
|
+
(0, swagger_1.ApiQuery)({ name: 'commitSha', required: false }),
|
|
140
|
+
(0, swagger_1.ApiQuery)({ name: 'filepath', required: false }),
|
|
141
|
+
(0, common_1.Get)('diff'),
|
|
142
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
143
|
+
__param(1, (0, common_1.Query)('commitSha')),
|
|
144
|
+
__param(2, (0, common_1.Query)('filepath')),
|
|
145
|
+
__metadata("design:type", Function),
|
|
146
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
147
|
+
__metadata("design:returntype", Promise)
|
|
148
|
+
], GitController.prototype, "getDiff", null);
|
|
149
|
+
__decorate([
|
|
150
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.COMMIT', summary: 'Create a commit (save point)' }),
|
|
151
|
+
(0, common_1.Post)('commit'),
|
|
152
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
153
|
+
__param(1, (0, common_1.Body)()),
|
|
154
|
+
__metadata("design:type", Function),
|
|
155
|
+
__metadata("design:paramtypes", [Object, CommitDto]),
|
|
156
|
+
__metadata("design:returntype", Promise)
|
|
157
|
+
], GitController.prototype, "commit", null);
|
|
158
|
+
__decorate([
|
|
159
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.PUSH', summary: 'Push local commits to remote origin (http(s) only)' }),
|
|
160
|
+
(0, common_1.Post)('push'),
|
|
161
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
162
|
+
__param(1, (0, common_1.Body)()),
|
|
163
|
+
__metadata("design:type", Function),
|
|
164
|
+
__metadata("design:paramtypes", [Object, PushDto]),
|
|
165
|
+
__metadata("design:returntype", Promise)
|
|
166
|
+
], GitController.prototype, "push", null);
|
|
167
|
+
__decorate([
|
|
168
|
+
(0, swagger_1.ApiOperation)({ operationId: 'GIT.REVERT', summary: 'Revert changes with optional backup restore' }),
|
|
169
|
+
(0, common_1.Post)('revert'),
|
|
170
|
+
__param(0, (0, workspace_decorator_js_1.Workspace)()),
|
|
171
|
+
__param(1, (0, common_1.Body)()),
|
|
172
|
+
__metadata("design:type", Function),
|
|
173
|
+
__metadata("design:paramtypes", [Object, RevertDto]),
|
|
174
|
+
__metadata("design:returntype", Promise)
|
|
175
|
+
], GitController.prototype, "revert", null);
|
|
176
|
+
exports.GitController = GitController = __decorate([
|
|
177
|
+
(0, swagger_1.ApiTags)('git'),
|
|
178
|
+
(0, common_1.Controller)('ws/:slug/git'),
|
|
179
|
+
__metadata("design:paramtypes", [git_service_js_1.GitService])
|
|
180
|
+
], GitController);
|
|
@@ -9,12 +9,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.GitModule = void 0;
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
11
|
const git_service_js_1 = require("./git.service.js");
|
|
12
|
+
const git_controller_js_1 = require("./git.controller.js");
|
|
12
13
|
let GitModule = class GitModule {
|
|
13
14
|
};
|
|
14
15
|
exports.GitModule = GitModule;
|
|
15
16
|
exports.GitModule = GitModule = __decorate([
|
|
16
17
|
(0, common_1.Module)({
|
|
17
18
|
providers: [git_service_js_1.GitService],
|
|
19
|
+
controllers: [git_controller_js_1.GitController],
|
|
18
20
|
exports: [git_service_js_1.GitService],
|
|
19
21
|
})
|
|
20
22
|
], GitModule);
|
|
@@ -38,6 +38,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
return result;
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
41
44
|
var GitService_1;
|
|
42
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
46
|
exports.GitService = void 0;
|
|
@@ -45,6 +48,7 @@ const common_1 = require("@nestjs/common");
|
|
|
45
48
|
const path_1 = require("path");
|
|
46
49
|
const fs = __importStar(require("fs"));
|
|
47
50
|
const git = __importStar(require("isomorphic-git"));
|
|
51
|
+
const node_1 = __importDefault(require("isomorphic-git/http/node"));
|
|
48
52
|
const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
|
|
49
53
|
const AUTHOR = { name: 'CrewX', email: 'crewx@local' };
|
|
50
54
|
let GitService = GitService_1 = class GitService {
|
|
@@ -54,10 +58,183 @@ let GitService = GitService_1 = class GitService {
|
|
|
54
58
|
get dir() {
|
|
55
59
|
return (0, workspace_context_store_js_1.getWorkspacePath)();
|
|
56
60
|
}
|
|
61
|
+
/** Validate that filepath does not escape the workspace root. */
|
|
62
|
+
validateFilepath(filepath) {
|
|
63
|
+
const dir = this.dir;
|
|
64
|
+
const abs = (0, path_1.resolve)((0, path_1.join)(dir, filepath));
|
|
65
|
+
const normalizedDir = (0, path_1.normalize)(dir);
|
|
66
|
+
if (!abs.startsWith(normalizedDir + '/') && abs !== normalizedDir) {
|
|
67
|
+
throw new common_1.BadRequestException({ code: 'INVALID_PATH', message: 'Path traversal not allowed' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Check whether the workspace directory is a valid git repo. */
|
|
71
|
+
async isGitRepo() {
|
|
72
|
+
try {
|
|
73
|
+
await git.currentBranch({ fs, dir: this.dir });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Return git status: isGitRepo flag, current branch, and list of changed files.
|
|
82
|
+
* Maps statusMatrix rows to GitStatusFile entries (modified/added/deleted/untracked).
|
|
83
|
+
*/
|
|
84
|
+
async getStatus() {
|
|
85
|
+
if (!(await this.isGitRepo())) {
|
|
86
|
+
return { isGitRepo: false, branch: null, files: [], remoteBranch: null, remoteHeadSha: null, ahead: null, behind: null };
|
|
87
|
+
}
|
|
88
|
+
const branch = (await git.currentBranch({ fs, dir: this.dir })) ?? null;
|
|
89
|
+
// statusMatrix row: [filepath, HEAD, workdir, stage]
|
|
90
|
+
// HEAD=0 absent from index, 1=identical to index, 2=different from index
|
|
91
|
+
// workdir=0 absent, 1=same as index, 2=different from index
|
|
92
|
+
// stage=0 absent, 1=same as HEAD, 2=same as workdir, 3=different from both
|
|
93
|
+
const matrix = await git.statusMatrix({ fs, dir: this.dir, cache: this.cache });
|
|
94
|
+
const files = [];
|
|
95
|
+
for (const [filepath, head, workdir, stage] of matrix) {
|
|
96
|
+
// Unchanged: HEAD=1, workdir=1, stage=1
|
|
97
|
+
if (head === 1 && workdir === 1 && stage === 1)
|
|
98
|
+
continue;
|
|
99
|
+
// Clean unmodified (index matches HEAD, workdir matches index)
|
|
100
|
+
if (head === 1 && workdir === 1 && stage === 0)
|
|
101
|
+
continue;
|
|
102
|
+
const isStaged = stage === 2 || stage === 3;
|
|
103
|
+
let status;
|
|
104
|
+
if (head === 0 && workdir === 2 && stage === 0) {
|
|
105
|
+
// Untracked: new file in workdir, not staged
|
|
106
|
+
status = 'untracked';
|
|
107
|
+
}
|
|
108
|
+
else if (head === 0) {
|
|
109
|
+
// Added: new file (staged)
|
|
110
|
+
status = 'added';
|
|
111
|
+
}
|
|
112
|
+
else if (workdir === 0) {
|
|
113
|
+
// Deleted: in HEAD, absent in workdir
|
|
114
|
+
status = 'deleted';
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Modified
|
|
118
|
+
status = 'modified';
|
|
119
|
+
}
|
|
120
|
+
files.push({ filepath: filepath, status, staged: isStaged });
|
|
121
|
+
}
|
|
122
|
+
const remote = await this.resolveRemoteTracking(branch);
|
|
123
|
+
return { isGitRepo: true, branch, files, ...remote };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Derive remote-tracking info for the given branch from local refs only.
|
|
127
|
+
* Never performs a network fetch. Returns all-null when upstream is absent.
|
|
128
|
+
*/
|
|
129
|
+
async resolveRemoteTracking(branch) {
|
|
130
|
+
const nullResult = { remoteBranch: null, remoteHeadSha: null, ahead: null, behind: null };
|
|
131
|
+
if (!branch)
|
|
132
|
+
return nullResult;
|
|
133
|
+
try {
|
|
134
|
+
const remoteName = (await git.getConfig({ fs, dir: this.dir, path: `branch.${branch}.remote` }));
|
|
135
|
+
const mergePath = (await git.getConfig({ fs, dir: this.dir, path: `branch.${branch}.merge` }));
|
|
136
|
+
if (!remoteName || !mergePath)
|
|
137
|
+
return nullResult;
|
|
138
|
+
// mergePath is e.g. 'refs/heads/main' — strip prefix to get short name
|
|
139
|
+
const remoteBranchName = mergePath.replace(/^refs\/heads\//, '');
|
|
140
|
+
const remoteBranch = `${remoteName}/${remoteBranchName}`;
|
|
141
|
+
const remoteRef = `refs/remotes/${remoteName}/${remoteBranchName}`;
|
|
142
|
+
let remoteHeadSha = null;
|
|
143
|
+
try {
|
|
144
|
+
remoteHeadSha = await git.resolveRef({ fs, dir: this.dir, ref: remoteRef });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// remote-tracking ref not present locally (never fetched)
|
|
148
|
+
return { remoteBranch, remoteHeadSha: null, ahead: null, behind: null };
|
|
149
|
+
}
|
|
150
|
+
// Compute ahead/behind via log set-difference (merge-base approximation)
|
|
151
|
+
const [localLog, remoteLog] = await Promise.all([
|
|
152
|
+
git.log({ fs, dir: this.dir, ref: 'HEAD', cache: this.cache }),
|
|
153
|
+
git.log({ fs, dir: this.dir, ref: remoteHeadSha, cache: this.cache }),
|
|
154
|
+
]);
|
|
155
|
+
const remoteOids = new Set(remoteLog.map((c) => c.oid));
|
|
156
|
+
const localOids = new Set(localLog.map((c) => c.oid));
|
|
157
|
+
const ahead = localLog.filter((c) => !remoteOids.has(c.oid)).length;
|
|
158
|
+
const behind = remoteLog.filter((c) => !localOids.has(c.oid)).length;
|
|
159
|
+
return { remoteBranch, remoteHeadSha, ahead, behind };
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return nullResult;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Stage all current changes and create a backup commit before a revert.
|
|
167
|
+
* Returns the backup commit SHA. (Not a stash — uses a real commit for untracked safety.)
|
|
168
|
+
*/
|
|
169
|
+
async createBackupCommit(reason) {
|
|
170
|
+
const matrix = await git.statusMatrix({ fs, dir: this.dir, cache: this.cache });
|
|
171
|
+
for (const [filepath, head, workdir] of matrix) {
|
|
172
|
+
if (head === 1 && workdir === 1)
|
|
173
|
+
continue; // unchanged
|
|
174
|
+
const fp = filepath;
|
|
175
|
+
if (workdir === 0) {
|
|
176
|
+
await git.remove({ fs, dir: this.dir, filepath: fp });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
await git.add({ fs, dir: this.dir, filepath: fp });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const sha = await git.commit({
|
|
183
|
+
fs,
|
|
184
|
+
dir: this.dir,
|
|
185
|
+
message: `backup: before revert — ${reason}`,
|
|
186
|
+
author: AUTHOR,
|
|
187
|
+
cache: this.cache,
|
|
188
|
+
});
|
|
189
|
+
return sha;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Unified revert operation.
|
|
193
|
+
* - 'file': discard working tree changes for a single file (checkout from HEAD)
|
|
194
|
+
* - 'commit': restore working tree to the state at commitSha
|
|
195
|
+
* - 'restore-backup': restore working tree from a previous backup commit
|
|
196
|
+
*
|
|
197
|
+
* For 'file' and 'commit': creates a backup commit first and returns its SHA.
|
|
198
|
+
* For 'restore-backup': restores from backupSha and returns the same SHA.
|
|
199
|
+
*/
|
|
200
|
+
async revert(opts) {
|
|
201
|
+
if (opts.mode === 'restore-backup') {
|
|
202
|
+
if (!opts.backupSha) {
|
|
203
|
+
throw new common_1.BadRequestException({ code: 'MISSING_BACKUP_SHA' });
|
|
204
|
+
}
|
|
205
|
+
await git.checkout({ fs, dir: this.dir, ref: opts.backupSha, force: true, cache: this.cache });
|
|
206
|
+
return { backupSha: opts.backupSha };
|
|
207
|
+
}
|
|
208
|
+
const label = opts.mode === 'file' ? (opts.filepath ?? 'file') : (opts.commitSha ?? 'commit');
|
|
209
|
+
const backupSha = await this.createBackupCommit(label);
|
|
210
|
+
if (opts.mode === 'file') {
|
|
211
|
+
if (!opts.filepath) {
|
|
212
|
+
throw new common_1.BadRequestException({ code: 'MISSING_FILEPATH' });
|
|
213
|
+
}
|
|
214
|
+
this.validateFilepath(opts.filepath);
|
|
215
|
+
await git.checkout({
|
|
216
|
+
fs,
|
|
217
|
+
dir: this.dir,
|
|
218
|
+
ref: 'HEAD',
|
|
219
|
+
filepaths: [opts.filepath],
|
|
220
|
+
force: true,
|
|
221
|
+
cache: this.cache,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// mode === 'commit'
|
|
226
|
+
if (!opts.commitSha) {
|
|
227
|
+
throw new common_1.BadRequestException({ code: 'MISSING_COMMIT_SHA' });
|
|
228
|
+
}
|
|
229
|
+
await git.checkout({ fs, dir: this.dir, ref: opts.commitSha, force: true, cache: this.cache });
|
|
230
|
+
}
|
|
231
|
+
return { backupSha };
|
|
232
|
+
}
|
|
57
233
|
/**
|
|
58
234
|
* Stage a file and create a commit. Returns the commit SHA.
|
|
59
235
|
*/
|
|
60
236
|
async commitFile(filepath, message) {
|
|
237
|
+
this.validateFilepath(filepath);
|
|
61
238
|
await git.add({ fs, dir: this.dir, filepath, cache: this.cache });
|
|
62
239
|
const sha = await git.commit({
|
|
63
240
|
fs,
|
|
@@ -68,6 +245,39 @@ let GitService = GitService_1 = class GitService {
|
|
|
68
245
|
});
|
|
69
246
|
return sha;
|
|
70
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Stage given files (or all changes when filepaths is empty/undefined) and create a commit.
|
|
250
|
+
*/
|
|
251
|
+
async commitFiles(filepaths, message) {
|
|
252
|
+
if (filepaths && filepaths.length > 0) {
|
|
253
|
+
for (const fp of filepaths) {
|
|
254
|
+
this.validateFilepath(fp);
|
|
255
|
+
await git.add({ fs, dir: this.dir, filepath: fp, cache: this.cache });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const matrix = await git.statusMatrix({ fs, dir: this.dir, cache: this.cache });
|
|
260
|
+
for (const [filepath, head, workdir] of matrix) {
|
|
261
|
+
if (head === 1 && workdir === 1)
|
|
262
|
+
continue;
|
|
263
|
+
const fp = filepath;
|
|
264
|
+
if (workdir === 0) {
|
|
265
|
+
await git.remove({ fs, dir: this.dir, filepath: fp });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
await git.add({ fs, dir: this.dir, filepath: fp });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const sha = await git.commit({
|
|
273
|
+
fs,
|
|
274
|
+
dir: this.dir,
|
|
275
|
+
message,
|
|
276
|
+
author: AUTHOR,
|
|
277
|
+
cache: this.cache,
|
|
278
|
+
});
|
|
279
|
+
return sha;
|
|
280
|
+
}
|
|
71
281
|
/**
|
|
72
282
|
* Return commit history, optionally filtered by filepath.
|
|
73
283
|
*/
|
|
@@ -90,7 +300,7 @@ let GitService = GitService_1 = class GitService {
|
|
|
90
300
|
* Restore a file to its state at the given commit SHA.
|
|
91
301
|
*/
|
|
92
302
|
async rollbackFile(filepath, commitSha) {
|
|
93
|
-
|
|
303
|
+
this.validateFilepath(filepath);
|
|
94
304
|
const { blob } = await git.readBlob({
|
|
95
305
|
fs,
|
|
96
306
|
dir: this.dir,
|
|
@@ -100,14 +310,118 @@ let GitService = GitService_1 = class GitService {
|
|
|
100
310
|
});
|
|
101
311
|
const absPath = (0, path_1.join)(this.dir, filepath);
|
|
102
312
|
fs.writeFileSync(absPath, new Uint8Array(Buffer.from(blob)));
|
|
103
|
-
// Commit the rollback
|
|
104
313
|
await this.commitFile(filepath, `revert: restore ${filepath} to ${commitSha.slice(0, 7)}`);
|
|
105
314
|
}
|
|
106
315
|
/**
|
|
107
|
-
* Return diff entries
|
|
108
|
-
* If
|
|
316
|
+
* Return diff entries.
|
|
317
|
+
* - If commitSha is provided: compare that commit to its parent.
|
|
318
|
+
* - If commitSha is omitted: working tree diff (HEAD vs workdir).
|
|
319
|
+
* Optionally filtered by filepath.
|
|
109
320
|
*/
|
|
110
321
|
async getFileDiff(commitSha, filepath) {
|
|
322
|
+
if (!commitSha) {
|
|
323
|
+
return this.getWorkingTreeDiff(filepath);
|
|
324
|
+
}
|
|
325
|
+
return this.getCommitDiff(commitSha, filepath);
|
|
326
|
+
}
|
|
327
|
+
/** Diff HEAD against current working tree. */
|
|
328
|
+
async getWorkingTreeDiff(filepath) {
|
|
329
|
+
const results = [];
|
|
330
|
+
const matrix = await git.statusMatrix({ fs, dir: this.dir, cache: this.cache });
|
|
331
|
+
for (const [fp, head, workdir] of matrix) {
|
|
332
|
+
if (filepath && fp !== filepath)
|
|
333
|
+
continue;
|
|
334
|
+
if (head === 1 && workdir === 1)
|
|
335
|
+
continue; // unchanged
|
|
336
|
+
const absPath = (0, path_1.join)(this.dir, fp);
|
|
337
|
+
let type;
|
|
338
|
+
if (head === 0) {
|
|
339
|
+
type = 'add';
|
|
340
|
+
}
|
|
341
|
+
else if (workdir === 0) {
|
|
342
|
+
type = 'delete';
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
type = 'modify';
|
|
346
|
+
}
|
|
347
|
+
let beforeContent;
|
|
348
|
+
let afterContent;
|
|
349
|
+
if (type !== 'add') {
|
|
350
|
+
try {
|
|
351
|
+
const headOid = await git.resolveRef({ fs, dir: this.dir, ref: 'HEAD' });
|
|
352
|
+
const { blob } = await git.readBlob({ fs, dir: this.dir, oid: headOid, filepath: fp, cache: this.cache });
|
|
353
|
+
beforeContent = Buffer.from(blob).toString('utf-8');
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
this.logger.warn(`Failed to read HEAD blob for ${fp}: ${e}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (type !== 'delete') {
|
|
360
|
+
try {
|
|
361
|
+
afterContent = fs.readFileSync(absPath, 'utf-8');
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
this.logger.warn(`Failed to read workdir file ${fp}: ${e}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
results.push({ filepath: fp, type, before: beforeContent, after: afterContent });
|
|
368
|
+
}
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Push local commits to a remote http(s) origin using an env token.
|
|
373
|
+
* Pre-guards reject: non-http remote, missing remote, behind > 0, missing token.
|
|
374
|
+
* force push is never used.
|
|
375
|
+
*/
|
|
376
|
+
async push(opts = {}) {
|
|
377
|
+
const dir = this.dir;
|
|
378
|
+
if (!(await this.isGitRepo())) {
|
|
379
|
+
throw new common_1.BadRequestException({ code: 'NOT_GIT_REPO', message: 'Not a Git repository' });
|
|
380
|
+
}
|
|
381
|
+
const currentBranch = (await git.currentBranch({ fs, dir })) ?? null;
|
|
382
|
+
if (!currentBranch) {
|
|
383
|
+
throw new common_1.BadRequestException({ code: 'DETACHED_HEAD', message: 'No current branch (detached HEAD)' });
|
|
384
|
+
}
|
|
385
|
+
const remote = opts.remote ?? 'origin';
|
|
386
|
+
const branch = opts.branch ?? currentBranch;
|
|
387
|
+
const remoteUrl = (await git.getConfig({ fs, dir, path: `remote.${remote}.url` }));
|
|
388
|
+
if (!remoteUrl) {
|
|
389
|
+
throw new common_1.BadRequestException({ code: 'NO_REMOTE', message: 'No remote repository configured' });
|
|
390
|
+
}
|
|
391
|
+
if (!/^https?:\/\//i.test(remoteUrl)) {
|
|
392
|
+
throw new common_1.BadRequestException({ code: 'SSH_REMOTE', message: 'SSH remotes are not supported' });
|
|
393
|
+
}
|
|
394
|
+
const tracking = await this.resolveRemoteTracking(branch);
|
|
395
|
+
if (tracking.behind !== null && tracking.behind > 0) {
|
|
396
|
+
throw new common_1.BadRequestException({ code: 'BEHIND_REMOTE', message: 'Pull remote changes before pushing' });
|
|
397
|
+
}
|
|
398
|
+
// Read token without exposing it in any log or error path
|
|
399
|
+
const token = process.env['CREWX_GIT_TOKEN'] ?? process.env['GITHUB_TOKEN'];
|
|
400
|
+
if (!token) {
|
|
401
|
+
throw new common_1.BadRequestException({ code: 'NO_TOKEN', message: 'Git authentication token is required' });
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
await git.push({
|
|
405
|
+
fs,
|
|
406
|
+
http: node_1.default,
|
|
407
|
+
dir,
|
|
408
|
+
remote,
|
|
409
|
+
ref: branch,
|
|
410
|
+
force: false,
|
|
411
|
+
onAuth: () => ({ username: token }),
|
|
412
|
+
});
|
|
413
|
+
return { ok: true };
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
417
|
+
// Strip any embedded credentials from error messages before logging/throwing
|
|
418
|
+
const safe = raw.replace(/https?:\/\/[^@\s]*@/gi, 'https://***@');
|
|
419
|
+
this.logger.warn(`git push failed: ${safe}`);
|
|
420
|
+
throw new common_1.BadRequestException({ code: 'PUSH_FAILED', message: `Push failed: ${safe}` });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** Diff a specific commit against its parent. */
|
|
424
|
+
async getCommitDiff(commitSha, filepath) {
|
|
111
425
|
const commits = await git.log({
|
|
112
426
|
fs,
|
|
113
427
|
dir: this.dir,
|
|
@@ -130,25 +444,20 @@ let GitService = GitService_1 = class GitService {
|
|
|
130
444
|
dir: this.dir,
|
|
131
445
|
trees: walkerTrees,
|
|
132
446
|
map: async (path, entries) => {
|
|
133
|
-
if (filepath && path !== filepath)
|
|
447
|
+
if (filepath && path !== '.' && path !== filepath && !filepath.startsWith(`${path}/`)) {
|
|
134
448
|
return null;
|
|
135
|
-
|
|
449
|
+
}
|
|
136
450
|
if (!parentTree) {
|
|
137
451
|
const [current] = entries;
|
|
138
452
|
if (!current)
|
|
139
453
|
return null;
|
|
140
454
|
const entryType = await current.type();
|
|
141
455
|
if (entryType === 'tree')
|
|
142
|
-
return
|
|
456
|
+
return undefined;
|
|
143
457
|
const oid = await current.oid();
|
|
144
458
|
let afterContent;
|
|
145
459
|
try {
|
|
146
|
-
const { blob } = await git.readBlob({
|
|
147
|
-
fs,
|
|
148
|
-
dir: this.dir,
|
|
149
|
-
oid,
|
|
150
|
-
cache: this.cache,
|
|
151
|
-
});
|
|
460
|
+
const { blob } = await git.readBlob({ fs, dir: this.dir, oid, cache: this.cache });
|
|
152
461
|
afterContent = Buffer.from(blob).toString('utf-8');
|
|
153
462
|
}
|
|
154
463
|
catch (e) {
|
|
@@ -159,13 +468,12 @@ let GitService = GitService_1 = class GitService {
|
|
|
159
468
|
const [before, after] = entries;
|
|
160
469
|
const beforeType = await before?.type();
|
|
161
470
|
const afterType = await after?.type();
|
|
162
|
-
// Skip directories
|
|
163
471
|
if (beforeType === 'tree' || afterType === 'tree')
|
|
164
|
-
return
|
|
472
|
+
return undefined;
|
|
165
473
|
const beforeOid = await before?.oid();
|
|
166
474
|
const afterOid = await after?.oid();
|
|
167
475
|
if (beforeOid === afterOid)
|
|
168
|
-
return null;
|
|
476
|
+
return null;
|
|
169
477
|
let type;
|
|
170
478
|
if (!before || beforeType === undefined) {
|
|
171
479
|
type = 'add';
|
|
@@ -180,33 +488,18 @@ let GitService = GitService_1 = class GitService {
|
|
|
180
488
|
let afterContent;
|
|
181
489
|
try {
|
|
182
490
|
if (type !== 'add' && beforeOid) {
|
|
183
|
-
const { blob } = await git.readBlob({
|
|
184
|
-
fs,
|
|
185
|
-
dir: this.dir,
|
|
186
|
-
oid: beforeOid,
|
|
187
|
-
cache: this.cache,
|
|
188
|
-
});
|
|
491
|
+
const { blob } = await git.readBlob({ fs, dir: this.dir, oid: beforeOid, cache: this.cache });
|
|
189
492
|
beforeContent = Buffer.from(blob).toString('utf-8');
|
|
190
493
|
}
|
|
191
494
|
if (type !== 'delete' && afterOid) {
|
|
192
|
-
const { blob } = await git.readBlob({
|
|
193
|
-
fs,
|
|
194
|
-
dir: this.dir,
|
|
195
|
-
oid: afterOid,
|
|
196
|
-
cache: this.cache,
|
|
197
|
-
});
|
|
495
|
+
const { blob } = await git.readBlob({ fs, dir: this.dir, oid: afterOid, cache: this.cache });
|
|
198
496
|
afterContent = Buffer.from(blob).toString('utf-8');
|
|
199
497
|
}
|
|
200
498
|
}
|
|
201
499
|
catch (e) {
|
|
202
500
|
this.logger.warn(`Failed to read blob for ${path}: ${e}`);
|
|
203
501
|
}
|
|
204
|
-
return {
|
|
205
|
-
filepath: path,
|
|
206
|
-
type,
|
|
207
|
-
before: beforeContent,
|
|
208
|
-
after: afterContent,
|
|
209
|
-
};
|
|
502
|
+
return { filepath: path, type, before: beforeContent, after: afterContent };
|
|
210
503
|
},
|
|
211
504
|
});
|
|
212
505
|
for (const entry of changes) {
|