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.
Files changed (82) hide show
  1. package/dist/assets/{MarketPage-BpajB_Ex.js → MarketPage-Cl9KvLAo.js} +1 -1
  2. package/dist/assets/PromptTab-gj_BvClc.js +27 -0
  3. package/dist/assets/{arc-B0fFLEOc.js → arc-DDzVSXZh.js} +1 -1
  4. package/dist/assets/{architectureDiagram-UYN6MBPD-CrC7MKTM.js → architectureDiagram-UYN6MBPD-p8OOXe5w.js} +1 -1
  5. package/dist/assets/{blockDiagram-ZHA2E4KO-DJsG-9x2.js → blockDiagram-ZHA2E4KO-BTlfXGnf.js} +7 -7
  6. package/dist/assets/{c4Diagram-6F5ED5ID-DlkyjvkS.js → c4Diagram-6F5ED5ID-CKbrDPWE.js} +1 -1
  7. package/dist/assets/channel-Hzz-cXd0.js +1 -0
  8. package/dist/assets/{chunk-5HRBRIJM-B_Csqjvw.js → chunk-5HRBRIJM-otR_A_1G.js} +1 -1
  9. package/dist/assets/{chunk-7U56Z5CX-BlNdzxbh.js → chunk-7U56Z5CX-BliJIH4-.js} +1 -1
  10. package/dist/assets/{chunk-ASOPGD6M-BiFWfa3w.js → chunk-ASOPGD6M-CmzypD4Z.js} +1 -1
  11. package/dist/assets/{chunk-KFBOBJHC-ocugjY-9.js → chunk-KFBOBJHC-BW4u3lPL.js} +1 -1
  12. package/dist/assets/{chunk-T2TOU4HS-kUreJ-bv.js → chunk-T2TOU4HS-BAp6Xscz.js} +1 -1
  13. package/dist/assets/{chunk-TMUBEWPD-Du19Q8X-.js → chunk-TMUBEWPD-Bv_RKIlz.js} +1 -1
  14. package/dist/assets/classDiagram-LNE6IOMH-CXR-DJKP.js +1 -0
  15. package/dist/assets/classDiagram-v2-MQ7JQ4JX-CXR-DJKP.js +1 -0
  16. package/dist/assets/{dagre-4EVJKHTY-8N5qxubq.js → dagre-4EVJKHTY-sYyP6lkA.js} +1 -1
  17. package/dist/assets/{diagram-QW4FP2JN-CjLH9uYk.js → diagram-QW4FP2JN-XWldv7Gz.js} +1 -1
  18. package/dist/assets/{erDiagram-6RL3IURR-CXgt8NtN.js → erDiagram-6RL3IURR-Cd8OVAQ0.js} +1 -1
  19. package/dist/assets/{flowDiagram-7ASYPVHJ-CdFlmdmT.js → flowDiagram-7ASYPVHJ-Cc3onSB6.js} +1 -1
  20. package/dist/assets/{ganttDiagram-NTVNEXSI-DDjK4yxy.js → ganttDiagram-NTVNEXSI-Q6HxMUkA.js} +1 -1
  21. package/dist/assets/{gitGraph-YCYPL57B-Bd9jYZ-C.js → gitGraph-YCYPL57B-BUh0zUco.js} +1 -1
  22. package/dist/assets/{gitGraphDiagram-NRZ2UAAF-pmwetPkN.js → gitGraphDiagram-NRZ2UAAF-CvPqw-ya.js} +1 -1
  23. package/dist/assets/{infoDiagram-A4XQUW5V-BoDEaOyd.js → infoDiagram-A4XQUW5V-CbKGdGqb.js} +1 -1
  24. package/dist/assets/{journeyDiagram-G5WM74LC-D1LRuhB3.js → journeyDiagram-G5WM74LC-BXyAvuxS.js} +1 -1
  25. package/dist/assets/{kanban-definition-QRCXZQQD-B66SGjA2.js → kanban-definition-QRCXZQQD-CxBFX9if.js} +1 -1
  26. package/dist/assets/{linear-VMCGabuP.js → linear-Bu0P9vdO.js} +1 -1
  27. package/dist/assets/main-AYmbxFu4.css +10 -0
  28. package/dist/assets/main-CcrxU2Tf.js +1454 -0
  29. package/dist/assets/{mindmap-definition-GWI6TPTV--NljwHD9.js → mindmap-definition-GWI6TPTV-BbXI83yG.js} +1 -1
  30. package/dist/assets/{pieDiagram-YF2LJOPJ-CP4IPp3Q.js → pieDiagram-YF2LJOPJ-DuXspLPW.js} +1 -1
  31. package/dist/assets/{quadrantDiagram-OS5C2QUG-baFc-DtQ.js → quadrantDiagram-OS5C2QUG-DnngSA8p.js} +1 -1
  32. package/dist/assets/{requirementDiagram-MIRIMTAZ-BTWwNcnt.js → requirementDiagram-MIRIMTAZ-ChJE8XYc.js} +1 -1
  33. package/dist/assets/{sankeyDiagram-Y46BX6SQ-CnrACC1i.js → sankeyDiagram-Y46BX6SQ-BlkQKMk0.js} +1 -1
  34. package/dist/assets/{sequenceDiagram-G6AWOVSC-Ct5WkHJl.js → sequenceDiagram-G6AWOVSC-BvYCRnWL.js} +1 -1
  35. package/dist/assets/{stateDiagram-MAYHULR4-DeZHZmIy.js → stateDiagram-MAYHULR4-Cb6BiEq_.js} +1 -1
  36. package/dist/assets/stateDiagram-v2-4JROLMXI-31Hvt1M0.js +1 -0
  37. package/dist/assets/{timeline-definition-U7ZMHBDA-BZfUE50X.js → timeline-definition-U7ZMHBDA-w1-dGiEP.js} +1 -1
  38. package/dist/assets/{xychartDiagram-6QU3TZC5-DV6BFyHM.js → xychartDiagram-6QU3TZC5-B85iIKzT.js} +1 -1
  39. package/dist/index.html +2 -2
  40. package/dist-server/__stubs__/ccusage-codex.js +8 -0
  41. package/dist-server/app.module.js +2 -0
  42. package/dist-server/common/limits/defaults.js +3 -3
  43. package/dist-server/domain/agent/agent.service.js +11 -0
  44. package/dist-server/domain/cli/cli.service.js +3 -3
  45. package/dist-server/domain/cron/cron.service.js +5 -1
  46. package/dist-server/domain/doc/doc.controller.js +22 -0
  47. package/dist-server/domain/doc/doc.service.js +42 -10
  48. package/dist-server/domain/git/git.controller.js +180 -0
  49. package/dist-server/domain/git/git.module.js +2 -0
  50. package/dist-server/domain/git/git.service.js +327 -34
  51. package/dist-server/domain/message/message.service.js +18 -8
  52. package/dist-server/domain/onboarding/onboarding.service.js +1 -1
  53. package/dist-server/domain/onboarding/presets/custom.js +2 -2
  54. package/dist-server/domain/onboarding/presets/dev-team.js +7 -7
  55. package/dist-server/domain/onboarding/presets/marketing-team.js +5 -5
  56. package/dist-server/domain/onboarding/presets/planning-team.js +5 -5
  57. package/dist-server/domain/onboarding/presets/solo.js +3 -3
  58. package/dist-server/domain/settings/settings.module.js +2 -0
  59. package/dist-server/domain/settings/settings.service.js +75 -8
  60. package/dist-server/domain/task/task.service.js +7 -7
  61. package/dist-server/domain/thread/thread.service.js +2 -1
  62. package/dist-server/domain/usage/usage.controller.js +30 -0
  63. package/dist-server/domain/usage/usage.service.js +212 -0
  64. package/dist-server/domain/wi/wi.controller.js +43 -0
  65. package/dist-server/domain/wi/wi.module.js +22 -0
  66. package/dist-server/domain/wi/wi.service.js +110 -0
  67. package/dist-server/domain/wi/wi.types.js +2 -0
  68. package/dist-server/domain/workspace/workspace.service.js +4 -1
  69. package/dist-server/modules/crewx-pool.service.js +194 -53
  70. package/dist-server/repository/task.repository.js +5 -1
  71. package/package.json +16 -14
  72. package/packages/cli/dist/commands/agent.js +1 -1
  73. package/packages/cli/dist/commands/init.d.ts +2 -0
  74. package/packages/cli/dist/commands/init.js +198 -6
  75. package/packages/cli/package.json +3 -2
  76. package/dist/assets/PromptTab-HhD4MQcW.js +0 -37
  77. package/dist/assets/channel-ClmdPiZd.js +0 -1
  78. package/dist/assets/classDiagram-LNE6IOMH-Q0hRWLPF.js +0 -1
  79. package/dist/assets/classDiagram-v2-MQ7JQ4JX-Q0hRWLPF.js +0 -1
  80. package/dist/assets/main-AqROOc4i.css +0 -10
  81. package/dist/assets/main-DWCTap27.js +0 -1289
  82. 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
- // Read file blob at that commit
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 for the given commit compared to its parent.
108
- * If filepath is provided, only include that file.
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
- // When no parent: single walker entry means all files are "add"
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 null;
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 null;
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; // No change
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) {