@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 daemon start` | Start background daemon |
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 |
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  apiService
3
- } from "./chunk-5XNQJJVE.js";
3
+ } from "./chunk-XZCWE3IA.js";
4
4
  export {
5
5
  apiService
6
6
  };
@@ -2,7 +2,7 @@ import {
2
2
  apiService,
3
3
  config,
4
4
  logger
5
- } from "./chunk-5XNQJJVE.js";
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-ZMYJOXXU.js");
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.2";
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(data)
443
+ body: JSON.stringify(payload)
437
444
  });
438
445
  logger.success("api", `Project ${projectId} synced`);
439
446
  }, "Sync project");
@@ -5,13 +5,13 @@ import {
5
5
  projectService,
6
6
  removePidFile,
7
7
  writePidFile
8
- } from "../chunk-WFWZCAJ6.js";
8
+ } from "../chunk-UGODXDEE.js";
9
9
  import {
10
10
  apiService,
11
11
  authService,
12
12
  config,
13
13
  logger
14
- } from "../chunk-5XNQJJVE.js";
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-WFWZCAJ6.js";
11
+ } from "./chunk-UGODXDEE.js";
12
12
  import {
13
13
  apiService,
14
14
  authService,
15
15
  config,
16
16
  logger
17
- } from "./chunk-5XNQJJVE.js";
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 chalk10 from "chalk";
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", "Failed to get repo info for creation metadata", e);
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.2";
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
- ${chalk10.bold("Examples:")}
1069
- ${chalk10.cyan("$")} stint login ${chalk10.gray("# Authenticate with Stint")}
1070
- ${chalk10.cyan("$")} stint link ${chalk10.gray("# Link current directory to a project")}
1071
- ${chalk10.cyan("$")} stint daemon start ${chalk10.gray("# Start background daemon")}
1072
- ${chalk10.cyan("$")} stint status ${chalk10.gray("# Check status")}
1073
- ${chalk10.cyan("$")} stint commits ${chalk10.gray("# List pending commits")}
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
- ${chalk10.bold("Documentation:")}
1076
- For more information, visit: ${chalk10.blue("https://stint.codes/docs")}
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(chalk10.red(`
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.2",
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
+ }