@gowelle/stint-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/api-K3EUONWR.js +6 -0
- package/dist/chunk-5DWSNHS6.js +448 -0
- package/dist/chunk-PPODHVVP.js +408 -0
- package/dist/daemon/runner.js +484 -0
- package/dist/index.js +854 -0
- package/package.json +63 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import {
|
|
2
|
+
apiService,
|
|
3
|
+
config,
|
|
4
|
+
logger
|
|
5
|
+
} from "./chunk-5DWSNHS6.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/process.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
var PID_FILE = path.join(os.homedir(), ".config", "stint", "daemon.pid");
|
|
13
|
+
function isProcessRunning(pid) {
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function killProcess(pid, signal = "SIGTERM") {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, signal);
|
|
24
|
+
logger.info("process", `Sent ${signal} to process ${pid}`);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error("process", `Failed to kill process ${pid}`, error);
|
|
27
|
+
throw new Error(`Failed to kill process ${pid}: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function spawnDetached(command, args, options = {}) {
|
|
31
|
+
const logDir = path.join(os.homedir(), ".config", "stint", "logs");
|
|
32
|
+
if (!fs.existsSync(logDir)) {
|
|
33
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
const stdoutPath = options.stdout || path.join(logDir, "daemon.log");
|
|
36
|
+
const stderrPath = options.stderr || path.join(logDir, "daemon-error.log");
|
|
37
|
+
const out = fs.openSync(stdoutPath, "a");
|
|
38
|
+
const err = fs.openSync(stderrPath, "a");
|
|
39
|
+
const child = spawn(command, args, {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: ["ignore", out, err],
|
|
42
|
+
cwd: options.cwd || process.cwd(),
|
|
43
|
+
env: options.env || process.env
|
|
44
|
+
});
|
|
45
|
+
child.unref();
|
|
46
|
+
logger.info("process", `Spawned detached process ${child.pid}`);
|
|
47
|
+
return child.pid;
|
|
48
|
+
}
|
|
49
|
+
function writePidFile(pid) {
|
|
50
|
+
const pidDir = path.dirname(PID_FILE);
|
|
51
|
+
if (!fs.existsSync(pidDir)) {
|
|
52
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 384 });
|
|
55
|
+
logger.info("process", `Wrote PID ${pid} to ${PID_FILE}`);
|
|
56
|
+
}
|
|
57
|
+
function readPidFile() {
|
|
58
|
+
try {
|
|
59
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const pidStr = fs.readFileSync(PID_FILE, "utf8").trim();
|
|
63
|
+
const pid = parseInt(pidStr, 10);
|
|
64
|
+
if (isNaN(pid)) {
|
|
65
|
+
logger.warn("process", `Invalid PID in file: ${pidStr}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return pid;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error("process", "Failed to read PID file", error);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function removePidFile() {
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(PID_FILE)) {
|
|
77
|
+
fs.unlinkSync(PID_FILE);
|
|
78
|
+
logger.info("process", "Removed PID file");
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error("process", "Failed to remove PID file", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validatePidFile() {
|
|
85
|
+
const pid = readPidFile();
|
|
86
|
+
if (pid === null) {
|
|
87
|
+
return { valid: false, pid: null };
|
|
88
|
+
}
|
|
89
|
+
const running = isProcessRunning(pid);
|
|
90
|
+
if (!running) {
|
|
91
|
+
logger.warn("process", `PID file exists but process ${pid} is not running`);
|
|
92
|
+
removePidFile();
|
|
93
|
+
return { valid: false, pid: null };
|
|
94
|
+
}
|
|
95
|
+
return { valid: true, pid };
|
|
96
|
+
}
|
|
97
|
+
function getPidFilePath() {
|
|
98
|
+
return PID_FILE;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/services/git.ts
|
|
102
|
+
import simpleGit from "simple-git";
|
|
103
|
+
var GitServiceImpl = class {
|
|
104
|
+
getGit(path3) {
|
|
105
|
+
return simpleGit(path3);
|
|
106
|
+
}
|
|
107
|
+
async isRepo(path3) {
|
|
108
|
+
try {
|
|
109
|
+
const git = this.getGit(path3);
|
|
110
|
+
return await git.checkIsRepo();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logger.error("git", `Failed to check if ${path3} is a repo`, error);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async getRepoInfo(path3) {
|
|
117
|
+
try {
|
|
118
|
+
const git = this.getGit(path3);
|
|
119
|
+
const branchSummary = await git.branch();
|
|
120
|
+
const currentBranch = branchSummary.current;
|
|
121
|
+
const branches = branchSummary.all;
|
|
122
|
+
const remotes = await git.getRemotes(true);
|
|
123
|
+
const remoteUrl = remotes.length > 0 ? remotes[0].refs.fetch : null;
|
|
124
|
+
const status = await this.getStatus(path3);
|
|
125
|
+
const log = await git.log({ maxCount: 1 });
|
|
126
|
+
const lastCommit = log.latest;
|
|
127
|
+
if (!lastCommit) {
|
|
128
|
+
throw new Error("No commits found in repository");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
currentBranch,
|
|
132
|
+
branches,
|
|
133
|
+
remoteUrl,
|
|
134
|
+
status,
|
|
135
|
+
lastCommitSha: lastCommit.hash,
|
|
136
|
+
lastCommitMessage: lastCommit.message,
|
|
137
|
+
lastCommitDate: lastCommit.date
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error("git", `Failed to get repo info for ${path3}`, error);
|
|
141
|
+
throw new Error(`Failed to get repository information: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async stageAll(path3) {
|
|
145
|
+
try {
|
|
146
|
+
const git = this.getGit(path3);
|
|
147
|
+
await git.add(".");
|
|
148
|
+
logger.info("git", `Staged all changes in ${path3}`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error("git", `Failed to stage all in ${path3}`, error);
|
|
151
|
+
throw new Error(`Failed to stage changes: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async stageFiles(path3, files) {
|
|
155
|
+
try {
|
|
156
|
+
const git = this.getGit(path3);
|
|
157
|
+
await git.add(files);
|
|
158
|
+
logger.info("git", `Staged ${files.length} files in ${path3}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error("git", `Failed to stage files in ${path3}`, error);
|
|
161
|
+
throw new Error(`Failed to stage files: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async commit(path3, message) {
|
|
165
|
+
try {
|
|
166
|
+
const git = this.getGit(path3);
|
|
167
|
+
const result = await git.commit(message);
|
|
168
|
+
const sha = result.commit;
|
|
169
|
+
logger.success("git", `Created commit ${sha} in ${path3}`);
|
|
170
|
+
return sha;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error("git", `Failed to commit in ${path3}`, error);
|
|
173
|
+
throw new Error(`Failed to create commit: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async getCurrentBranch(path3) {
|
|
177
|
+
try {
|
|
178
|
+
const git = this.getGit(path3);
|
|
179
|
+
const branchSummary = await git.branch();
|
|
180
|
+
return branchSummary.current;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error("git", `Failed to get current branch in ${path3}`, error);
|
|
183
|
+
throw new Error(`Failed to get current branch: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async getBranches(path3) {
|
|
187
|
+
try {
|
|
188
|
+
const git = this.getGit(path3);
|
|
189
|
+
const branchSummary = await git.branch();
|
|
190
|
+
return branchSummary.all.map((name) => ({
|
|
191
|
+
name,
|
|
192
|
+
current: name === branchSummary.current,
|
|
193
|
+
commit: branchSummary.branches[name]?.commit || ""
|
|
194
|
+
}));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error("git", `Failed to get branches in ${path3}`, error);
|
|
197
|
+
throw new Error(`Failed to get branches: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async getStatus(path3) {
|
|
201
|
+
try {
|
|
202
|
+
const git = this.getGit(path3);
|
|
203
|
+
const status = await git.status();
|
|
204
|
+
return {
|
|
205
|
+
staged: status.staged,
|
|
206
|
+
unstaged: status.modified.concat(status.deleted),
|
|
207
|
+
untracked: status.not_added,
|
|
208
|
+
ahead: status.ahead,
|
|
209
|
+
behind: status.behind
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.error("git", `Failed to get status in ${path3}`, error);
|
|
213
|
+
throw new Error(`Failed to get git status: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var gitService = new GitServiceImpl();
|
|
218
|
+
|
|
219
|
+
// src/services/project.ts
|
|
220
|
+
import path2 from "path";
|
|
221
|
+
var ProjectServiceImpl = class {
|
|
222
|
+
async linkProject(projectPath, projectId) {
|
|
223
|
+
try {
|
|
224
|
+
const absolutePath = path2.resolve(projectPath);
|
|
225
|
+
const isRepo = await gitService.isRepo(absolutePath);
|
|
226
|
+
if (!isRepo) {
|
|
227
|
+
throw new Error(`${absolutePath} is not a git repository`);
|
|
228
|
+
}
|
|
229
|
+
const linkedProject = {
|
|
230
|
+
projectId,
|
|
231
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
232
|
+
};
|
|
233
|
+
config.setProject(absolutePath, linkedProject);
|
|
234
|
+
logger.success("project", `Linked ${absolutePath} to project ${projectId}`);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.error("project", "Failed to link project", error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async unlinkProject(projectPath) {
|
|
241
|
+
try {
|
|
242
|
+
const absolutePath = path2.resolve(projectPath);
|
|
243
|
+
const linkedProject = this.getLinkedProject(absolutePath);
|
|
244
|
+
if (!linkedProject) {
|
|
245
|
+
throw new Error(`${absolutePath} is not linked to any project`);
|
|
246
|
+
}
|
|
247
|
+
config.removeProject(absolutePath);
|
|
248
|
+
logger.success("project", `Unlinked ${absolutePath}`);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error("project", "Failed to unlink project", error);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
getLinkedProject(projectPath) {
|
|
255
|
+
const absolutePath = path2.resolve(projectPath);
|
|
256
|
+
return config.getProject(absolutePath) || null;
|
|
257
|
+
}
|
|
258
|
+
getAllLinkedProjects() {
|
|
259
|
+
return config.getProjects();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get local path for a project ID
|
|
263
|
+
*/
|
|
264
|
+
getProjectPath(projectId) {
|
|
265
|
+
const allProjects = this.getAllLinkedProjects();
|
|
266
|
+
for (const [path3, linkedProject] of Object.entries(allProjects)) {
|
|
267
|
+
if (linkedProject.projectId === projectId) {
|
|
268
|
+
return path3;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var projectService = new ProjectServiceImpl();
|
|
275
|
+
|
|
276
|
+
// src/daemon/queue.ts
|
|
277
|
+
var CommitQueueProcessor = class {
|
|
278
|
+
queue = [];
|
|
279
|
+
isProcessing = false;
|
|
280
|
+
/**
|
|
281
|
+
* Add commit to processing queue
|
|
282
|
+
*/
|
|
283
|
+
addToQueue(commit, project) {
|
|
284
|
+
this.queue.push({ commit, project });
|
|
285
|
+
logger.info("queue", `Added commit ${commit.id} to queue (position: ${this.queue.length})`);
|
|
286
|
+
if (!this.isProcessing) {
|
|
287
|
+
this.processQueue();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Process commits sequentially
|
|
292
|
+
*/
|
|
293
|
+
async processQueue() {
|
|
294
|
+
if (this.isProcessing) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.isProcessing = true;
|
|
298
|
+
while (this.queue.length > 0) {
|
|
299
|
+
const item = this.queue.shift();
|
|
300
|
+
if (!item) break;
|
|
301
|
+
try {
|
|
302
|
+
await this.executeCommit(item.commit, item.project);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
logger.error("queue", `Failed to execute commit ${item.commit.id}`, error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.isProcessing = false;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Execute a single commit
|
|
311
|
+
*/
|
|
312
|
+
async executeCommit(commit, project) {
|
|
313
|
+
logger.info("queue", `Processing commit: ${commit.id} - ${commit.message}`);
|
|
314
|
+
try {
|
|
315
|
+
const projectPath = this.findProjectPath(project.id);
|
|
316
|
+
if (!projectPath) {
|
|
317
|
+
throw new Error(`Project ${project.id} is not linked to any local directory`);
|
|
318
|
+
}
|
|
319
|
+
logger.info("queue", `Executing in directory: ${projectPath}`);
|
|
320
|
+
const isRepo = await gitService.isRepo(projectPath);
|
|
321
|
+
if (!isRepo) {
|
|
322
|
+
throw new Error(`Directory ${projectPath} is not a git repository`);
|
|
323
|
+
}
|
|
324
|
+
const status = await gitService.getStatus(projectPath);
|
|
325
|
+
const hasChanges = status.staged.length > 0 || status.unstaged.length > 0 || status.untracked.length > 0;
|
|
326
|
+
if (hasChanges) {
|
|
327
|
+
throw new Error("Repository has uncommitted changes. Please commit or stash them first.");
|
|
328
|
+
}
|
|
329
|
+
if (commit.files && commit.files.length > 0) {
|
|
330
|
+
logger.info("queue", `Staging specific files: ${commit.files.join(", ")}`);
|
|
331
|
+
await gitService.stageFiles(projectPath, commit.files);
|
|
332
|
+
} else {
|
|
333
|
+
logger.info("queue", "Staging all changes");
|
|
334
|
+
await gitService.stageAll(projectPath);
|
|
335
|
+
}
|
|
336
|
+
logger.info("queue", `Creating commit with message: "${commit.message}"`);
|
|
337
|
+
const sha = await gitService.commit(projectPath, commit.message);
|
|
338
|
+
logger.success("queue", `Commit created successfully: ${sha}`);
|
|
339
|
+
await this.reportSuccess(commit.id, sha);
|
|
340
|
+
return sha;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const errorMessage = error.message;
|
|
343
|
+
logger.error("queue", `Commit execution failed: ${errorMessage}`);
|
|
344
|
+
await this.reportFailure(commit.id, errorMessage);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Report successful execution to API
|
|
350
|
+
*/
|
|
351
|
+
async reportSuccess(commitId, sha) {
|
|
352
|
+
try {
|
|
353
|
+
await apiService.markCommitExecuted(commitId, sha);
|
|
354
|
+
logger.success("queue", `Reported commit execution to API: ${commitId} -> ${sha}`);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.error("queue", "Failed to report commit success to API", error);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Report failed execution to API
|
|
361
|
+
*/
|
|
362
|
+
async reportFailure(commitId, error) {
|
|
363
|
+
try {
|
|
364
|
+
await apiService.markCommitFailed(commitId, error);
|
|
365
|
+
logger.info("queue", `Reported commit failure to API: ${commitId}`);
|
|
366
|
+
} catch (apiError) {
|
|
367
|
+
logger.error("queue", "Failed to report commit failure to API", apiError);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Find local path for a project ID
|
|
372
|
+
*/
|
|
373
|
+
findProjectPath(projectId) {
|
|
374
|
+
const allProjects = projectService.getAllLinkedProjects();
|
|
375
|
+
for (const [path3, linkedProject] of Object.entries(allProjects)) {
|
|
376
|
+
if (linkedProject.projectId === projectId) {
|
|
377
|
+
return path3;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if queue is currently processing
|
|
384
|
+
*/
|
|
385
|
+
isCurrentlyProcessing() {
|
|
386
|
+
return this.isProcessing;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get queue length
|
|
390
|
+
*/
|
|
391
|
+
getQueueLength() {
|
|
392
|
+
return this.queue.length;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var commitQueue = new CommitQueueProcessor();
|
|
396
|
+
|
|
397
|
+
export {
|
|
398
|
+
gitService,
|
|
399
|
+
projectService,
|
|
400
|
+
isProcessRunning,
|
|
401
|
+
killProcess,
|
|
402
|
+
spawnDetached,
|
|
403
|
+
writePidFile,
|
|
404
|
+
removePidFile,
|
|
405
|
+
validatePidFile,
|
|
406
|
+
getPidFilePath,
|
|
407
|
+
commitQueue
|
|
408
|
+
};
|