@gowelle/stint-agent 1.0.2 → 1.0.4
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/README.md
CHANGED
|
@@ -34,7 +34,7 @@ stint login
|
|
|
34
34
|
# Check your authentication status
|
|
35
35
|
stint whoami
|
|
36
36
|
|
|
37
|
-
# Link a project
|
|
37
|
+
# Link a project (or create a new one)
|
|
38
38
|
cd /path/to/your/project
|
|
39
39
|
stint link
|
|
40
40
|
|
|
@@ -47,6 +47,13 @@ stint daemon status
|
|
|
47
47
|
|
|
48
48
|
## Commands
|
|
49
49
|
|
|
50
|
+
### General
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `stint --version`, `stint -V` | Show current agent version |
|
|
55
|
+
| `stint --help`, `stint -h` | Show help information |
|
|
56
|
+
|
|
50
57
|
### Authentication
|
|
51
58
|
|
|
52
59
|
| Command | Description |
|
|
@@ -55,11 +62,13 @@ stint daemon status
|
|
|
55
62
|
| `stint logout` | Remove stored credentials |
|
|
56
63
|
| `stint whoami` | Show current user and machine information |
|
|
57
64
|
|
|
58
|
-
### Daemon
|
|
65
|
+
### Daemon Lifecycle
|
|
59
66
|
|
|
60
67
|
| Command | Description |
|
|
61
68
|
|---------|-------------|
|
|
62
|
-
| `stint
|
|
69
|
+
| `stint install` | Register daemon to run on system startup (Login required) |
|
|
70
|
+
| `stint uninstall` | Remove daemon from system startup |
|
|
71
|
+
| `stint daemon start` | Start background daemon manually |
|
|
63
72
|
| `stint daemon stop` | Stop daemon gracefully |
|
|
64
73
|
| `stint daemon status` | Check if daemon is running |
|
|
65
74
|
| `stint daemon logs [--lines N]` | View daemon logs (default: 50 lines) |
|
|
@@ -69,7 +78,7 @@ stint daemon status
|
|
|
69
78
|
|
|
70
79
|
| Command | Description |
|
|
71
80
|
|---------|-------------|
|
|
72
|
-
| `stint link` | Link current directory to a Stint project |
|
|
81
|
+
| `stint link` | Link current directory to a Stint project (or create a new one) |
|
|
73
82
|
| `stint unlink [--force]` | Remove project link |
|
|
74
83
|
| `stint status` | Show project, git, auth, and daemon status |
|
|
75
84
|
| `stint sync` | Manually sync repository information to server |
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
apiService,
|
|
3
3
|
config,
|
|
4
4
|
logger
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XZCWE3IA.js";
|
|
6
6
|
|
|
7
7
|
// src/utils/process.ts
|
|
8
8
|
import fs from "fs";
|
|
@@ -121,6 +121,20 @@ var GitServiceImpl = class {
|
|
|
121
121
|
const branches = branchSummary.all;
|
|
122
122
|
const remotes = await git.getRemotes(true);
|
|
123
123
|
const remoteUrl = remotes.length > 0 ? remotes[0].refs.fetch : null;
|
|
124
|
+
let defaultBranch = currentBranch;
|
|
125
|
+
try {
|
|
126
|
+
const result = await git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
127
|
+
const match = result.trim().match(/refs\/remotes\/origin\/(.+)/);
|
|
128
|
+
if (match) {
|
|
129
|
+
defaultBranch = match[1];
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
if (branches.includes("main")) {
|
|
133
|
+
defaultBranch = "main";
|
|
134
|
+
} else if (branches.includes("master")) {
|
|
135
|
+
defaultBranch = "master";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
124
138
|
const status = await this.getStatus(path3);
|
|
125
139
|
const log = await git.log({ maxCount: 1 });
|
|
126
140
|
const lastCommit = log.latest;
|
|
@@ -128,7 +142,9 @@ var GitServiceImpl = class {
|
|
|
128
142
|
throw new Error("No commits found in repository");
|
|
129
143
|
}
|
|
130
144
|
return {
|
|
145
|
+
repoPath: path3,
|
|
131
146
|
currentBranch,
|
|
147
|
+
defaultBranch,
|
|
132
148
|
branches,
|
|
133
149
|
remoteUrl,
|
|
134
150
|
status,
|
|
@@ -254,7 +254,7 @@ var AuthServiceImpl = class {
|
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
256
|
try {
|
|
257
|
-
const { apiService: apiService2 } = await import("./api-
|
|
257
|
+
const { apiService: apiService2 } = await import("./api-HRVUUV3V.js");
|
|
258
258
|
const user = await apiService2.getCurrentUser();
|
|
259
259
|
logger.info("auth", `Token validated for user: ${user.email}`);
|
|
260
260
|
return user;
|
|
@@ -274,7 +274,7 @@ var AuthServiceImpl = class {
|
|
|
274
274
|
var authService = new AuthServiceImpl();
|
|
275
275
|
|
|
276
276
|
// src/services/api.ts
|
|
277
|
-
var AGENT_VERSION = "1.0.
|
|
277
|
+
var AGENT_VERSION = "1.0.4";
|
|
278
278
|
var ApiServiceImpl = class {
|
|
279
279
|
sessionId = null;
|
|
280
280
|
async getHeaders() {
|
|
@@ -351,7 +351,7 @@ var ApiServiceImpl = class {
|
|
|
351
351
|
return session;
|
|
352
352
|
}, "Connect");
|
|
353
353
|
}
|
|
354
|
-
async disconnect() {
|
|
354
|
+
async disconnect(reason) {
|
|
355
355
|
if (!this.sessionId) {
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
@@ -359,7 +359,8 @@ var ApiServiceImpl = class {
|
|
|
359
359
|
await this.request("/api/agent/disconnect", {
|
|
360
360
|
method: "POST",
|
|
361
361
|
body: JSON.stringify({
|
|
362
|
-
session_id: this.sessionId
|
|
362
|
+
session_id: this.sessionId,
|
|
363
|
+
reason: reason || "Agent disconnected"
|
|
363
364
|
})
|
|
364
365
|
});
|
|
365
366
|
this.sessionId = null;
|
|
@@ -431,9 +432,15 @@ var ApiServiceImpl = class {
|
|
|
431
432
|
async syncProject(projectId, data) {
|
|
432
433
|
logger.info("api", `Syncing project ${projectId}`);
|
|
433
434
|
await this.withRetry(async () => {
|
|
435
|
+
const payload = {
|
|
436
|
+
repo_path: data.repoPath,
|
|
437
|
+
remote_url: data.remoteUrl,
|
|
438
|
+
default_branch: data.defaultBranch,
|
|
439
|
+
current_branch: data.currentBranch
|
|
440
|
+
};
|
|
434
441
|
await this.request(`/api/agent/projects/${projectId}/sync`, {
|
|
435
442
|
method: "POST",
|
|
436
|
-
body: JSON.stringify(
|
|
443
|
+
body: JSON.stringify(payload)
|
|
437
444
|
});
|
|
438
445
|
logger.success("api", `Project ${projectId} synced`);
|
|
439
446
|
}, "Sync project");
|
package/dist/daemon/runner.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
projectService,
|
|
6
6
|
removePidFile,
|
|
7
7
|
writePidFile
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-UGODXDEE.js";
|
|
9
9
|
import {
|
|
10
10
|
apiService,
|
|
11
11
|
authService,
|
|
12
12
|
config,
|
|
13
13
|
logger
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-XZCWE3IA.js";
|
|
15
15
|
|
|
16
16
|
// src/daemon/runner.ts
|
|
17
17
|
import "dotenv/config";
|
|
@@ -27,8 +27,12 @@ var WebSocketServiceImpl = class {
|
|
|
27
27
|
isManualDisconnect = false;
|
|
28
28
|
// Event handlers
|
|
29
29
|
commitApprovedHandlers = [];
|
|
30
|
+
commitPendingHandlers = [];
|
|
31
|
+
suggestionCreatedHandlers = [];
|
|
30
32
|
projectUpdatedHandlers = [];
|
|
31
33
|
disconnectHandlers = [];
|
|
34
|
+
agentDisconnectedHandlers = [];
|
|
35
|
+
syncRequestedHandlers = [];
|
|
32
36
|
async connect() {
|
|
33
37
|
try {
|
|
34
38
|
const token = await authService.getToken();
|
|
@@ -108,12 +112,24 @@ var WebSocketServiceImpl = class {
|
|
|
108
112
|
onCommitApproved(handler) {
|
|
109
113
|
this.commitApprovedHandlers.push(handler);
|
|
110
114
|
}
|
|
115
|
+
onCommitPending(handler) {
|
|
116
|
+
this.commitPendingHandlers.push(handler);
|
|
117
|
+
}
|
|
118
|
+
onSuggestionCreated(handler) {
|
|
119
|
+
this.suggestionCreatedHandlers.push(handler);
|
|
120
|
+
}
|
|
111
121
|
onProjectUpdated(handler) {
|
|
112
122
|
this.projectUpdatedHandlers.push(handler);
|
|
113
123
|
}
|
|
114
124
|
onDisconnect(handler) {
|
|
115
125
|
this.disconnectHandlers.push(handler);
|
|
116
126
|
}
|
|
127
|
+
onAgentDisconnected(handler) {
|
|
128
|
+
this.agentDisconnectedHandlers.push(handler);
|
|
129
|
+
}
|
|
130
|
+
onSyncRequested(handler) {
|
|
131
|
+
this.syncRequestedHandlers.push(handler);
|
|
132
|
+
}
|
|
117
133
|
sendMessage(message) {
|
|
118
134
|
if (!this.isConnected()) {
|
|
119
135
|
logger.warn("websocket", "Cannot send message: not connected");
|
|
@@ -139,6 +155,18 @@ var WebSocketServiceImpl = class {
|
|
|
139
155
|
this.commitApprovedHandlers.forEach((handler) => handler(commit, project));
|
|
140
156
|
return;
|
|
141
157
|
}
|
|
158
|
+
if (message.event === "commit.pending") {
|
|
159
|
+
const { pendingCommit } = message.data;
|
|
160
|
+
logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
|
|
161
|
+
this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (message.event === "suggestion.created") {
|
|
165
|
+
const { suggestion } = message.data;
|
|
166
|
+
logger.info("websocket", `Suggestion created: ${suggestion.id}`);
|
|
167
|
+
this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
142
170
|
if (message.event === "project.updated") {
|
|
143
171
|
const { project } = message.data;
|
|
144
172
|
logger.info("websocket", `Project updated: ${project.id}`);
|
|
@@ -148,6 +176,13 @@ var WebSocketServiceImpl = class {
|
|
|
148
176
|
if (message.event === "sync.requested") {
|
|
149
177
|
const { projectId } = message.data;
|
|
150
178
|
logger.info("websocket", `Sync requested for project: ${projectId}`);
|
|
179
|
+
this.syncRequestedHandlers.forEach((handler) => handler(projectId));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (message.event === "agent.disconnected") {
|
|
183
|
+
const reason = message.data?.reason || "Server requested disconnect";
|
|
184
|
+
logger.warn("websocket", `Agent disconnected by server: ${reason}`);
|
|
185
|
+
this.agentDisconnectedHandlers.forEach((handler) => handler(reason));
|
|
151
186
|
return;
|
|
152
187
|
}
|
|
153
188
|
logger.debug("websocket", `Unhandled event: ${message.event}`);
|
|
@@ -358,6 +393,19 @@ var FileWatcher = class {
|
|
|
358
393
|
addProject(projectPath, projectId) {
|
|
359
394
|
this.watchProject(projectPath, projectId);
|
|
360
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Sync a project by ID (called when server requests a sync)
|
|
398
|
+
*/
|
|
399
|
+
async syncProjectById(projectId) {
|
|
400
|
+
const linkedProjects = projectService.getAllLinkedProjects();
|
|
401
|
+
for (const [projectPath, linkedProject] of Object.entries(linkedProjects)) {
|
|
402
|
+
if (linkedProject.projectId === projectId) {
|
|
403
|
+
await this.performSync(projectPath, projectId);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
logger.warn("watcher", `Cannot sync: project ${projectId} not found in linked projects`);
|
|
408
|
+
}
|
|
361
409
|
/**
|
|
362
410
|
* Remove a project from watching (called when a project is unlinked)
|
|
363
411
|
*/
|
|
@@ -377,15 +425,42 @@ var FileWatcher = class {
|
|
|
377
425
|
}
|
|
378
426
|
};
|
|
379
427
|
|
|
428
|
+
// src/utils/notify.ts
|
|
429
|
+
import notifier from "node-notifier";
|
|
430
|
+
function notify(options) {
|
|
431
|
+
try {
|
|
432
|
+
notifier.notify({
|
|
433
|
+
title: options.title,
|
|
434
|
+
message: options.message,
|
|
435
|
+
open: options.open,
|
|
436
|
+
sound: true,
|
|
437
|
+
wait: false,
|
|
438
|
+
appID: "Stint Agent"
|
|
439
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
+
}, (error) => {
|
|
441
|
+
if (error) {
|
|
442
|
+
logger.error("notify", "Failed to send notification", error);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
logger.error("notify", "Failed to send notification", error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
380
450
|
// src/daemon/index.ts
|
|
381
451
|
var heartbeatInterval = null;
|
|
382
452
|
var isShuttingDown = false;
|
|
453
|
+
var shutdownReason;
|
|
383
454
|
var fileWatcher = new FileWatcher();
|
|
384
455
|
async function startDaemon() {
|
|
385
456
|
logger.info("daemon", "Starting daemon...");
|
|
386
457
|
try {
|
|
387
458
|
const user = await authService.validateToken();
|
|
388
459
|
if (!user) {
|
|
460
|
+
notify({
|
|
461
|
+
title: "Stint Agent",
|
|
462
|
+
message: 'Authentication expired. Please run "stint login" to reconnect.'
|
|
463
|
+
});
|
|
389
464
|
throw new Error('Not authenticated. Please run "stint login" first.');
|
|
390
465
|
}
|
|
391
466
|
logger.info("daemon", `Authenticated as ${user.email}`);
|
|
@@ -406,6 +481,30 @@ async function startDaemon() {
|
|
|
406
481
|
websocketService.onDisconnect(() => {
|
|
407
482
|
logger.warn("daemon", "WebSocket disconnected, will attempt to reconnect");
|
|
408
483
|
});
|
|
484
|
+
websocketService.onAgentDisconnected(async (reason) => {
|
|
485
|
+
logger.warn("daemon", `Server disconnected agent: ${reason}`);
|
|
486
|
+
logger.info("daemon", "Initiating graceful shutdown...");
|
|
487
|
+
await shutdown(`Server: ${reason}`);
|
|
488
|
+
process.exit(0);
|
|
489
|
+
});
|
|
490
|
+
websocketService.onSuggestionCreated((suggestion) => {
|
|
491
|
+
logger.info("daemon", `Suggestion created: ${suggestion.title} (${suggestion.priority})`);
|
|
492
|
+
notify({
|
|
493
|
+
title: "New Suggestion",
|
|
494
|
+
message: `${suggestion.title}
|
|
495
|
+
Priority: ${suggestion.priority}`,
|
|
496
|
+
open: `https://stint.codes/projects/${suggestion.project_id}/suggestions/${suggestion.id}`
|
|
497
|
+
// Hypothetical URL structure
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
websocketService.onSyncRequested(async (projectId) => {
|
|
501
|
+
logger.info("daemon", `Server requested sync for project: ${projectId}`);
|
|
502
|
+
try {
|
|
503
|
+
await fileWatcher.syncProjectById(projectId);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
logger.error("daemon", `Failed to sync project ${projectId}`, error);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
409
508
|
setupSignalHandlers();
|
|
410
509
|
startHeartbeat();
|
|
411
510
|
fileWatcher.start();
|
|
@@ -443,15 +542,16 @@ function setupSignalHandlers() {
|
|
|
443
542
|
signals.forEach((signal) => {
|
|
444
543
|
process.on(signal, async () => {
|
|
445
544
|
logger.info("daemon", `Received ${signal}, shutting down...`);
|
|
446
|
-
await shutdown();
|
|
545
|
+
await shutdown(`Signal: ${signal}`);
|
|
447
546
|
process.exit(0);
|
|
448
547
|
});
|
|
449
548
|
});
|
|
450
549
|
logger.info("daemon", "Signal handlers registered");
|
|
451
550
|
}
|
|
452
|
-
async function shutdown() {
|
|
551
|
+
async function shutdown(reason) {
|
|
453
552
|
if (isShuttingDown) return;
|
|
454
553
|
isShuttingDown = true;
|
|
554
|
+
shutdownReason = reason;
|
|
455
555
|
logger.info("daemon", "Shutting down daemon...");
|
|
456
556
|
stopHeartbeat();
|
|
457
557
|
try {
|
|
@@ -467,7 +567,7 @@ async function shutdown() {
|
|
|
467
567
|
logger.error("daemon", "Failed to disconnect from WebSocket", error);
|
|
468
568
|
}
|
|
469
569
|
try {
|
|
470
|
-
await apiService.disconnect();
|
|
570
|
+
await apiService.disconnect(shutdownReason);
|
|
471
571
|
logger.info("daemon", "Disconnected from API");
|
|
472
572
|
} catch (error) {
|
|
473
573
|
logger.error("daemon", "Failed to disconnect from API", error);
|
package/dist/index.js
CHANGED
|
@@ -8,18 +8,18 @@ import {
|
|
|
8
8
|
projectService,
|
|
9
9
|
spawnDetached,
|
|
10
10
|
validatePidFile
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UGODXDEE.js";
|
|
12
12
|
import {
|
|
13
13
|
apiService,
|
|
14
14
|
authService,
|
|
15
15
|
config,
|
|
16
16
|
logger
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-XZCWE3IA.js";
|
|
18
18
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
import "dotenv/config";
|
|
21
21
|
import { Command } from "commander";
|
|
22
|
-
import
|
|
22
|
+
import chalk11 from "chalk";
|
|
23
23
|
|
|
24
24
|
// src/commands/login.ts
|
|
25
25
|
import open from "open";
|
|
@@ -514,7 +514,7 @@ function registerLinkCommand(program2) {
|
|
|
514
514
|
try {
|
|
515
515
|
repoInfo = await gitService.getRepoInfo(cwd);
|
|
516
516
|
} catch (e) {
|
|
517
|
-
logger.warn("link",
|
|
517
|
+
logger.warn("link", `Failed to get repo info for creation metadata: ${e.message}`);
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
const newProject = await apiService.createProject({
|
|
@@ -1061,19 +1061,225 @@ function getTimeAgo(date) {
|
|
|
1061
1061
|
return date.toLocaleDateString();
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
+
// src/commands/install.ts
|
|
1065
|
+
import ora10 from "ora";
|
|
1066
|
+
import chalk10 from "chalk";
|
|
1067
|
+
import fs2 from "fs";
|
|
1068
|
+
import path4 from "path";
|
|
1069
|
+
import os3 from "os";
|
|
1070
|
+
import { exec } from "child_process";
|
|
1071
|
+
import { promisify } from "util";
|
|
1072
|
+
var execAsync = promisify(exec);
|
|
1073
|
+
var WINDOWS_TASK_NAME = "StintAgentDaemon";
|
|
1074
|
+
var MAC_PLIST_NAME = "codes.stint.agent.plist";
|
|
1075
|
+
var SYSTEMD_SERVICE_NAME = "stint-agent.service";
|
|
1076
|
+
function getDaemonCommand() {
|
|
1077
|
+
const scriptPath = process.argv[1];
|
|
1078
|
+
return `"${process.execPath}" "${scriptPath}" daemon start`;
|
|
1079
|
+
}
|
|
1080
|
+
async function installWindows() {
|
|
1081
|
+
const command = getDaemonCommand();
|
|
1082
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
1083
|
+
try {
|
|
1084
|
+
await execAsync(`schtasks /Create /SC ONLOGON /TN "${WINDOWS_TASK_NAME}" /TR "${escapedCommand}" /F`);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error.message.includes("Access is denied")) {
|
|
1087
|
+
throw new Error("Access denied. Please run this command as Administrator (Right-click Terminal > Run as administrator).");
|
|
1088
|
+
}
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async function uninstallWindows() {
|
|
1093
|
+
await execAsync(`schtasks /Delete /TN "${WINDOWS_TASK_NAME}" /F`);
|
|
1094
|
+
}
|
|
1095
|
+
function getMacPlistContent() {
|
|
1096
|
+
const scriptPath = process.argv[1];
|
|
1097
|
+
const logPath = path4.join(os3.homedir(), ".config", "stint", "logs", "launchd.log");
|
|
1098
|
+
const errorPath = path4.join(os3.homedir(), ".config", "stint", "logs", "launchd.error.log");
|
|
1099
|
+
const logDir = path4.dirname(logPath);
|
|
1100
|
+
if (!fs2.existsSync(logDir)) {
|
|
1101
|
+
fs2.mkdirSync(logDir, { recursive: true });
|
|
1102
|
+
}
|
|
1103
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1104
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1105
|
+
<plist version="1.0">
|
|
1106
|
+
<dict>
|
|
1107
|
+
<key>Label</key>
|
|
1108
|
+
<string>codes.stint.agent</string>
|
|
1109
|
+
<key>ProgramArguments</key>
|
|
1110
|
+
<array>
|
|
1111
|
+
<string>${process.execPath}</string>
|
|
1112
|
+
<string>${scriptPath}</string>
|
|
1113
|
+
<string>daemon</string>
|
|
1114
|
+
<string>start</string>
|
|
1115
|
+
</array>
|
|
1116
|
+
<key>RunAtLoad</key>
|
|
1117
|
+
<true/>
|
|
1118
|
+
<key>StandardOutPath</key>
|
|
1119
|
+
<string>${logPath}</string>
|
|
1120
|
+
<key>StandardErrorPath</key>
|
|
1121
|
+
<string>${errorPath}</string>
|
|
1122
|
+
<key>EnvironmentVariables</key>
|
|
1123
|
+
<dict>
|
|
1124
|
+
<key>PATH</key>
|
|
1125
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1126
|
+
</dict>
|
|
1127
|
+
</dict>
|
|
1128
|
+
</plist>`;
|
|
1129
|
+
}
|
|
1130
|
+
async function installMac() {
|
|
1131
|
+
const plistContent = getMacPlistContent();
|
|
1132
|
+
const launchAgentsDir = path4.join(os3.homedir(), "Library", "LaunchAgents");
|
|
1133
|
+
const plistPath = path4.join(launchAgentsDir, MAC_PLIST_NAME);
|
|
1134
|
+
if (!fs2.existsSync(launchAgentsDir)) {
|
|
1135
|
+
fs2.mkdirSync(launchAgentsDir, { recursive: true });
|
|
1136
|
+
}
|
|
1137
|
+
fs2.writeFileSync(plistPath, plistContent);
|
|
1138
|
+
try {
|
|
1139
|
+
await execAsync(`launchctl unload "${plistPath}"`);
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
await execAsync(`launchctl load "${plistPath}"`);
|
|
1143
|
+
}
|
|
1144
|
+
async function uninstallMac() {
|
|
1145
|
+
const launchAgentsDir = path4.join(os3.homedir(), "Library", "LaunchAgents");
|
|
1146
|
+
const plistPath = path4.join(launchAgentsDir, MAC_PLIST_NAME);
|
|
1147
|
+
if (fs2.existsSync(plistPath)) {
|
|
1148
|
+
try {
|
|
1149
|
+
await execAsync(`launchctl unload "${plistPath}"`);
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
fs2.unlinkSync(plistPath);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function getSystemdServiceContent() {
|
|
1156
|
+
const scriptPath = process.argv[1];
|
|
1157
|
+
return `[Unit]
|
|
1158
|
+
Description=Stint Agent (Project Assistant)
|
|
1159
|
+
After=network.target
|
|
1160
|
+
|
|
1161
|
+
[Service]
|
|
1162
|
+
Type=forking
|
|
1163
|
+
ExecStart=${process.execPath} "${scriptPath}" daemon start
|
|
1164
|
+
Restart=on-failure
|
|
1165
|
+
RestartSec=5
|
|
1166
|
+
StandardOutput=journal
|
|
1167
|
+
StandardError=journal
|
|
1168
|
+
|
|
1169
|
+
[Install]
|
|
1170
|
+
WantedBy=default.target`;
|
|
1171
|
+
}
|
|
1172
|
+
async function installLinux() {
|
|
1173
|
+
const systemdDir = path4.join(os3.homedir(), ".config", "systemd", "user");
|
|
1174
|
+
const servicePath = path4.join(systemdDir, SYSTEMD_SERVICE_NAME);
|
|
1175
|
+
if (!fs2.existsSync(systemdDir)) {
|
|
1176
|
+
fs2.mkdirSync(systemdDir, { recursive: true });
|
|
1177
|
+
}
|
|
1178
|
+
const serviceContent = getSystemdServiceContent();
|
|
1179
|
+
fs2.writeFileSync(servicePath, serviceContent);
|
|
1180
|
+
await execAsync("systemctl --user daemon-reload");
|
|
1181
|
+
await execAsync(`systemctl --user enable ${SYSTEMD_SERVICE_NAME}`);
|
|
1182
|
+
await execAsync(`systemctl --user start ${SYSTEMD_SERVICE_NAME}`);
|
|
1183
|
+
}
|
|
1184
|
+
async function uninstallLinux() {
|
|
1185
|
+
const systemdDir = path4.join(os3.homedir(), ".config", "systemd", "user");
|
|
1186
|
+
const servicePath = path4.join(systemdDir, SYSTEMD_SERVICE_NAME);
|
|
1187
|
+
try {
|
|
1188
|
+
await execAsync(`systemctl --user stop ${SYSTEMD_SERVICE_NAME}`);
|
|
1189
|
+
await execAsync(`systemctl --user disable ${SYSTEMD_SERVICE_NAME}`);
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
if (fs2.existsSync(servicePath)) {
|
|
1193
|
+
fs2.unlinkSync(servicePath);
|
|
1194
|
+
await execAsync("systemctl --user daemon-reload");
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function registerInstallCommand(program2) {
|
|
1198
|
+
program2.command("install").description("Install stint agent to run on system startup").action(async () => {
|
|
1199
|
+
const spinner = ora10("Checking authentication...").start();
|
|
1200
|
+
try {
|
|
1201
|
+
const user = await authService.validateToken();
|
|
1202
|
+
if (!user) {
|
|
1203
|
+
spinner.fail("Not authenticated");
|
|
1204
|
+
console.log(chalk10.red("\n\u2716 You must be logged in to install the background agent on startup."));
|
|
1205
|
+
console.log(chalk10.gray('Run "stint login" first.\n'));
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
spinner.text = "Installing startup agent...";
|
|
1209
|
+
const platform = os3.platform();
|
|
1210
|
+
if (platform === "win32") {
|
|
1211
|
+
await installWindows();
|
|
1212
|
+
} else if (platform === "darwin") {
|
|
1213
|
+
await installMac();
|
|
1214
|
+
} else if (platform === "linux") {
|
|
1215
|
+
await installLinux();
|
|
1216
|
+
} else {
|
|
1217
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
1218
|
+
}
|
|
1219
|
+
spinner.succeed("Installed successfully!");
|
|
1220
|
+
console.log(chalk10.green(`
|
|
1221
|
+
\u2713 Stint agent configured to start on login`));
|
|
1222
|
+
if (platform === "win32") {
|
|
1223
|
+
console.log(chalk10.gray(`Registered Task Scheduler task: ${WINDOWS_TASK_NAME}`));
|
|
1224
|
+
} else if (platform === "darwin") {
|
|
1225
|
+
console.log(chalk10.gray(`Created LaunchAgent: ~/Library/LaunchAgents/${MAC_PLIST_NAME}`));
|
|
1226
|
+
} else if (platform === "linux") {
|
|
1227
|
+
console.log(chalk10.gray(`Created systemd user service: ${SYSTEMD_SERVICE_NAME}`));
|
|
1228
|
+
}
|
|
1229
|
+
console.log();
|
|
1230
|
+
logger.success("install", `Agent installed on startup for ${platform}`);
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
spinner.fail("Installation failed");
|
|
1233
|
+
logger.error("install", "Install command failed", error);
|
|
1234
|
+
console.error(chalk10.red(`
|
|
1235
|
+
\u2716 Error: ${error.message}
|
|
1236
|
+
`));
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function registerUninstallCommand(program2) {
|
|
1242
|
+
program2.command("uninstall").description("Remove stint agent from system startup").action(async () => {
|
|
1243
|
+
const spinner = ora10("Removing startup agent...").start();
|
|
1244
|
+
try {
|
|
1245
|
+
const platform = os3.platform();
|
|
1246
|
+
if (platform === "win32") {
|
|
1247
|
+
await uninstallWindows();
|
|
1248
|
+
} else if (platform === "darwin") {
|
|
1249
|
+
await uninstallMac();
|
|
1250
|
+
} else if (platform === "linux") {
|
|
1251
|
+
await uninstallLinux();
|
|
1252
|
+
} else {
|
|
1253
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
1254
|
+
}
|
|
1255
|
+
spinner.succeed("Uninstalled successfully");
|
|
1256
|
+
console.log(chalk10.gray("\nStint agent removed from system startup.\n"));
|
|
1257
|
+
logger.success("install", `Agent uninstalled from startup for ${platform}`);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
spinner.fail("Uninstall failed");
|
|
1260
|
+
logger.error("install", "Uninstall command failed", error);
|
|
1261
|
+
console.error(chalk10.red(`
|
|
1262
|
+
\u2716 Error: ${error.message}
|
|
1263
|
+
`));
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1064
1269
|
// src/index.ts
|
|
1065
|
-
var AGENT_VERSION = "1.0.
|
|
1270
|
+
var AGENT_VERSION = "1.0.4";
|
|
1066
1271
|
var program = new Command();
|
|
1067
1272
|
program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version(AGENT_VERSION, "-V, --version", "output the current version").addHelpText("after", `
|
|
1068
|
-
${
|
|
1069
|
-
${
|
|
1070
|
-
${
|
|
1071
|
-
${
|
|
1072
|
-
${
|
|
1073
|
-
${
|
|
1273
|
+
${chalk11.bold("Examples:")}
|
|
1274
|
+
${chalk11.cyan("$")} stint login ${chalk11.gray("# Authenticate with Stint")}
|
|
1275
|
+
${chalk11.cyan("$")} stint install ${chalk11.gray("# Install agent to run on startup")}
|
|
1276
|
+
${chalk11.cyan("$")} stint link ${chalk11.gray("# Link current directory to a project")}
|
|
1277
|
+
${chalk11.cyan("$")} stint daemon start ${chalk11.gray("# Start background daemon")}
|
|
1278
|
+
${chalk11.cyan("$")} stint status ${chalk11.gray("# Check status")}
|
|
1279
|
+
${chalk11.cyan("$")} stint commits ${chalk11.gray("# List pending commits")}
|
|
1074
1280
|
|
|
1075
|
-
${
|
|
1076
|
-
For more information, visit: ${
|
|
1281
|
+
${chalk11.bold("Documentation:")}
|
|
1282
|
+
For more information, visit: ${chalk11.blue("https://stint.codes/docs")}
|
|
1077
1283
|
`);
|
|
1078
1284
|
registerLoginCommand(program);
|
|
1079
1285
|
registerLogoutCommand(program);
|
|
@@ -1084,6 +1290,8 @@ registerStatusCommand(program);
|
|
|
1084
1290
|
registerSyncCommand(program);
|
|
1085
1291
|
registerDaemonCommands(program);
|
|
1086
1292
|
registerCommitCommands(program);
|
|
1293
|
+
registerInstallCommand(program);
|
|
1294
|
+
registerUninstallCommand(program);
|
|
1087
1295
|
program.exitOverride();
|
|
1088
1296
|
try {
|
|
1089
1297
|
await program.parseAsync(process.argv);
|
|
@@ -1091,7 +1299,7 @@ try {
|
|
|
1091
1299
|
const commanderError = error;
|
|
1092
1300
|
if (commanderError.code !== "commander.help" && commanderError.code !== "commander.version") {
|
|
1093
1301
|
logger.error("cli", "Command execution failed", error);
|
|
1094
|
-
console.error(
|
|
1302
|
+
console.error(chalk11.red(`
|
|
1095
1303
|
\u2716 Error: ${error.message}
|
|
1096
1304
|
`));
|
|
1097
1305
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gowelle/stint-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Local agent for Stint - Project Assistant",
|
|
5
5
|
"author": "Gowelle John <gowelle.john@icloud.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"conf": "^12.0.0",
|
|
43
43
|
"dotenv": "^17.2.3",
|
|
44
44
|
"node-fetch": "^3.3.2",
|
|
45
|
+
"node-notifier": "^10.0.1",
|
|
45
46
|
"open": "^10.0.0",
|
|
46
47
|
"ora": "^8.0.1",
|
|
47
48
|
"simple-git": "^3.22.0",
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/node": "^20.11.0",
|
|
53
|
+
"@types/node-notifier": "^8.0.5",
|
|
52
54
|
"@types/ws": "^8.5.10",
|
|
53
55
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
54
56
|
"@typescript-eslint/parser": "^8.50.0",
|
|
@@ -60,4 +62,4 @@
|
|
|
60
62
|
"engines": {
|
|
61
63
|
"node": ">=20.0.0"
|
|
62
64
|
}
|
|
63
|
-
}
|
|
65
|
+
}
|