@gowelle/stint-agent 1.1.0 → 1.2.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/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  commitQueue,
4
+ websocketService
5
+ } from "./chunk-IJUJ6NEL.js";
6
+ import {
7
+ apiService
8
+ } from "./chunk-W4JGOGR7.js";
9
+ import {
4
10
  getPidFilePath,
5
11
  gitService,
6
12
  isProcessRunning,
@@ -8,18 +14,17 @@ import {
8
14
  projectService,
9
15
  spawnDetached,
10
16
  validatePidFile
11
- } from "./chunk-OHOFKJL7.js";
17
+ } from "./chunk-FBQA4K5J.js";
12
18
  import {
13
- apiService,
14
19
  authService,
15
20
  config,
16
21
  logger
17
- } from "./chunk-IAERVP6F.js";
22
+ } from "./chunk-RHMTZK2J.js";
18
23
 
19
24
  // src/index.ts
20
25
  import "dotenv/config";
21
26
  import { Command } from "commander";
22
- import chalk12 from "chalk";
27
+ import chalk13 from "chalk";
23
28
 
24
29
  // src/commands/login.ts
25
30
  import open from "open";
@@ -609,10 +614,22 @@ import process4 from "process";
609
614
  import path2 from "path";
610
615
  import os from "os";
611
616
  function registerStatusCommand(program2) {
612
- program2.command("status").description("Show linked project and connection status").action(async () => {
617
+ program2.command("status").description("Show linked project and connection status").option("-d, --dashboard", "Launch interactive TUI dashboard").action(async (options) => {
618
+ const cwd = process4.cwd();
619
+ if (options.dashboard) {
620
+ try {
621
+ const { render } = await import("ink");
622
+ const { createElement } = await import("react");
623
+ const { StatusDashboard } = await import("./StatusDashboard-7ZIGEOQ4.js");
624
+ render(createElement(StatusDashboard, { cwd }));
625
+ return;
626
+ } catch (error) {
627
+ console.error(chalk6.red(`Failed to start dashboard: ${error.message}`));
628
+ process4.exit(1);
629
+ }
630
+ }
613
631
  const spinner = ora6("Gathering status...").start();
614
632
  try {
615
- const cwd = process4.cwd();
616
633
  const linkedProject = await projectService.getLinkedProject(cwd);
617
634
  const user = await authService.validateToken();
618
635
  spinner.stop();
@@ -705,13 +722,24 @@ function registerSyncCommand(program2) {
705
722
  console.log(chalk7.gray('Run "stint link" first to link this directory.\n'));
706
723
  process5.exit(1);
707
724
  }
708
- spinner.text = "Gathering repository information...";
725
+ spinner.text = "Analyzing repository...";
726
+ const status = await gitService.getStatus(cwd);
727
+ const totalFiles = status.staged.length + status.unstaged.length + status.untracked.length;
728
+ spinner.text = `Found ${totalFiles} files to analyze...`;
729
+ spinner.text = "Getting branch information...";
709
730
  const repoInfo = await gitService.getRepoInfo(cwd);
710
- spinner.text = "Syncing with server...";
711
- await apiService.syncProject(linkedProject.projectId, repoInfo);
712
- spinner.succeed("Sync completed successfully!");
713
- console.log(chalk7.green("\n\u2713 Repository information synced"));
731
+ spinner.text = "Preparing sync payload...";
732
+ const syncSpinner = ora7("Connecting to server...").start();
733
+ try {
734
+ await apiService.syncProject(linkedProject.projectId, repoInfo);
735
+ syncSpinner.succeed("Server sync completed");
736
+ } catch (error) {
737
+ syncSpinner.fail("Server sync failed");
738
+ throw error;
739
+ }
740
+ console.log(chalk7.green("\n\u2713 Repository sync completed"));
714
741
  console.log(chalk7.gray("\u2500".repeat(50)));
742
+ console.log(`${chalk7.bold("Files:")} ${totalFiles} total (${status.staged.length} staged, ${status.unstaged.length} modified, ${status.untracked.length} untracked)`);
715
743
  console.log(`${chalk7.bold("Project ID:")} ${linkedProject.projectId}`);
716
744
  console.log(`${chalk7.bold("Branch:")} ${repoInfo.currentBranch}`);
717
745
  console.log(`${chalk7.bold("Commit:")} ${repoInfo.lastCommitSha.substring(0, 7)} - ${repoInfo.lastCommitMessage}`);
@@ -734,11 +762,160 @@ import ora8 from "ora";
734
762
  import chalk8 from "chalk";
735
763
  import fs from "fs";
736
764
  import path3 from "path";
737
- import os2 from "os";
765
+ import os3 from "os";
738
766
  import { fileURLToPath } from "url";
739
767
  import { dirname } from "path";
768
+ import { createInterface } from "readline";
769
+
770
+ // src/utils/monitor.ts
771
+ import os2 from "os";
772
+ import { readFileSync } from "fs";
773
+ import { execSync } from "child_process";
774
+ function getProcessStats(pid) {
775
+ try {
776
+ const platform = os2.platform();
777
+ if (platform === "linux") {
778
+ return getLinuxStats(pid);
779
+ } else if (platform === "darwin") {
780
+ return getMacStats(pid);
781
+ } else if (platform === "win32") {
782
+ return getWindowsStats(pid);
783
+ }
784
+ throw new Error(`Unsupported platform: ${platform}`);
785
+ } catch (error) {
786
+ logger.error("monitor", `Failed to get process stats for PID ${pid}`, error);
787
+ return null;
788
+ }
789
+ }
790
+ function getLinuxStats(pid) {
791
+ const statContent = readFileSync(`/proc/${pid}/stat`, "utf8");
792
+ const statParts = statContent.split(" ");
793
+ const statusContent = readFileSync(`/proc/${pid}/status`, "utf8");
794
+ const vmRSS = parseInt(statusContent.match(/VmRSS:\s+(\d+)/)?.[1] || "0");
795
+ readFileSync("/proc/stat", "utf8");
796
+ const utime = parseInt(statParts[13]);
797
+ const stime = parseInt(statParts[14]);
798
+ const starttime = parseInt(statParts[21]);
799
+ const threads = parseInt(statParts[19]);
800
+ const totalTime = utime + stime;
801
+ const seconds = os2.uptime() - starttime / os2.cpus().length;
802
+ const cpuPercent = totalTime / seconds * 100 / os2.cpus().length;
803
+ return {
804
+ pid,
805
+ cpuPercent: Math.round(cpuPercent * 100) / 100,
806
+ memoryMB: Math.round(vmRSS / 1024 * 100) / 100,
807
+ uptime: Math.round(seconds),
808
+ threads
809
+ };
810
+ }
811
+ function getMacStats(pid) {
812
+ const psOutput = execSync(`ps -p ${pid} -o %cpu,%mem,etime,thcount`).toString();
813
+ const [cpu, mem, etime, threads] = psOutput.split("\n")[1].trim().split(/\s+/);
814
+ const uptimeSeconds = parseElapsedTime(etime);
815
+ const totalMem = os2.totalmem() / (1024 * 1024);
816
+ const memoryMB = parseFloat(mem) / 100 * totalMem;
817
+ return {
818
+ pid,
819
+ cpuPercent: Math.round(parseFloat(cpu) * 100) / 100,
820
+ memoryMB: Math.round(memoryMB * 100) / 100,
821
+ uptime: uptimeSeconds,
822
+ threads: parseInt(threads)
823
+ };
824
+ }
825
+ function getWindowsStats(pid) {
826
+ const wmicOutput = execSync(
827
+ `wmic path Win32_PerfFormattedData_PerfProc_Process WHERE IDProcess=${pid} get PercentProcessorTime,WorkingSetPrivate,ElapsedTime,ThreadCount /format:csv`
828
+ ).toString();
829
+ const [, , data] = wmicOutput.trim().split("\n");
830
+ if (!data) {
831
+ throw new Error(`Process ${pid} not found`);
832
+ }
833
+ const [, cpu, workingSet, elapsedTime, threads] = data.split(",");
834
+ return {
835
+ pid,
836
+ cpuPercent: Math.round(parseInt(cpu) / os2.cpus().length * 100) / 100,
837
+ memoryMB: Math.round(parseInt(workingSet) / (1024 * 1024) * 100) / 100,
838
+ uptime: Math.round(parseInt(elapsedTime) / 1e3),
839
+ threads: parseInt(threads)
840
+ };
841
+ }
842
+ function parseElapsedTime(etime) {
843
+ const parts = etime.split("-");
844
+ const timeStr = parts[parts.length - 1];
845
+ const timeParts = timeStr.split(":");
846
+ let seconds = 0;
847
+ if (timeParts.length === 3) {
848
+ seconds = parseInt(timeParts[0]) * 3600 + parseInt(timeParts[1]) * 60 + parseInt(timeParts[2]);
849
+ } else if (timeParts.length === 2) {
850
+ seconds = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
851
+ } else {
852
+ seconds = parseInt(timeParts[0]);
853
+ }
854
+ if (parts.length === 2) {
855
+ seconds += parseInt(parts[0]) * 86400;
856
+ }
857
+ return seconds;
858
+ }
859
+
860
+ // src/commands/daemon.ts
740
861
  var __filename = fileURLToPath(import.meta.url);
741
862
  var __dirname = dirname(__filename);
863
+ function parseLogLine(line) {
864
+ const match = line.match(/\[(.*?)\] (\w+)\s+\[(.*?)\] (.*)/);
865
+ if (!match) return null;
866
+ const [, timestamp, level, category, message] = match;
867
+ return {
868
+ timestamp: new Date(timestamp),
869
+ level,
870
+ category,
871
+ message
872
+ };
873
+ }
874
+ function shouldIncludeLine(parsed, filter) {
875
+ if (!parsed) return false;
876
+ if (filter.level && filter.level.toUpperCase() !== parsed.level) {
877
+ return false;
878
+ }
879
+ if (filter.category && !parsed.category.toLowerCase().includes(filter.category.toLowerCase())) {
880
+ return false;
881
+ }
882
+ if (filter.since && parsed.timestamp < filter.since) {
883
+ return false;
884
+ }
885
+ if (filter.until && parsed.timestamp > filter.until) {
886
+ return false;
887
+ }
888
+ if (filter.search && !parsed.message.toLowerCase().includes(filter.search.toLowerCase())) {
889
+ return false;
890
+ }
891
+ return true;
892
+ }
893
+ function formatUptime(seconds) {
894
+ const days = Math.floor(seconds / 86400);
895
+ const hours = Math.floor(seconds % 86400 / 3600);
896
+ const minutes = Math.floor(seconds % 3600 / 60);
897
+ const secs = seconds % 60;
898
+ const parts = [];
899
+ if (days > 0) parts.push(`${days}d`);
900
+ if (hours > 0) parts.push(`${hours}h`);
901
+ if (minutes > 0) parts.push(`${minutes}m`);
902
+ if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
903
+ return parts.join(" ");
904
+ }
905
+ function colorizeLevel(level) {
906
+ switch (level.trim()) {
907
+ case "ERROR":
908
+ return chalk8.red(level);
909
+ case "WARN":
910
+ return chalk8.yellow(level);
911
+ case "INFO":
912
+ return chalk8.blue(level);
913
+ case "DEBUG":
914
+ return chalk8.gray(level);
915
+ default:
916
+ return level;
917
+ }
918
+ }
742
919
  function registerDaemonCommands(program2) {
743
920
  const daemon = program2.command("daemon").description("Manage the Stint daemon");
744
921
  daemon.command("start").description("Start the daemon in the background").action(async () => {
@@ -774,7 +951,7 @@ function registerDaemonCommands(program2) {
774
951
  console.log(chalk8.green(`
775
952
  \u2713 Daemon is running in the background`));
776
953
  console.log(chalk8.gray(`PID: ${daemonPid}`));
777
- console.log(chalk8.gray(`Logs: ${path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log")}
954
+ console.log(chalk8.gray(`Logs: ${path3.join(os3.homedir(), ".config", "stint", "logs", "daemon.log")}
778
955
  `));
779
956
  logger.success("daemon", `Daemon started with PID ${daemonPid}`);
780
957
  } catch (error) {
@@ -835,7 +1012,16 @@ function registerDaemonCommands(program2) {
835
1012
  console.log(`${chalk8.bold("Status:")} ${chalk8.green("\u2713 Running")}`);
836
1013
  console.log(`${chalk8.bold("PID:")} ${pid}`);
837
1014
  console.log(`${chalk8.bold("PID File:")} ${getPidFilePath()}`);
838
- console.log(`${chalk8.bold("Logs:")} ${path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log")}`);
1015
+ console.log(`${chalk8.bold("Logs:")} ${path3.join(os3.homedir(), ".config", "stint", "logs", "daemon.log")}`);
1016
+ const stats = await getProcessStats(pid);
1017
+ if (stats) {
1018
+ console.log(chalk8.blue("\n\u{1F4CA} Resource Usage:"));
1019
+ console.log(chalk8.gray("\u2500".repeat(50)));
1020
+ console.log(`${chalk8.bold("CPU:")} ${stats.cpuPercent}%`);
1021
+ console.log(`${chalk8.bold("Memory:")} ${stats.memoryMB} MB`);
1022
+ console.log(`${chalk8.bold("Threads:")} ${stats.threads}`);
1023
+ console.log(`${chalk8.bold("Uptime:")} ${formatUptime(stats.uptime)}`);
1024
+ }
839
1025
  } else {
840
1026
  console.log(`${chalk8.bold("Status:")} ${chalk8.yellow("Not running")}`);
841
1027
  console.log(chalk8.gray('Run "stint daemon start" to start the daemon.'));
@@ -847,36 +1033,6 @@ function registerDaemonCommands(program2) {
847
1033
  logger.error("daemon", "Status command failed", error);
848
1034
  console.error(chalk8.red(`
849
1035
  \u2716 Error: ${error.message}
850
- `));
851
- process.exit(1);
852
- }
853
- });
854
- daemon.command("logs").description("Tail daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").action(async (options) => {
855
- try {
856
- const logFile = path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log");
857
- if (!fs.existsSync(logFile)) {
858
- console.log(chalk8.yellow("\n\u26A0 No daemon logs found."));
859
- console.log(chalk8.gray("The daemon has not been started yet.\n"));
860
- return;
861
- }
862
- const lines = parseInt(options.lines, 10);
863
- const content = fs.readFileSync(logFile, "utf8");
864
- const logLines = content.split("\n").filter((line) => line.trim());
865
- const lastLines = logLines.slice(-lines);
866
- console.log(chalk8.blue(`
867
- \u{1F4CB} Last ${lastLines.length} lines of daemon logs:
868
- `));
869
- console.log(chalk8.gray("\u2500".repeat(80)));
870
- lastLines.forEach((line) => console.log(line));
871
- console.log(chalk8.gray("\u2500".repeat(80)));
872
- console.log(chalk8.gray(`
873
- Log file: ${logFile}`));
874
- console.log(chalk8.gray('Use "tail -f" to follow logs in real-time.\n'));
875
- logger.info("daemon", "Logs command executed");
876
- } catch (error) {
877
- logger.error("daemon", "Logs command failed", error);
878
- console.error(chalk8.red(`
879
- \u2716 Error: ${error.message}
880
1036
  `));
881
1037
  process.exit(1);
882
1038
  }
@@ -925,6 +1081,119 @@ Log file: ${logFile}`));
925
1081
  throw error;
926
1082
  }
927
1083
  });
1084
+ daemon.command("logs").description("View and filter daemon logs").option("-l, --level <level>", "Filter by log level (INFO, WARN, ERROR, DEBUG)").option("-c, --category <category>", "Filter by log category").option("-s, --since <date>", 'Show logs since date/time (ISO format or relative time like "1h", "2d")').option("-u, --until <date>", 'Show logs until date/time (ISO format or relative time like "1h", "2d")').option("--search <text>", "Search for specific text in log messages").option("-f, --follow", "Follow log output in real time").option("-n, --lines <number>", "Number of lines to show", "50").action(async (command) => {
1085
+ const spinner = ora8("Loading logs...").start();
1086
+ try {
1087
+ const logPath = path3.join(os3.homedir(), ".config", "stint", "logs", "agent.log");
1088
+ if (!fs.existsSync(logPath)) {
1089
+ spinner.fail("No logs found");
1090
+ return;
1091
+ }
1092
+ const now = /* @__PURE__ */ new Date();
1093
+ let since;
1094
+ let until;
1095
+ if (command.since) {
1096
+ if (command.since.match(/^\d+[hdw]$/)) {
1097
+ const value = parseInt(command.since.slice(0, -1));
1098
+ const unit = command.since.slice(-1);
1099
+ const ms = value * {
1100
+ h: 60 * 60 * 1e3,
1101
+ d: 24 * 60 * 60 * 1e3,
1102
+ w: 7 * 24 * 60 * 60 * 1e3
1103
+ }[unit];
1104
+ since = new Date(now.getTime() - ms);
1105
+ } else {
1106
+ since = new Date(command.since);
1107
+ }
1108
+ }
1109
+ if (command.until) {
1110
+ if (command.until.match(/^\d+[hdw]$/)) {
1111
+ const value = parseInt(command.until.slice(0, -1));
1112
+ const unit = command.until.slice(-1);
1113
+ const ms = value * {
1114
+ h: 60 * 60 * 1e3,
1115
+ d: 24 * 60 * 60 * 1e3,
1116
+ w: 7 * 24 * 60 * 60 * 1e3
1117
+ }[unit];
1118
+ until = new Date(now.getTime() - ms);
1119
+ } else {
1120
+ until = new Date(command.until);
1121
+ }
1122
+ }
1123
+ const filter = {
1124
+ level: command.level?.toUpperCase(),
1125
+ category: command.category,
1126
+ since,
1127
+ until,
1128
+ search: command.search
1129
+ };
1130
+ const maxLines = command.follow ? 10 : parseInt(command.lines);
1131
+ const lines = [];
1132
+ const fileStream = fs.createReadStream(logPath, { encoding: "utf8" });
1133
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
1134
+ spinner.stop();
1135
+ for await (const line of rl) {
1136
+ const parsed = parseLogLine(line);
1137
+ if (parsed && shouldIncludeLine(parsed, filter)) {
1138
+ lines.push(line);
1139
+ if (lines.length > maxLines && !command.follow) {
1140
+ lines.shift();
1141
+ }
1142
+ }
1143
+ }
1144
+ if (lines.length === 0) {
1145
+ console.log(chalk8.yellow("\nNo matching logs found\n"));
1146
+ return;
1147
+ }
1148
+ console.log();
1149
+ lines.forEach((line) => {
1150
+ const parsed = parseLogLine(line);
1151
+ if (parsed) {
1152
+ const { timestamp, level, category, message } = parsed;
1153
+ console.log(
1154
+ chalk8.gray(`[${timestamp.toISOString()}]`),
1155
+ colorizeLevel(level.padEnd(5)),
1156
+ chalk8.cyan(`[${category}]`),
1157
+ message
1158
+ );
1159
+ }
1160
+ });
1161
+ console.log();
1162
+ if (command.follow) {
1163
+ console.log(chalk8.gray("Following log output (Ctrl+C to exit)...\n"));
1164
+ const tail = fs.watch(logPath, (eventType) => {
1165
+ if (eventType === "change") {
1166
+ const newLines = fs.readFileSync(logPath, "utf8").split("\n").slice(-1);
1167
+ newLines.forEach((line) => {
1168
+ if (!line) return;
1169
+ const parsed = parseLogLine(line);
1170
+ if (parsed && shouldIncludeLine(parsed, filter)) {
1171
+ const { timestamp, level, category, message } = parsed;
1172
+ console.log(
1173
+ chalk8.gray(`[${timestamp.toISOString()}]`),
1174
+ colorizeLevel(level.padEnd(5)),
1175
+ chalk8.cyan(`[${category}]`),
1176
+ message
1177
+ );
1178
+ }
1179
+ });
1180
+ }
1181
+ });
1182
+ process.on("SIGINT", () => {
1183
+ tail.close();
1184
+ console.log(chalk8.gray("\nStopped following logs\n"));
1185
+ process.exit(0);
1186
+ });
1187
+ }
1188
+ } catch (error) {
1189
+ spinner.fail("Failed to read logs");
1190
+ logger.error("daemon", "Logs command failed", error);
1191
+ console.error(chalk8.red(`
1192
+ \u2716 Error: ${error.message}
1193
+ `));
1194
+ process.exit(1);
1195
+ }
1196
+ });
928
1197
  }
929
1198
 
930
1199
  // src/commands/commit.ts
@@ -1022,7 +1291,7 @@ function registerCommitCommands(program2) {
1022
1291
  console.log(chalk9.yellow("\nCommit cancelled.\n"));
1023
1292
  return;
1024
1293
  }
1025
- const execSpinner = ora9("Executing commit...").start();
1294
+ const execSpinner = ora9("Preparing commit...").start();
1026
1295
  const project = {
1027
1296
  id: linkedProject.projectId,
1028
1297
  name: "Current Project",
@@ -1030,14 +1299,24 @@ function registerCommitCommands(program2) {
1030
1299
  createdAt: "",
1031
1300
  updatedAt: ""
1032
1301
  };
1033
- const sha = await commitQueue.executeCommit(commit, project);
1302
+ const sha = await commitQueue.executeCommit(commit, project, (stage) => {
1303
+ execSpinner.text = stage;
1304
+ });
1034
1305
  execSpinner.succeed("Commit executed successfully!");
1035
1306
  console.log(chalk9.green("\n\u2713 Commit executed"));
1036
1307
  console.log(chalk9.gray("\u2500".repeat(50)));
1037
1308
  console.log(`${chalk9.bold("Commit ID:")} ${commit.id}`);
1038
1309
  console.log(`${chalk9.bold("Message:")} ${commit.message}`);
1039
1310
  console.log(`${chalk9.bold("SHA:")} ${sha}`);
1311
+ console.log(`${chalk9.bold("Files:")} ${status.staged.length} files committed`);
1040
1312
  console.log();
1313
+ if (status.staged.length > 0) {
1314
+ console.log(chalk9.gray("Committed files:"));
1315
+ status.staged.forEach((file) => {
1316
+ console.log(chalk9.green(` + ${file}`));
1317
+ });
1318
+ console.log();
1319
+ }
1041
1320
  logger.success("commit", `Executed commit ${commit.id} -> ${sha}`);
1042
1321
  } catch (error) {
1043
1322
  if (ora9().isSpinning) {
@@ -1066,7 +1345,7 @@ import ora10 from "ora";
1066
1345
  import chalk10 from "chalk";
1067
1346
  import fs2 from "fs";
1068
1347
  import path4 from "path";
1069
- import os3 from "os";
1348
+ import os4 from "os";
1070
1349
  import { exec } from "child_process";
1071
1350
  import { promisify } from "util";
1072
1351
  var execAsync = promisify(exec);
@@ -1094,8 +1373,8 @@ async function uninstallWindows() {
1094
1373
  }
1095
1374
  function getMacPlistContent() {
1096
1375
  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");
1376
+ const logPath = path4.join(os4.homedir(), ".config", "stint", "logs", "launchd.log");
1377
+ const errorPath = path4.join(os4.homedir(), ".config", "stint", "logs", "launchd.error.log");
1099
1378
  const logDir = path4.dirname(logPath);
1100
1379
  if (!fs2.existsSync(logDir)) {
1101
1380
  fs2.mkdirSync(logDir, { recursive: true });
@@ -1129,7 +1408,7 @@ function getMacPlistContent() {
1129
1408
  }
1130
1409
  async function installMac() {
1131
1410
  const plistContent = getMacPlistContent();
1132
- const launchAgentsDir = path4.join(os3.homedir(), "Library", "LaunchAgents");
1411
+ const launchAgentsDir = path4.join(os4.homedir(), "Library", "LaunchAgents");
1133
1412
  const plistPath = path4.join(launchAgentsDir, MAC_PLIST_NAME);
1134
1413
  if (!fs2.existsSync(launchAgentsDir)) {
1135
1414
  fs2.mkdirSync(launchAgentsDir, { recursive: true });
@@ -1142,7 +1421,7 @@ async function installMac() {
1142
1421
  await execAsync(`launchctl load "${plistPath}"`);
1143
1422
  }
1144
1423
  async function uninstallMac() {
1145
- const launchAgentsDir = path4.join(os3.homedir(), "Library", "LaunchAgents");
1424
+ const launchAgentsDir = path4.join(os4.homedir(), "Library", "LaunchAgents");
1146
1425
  const plistPath = path4.join(launchAgentsDir, MAC_PLIST_NAME);
1147
1426
  if (fs2.existsSync(plistPath)) {
1148
1427
  try {
@@ -1170,7 +1449,7 @@ StandardError=journal
1170
1449
  WantedBy=default.target`;
1171
1450
  }
1172
1451
  async function installLinux() {
1173
- const systemdDir = path4.join(os3.homedir(), ".config", "systemd", "user");
1452
+ const systemdDir = path4.join(os4.homedir(), ".config", "systemd", "user");
1174
1453
  const servicePath = path4.join(systemdDir, SYSTEMD_SERVICE_NAME);
1175
1454
  if (!fs2.existsSync(systemdDir)) {
1176
1455
  fs2.mkdirSync(systemdDir, { recursive: true });
@@ -1182,7 +1461,7 @@ async function installLinux() {
1182
1461
  await execAsync(`systemctl --user start ${SYSTEMD_SERVICE_NAME}`);
1183
1462
  }
1184
1463
  async function uninstallLinux() {
1185
- const systemdDir = path4.join(os3.homedir(), ".config", "systemd", "user");
1464
+ const systemdDir = path4.join(os4.homedir(), ".config", "systemd", "user");
1186
1465
  const servicePath = path4.join(systemdDir, SYSTEMD_SERVICE_NAME);
1187
1466
  try {
1188
1467
  await execAsync(`systemctl --user stop ${SYSTEMD_SERVICE_NAME}`);
@@ -1206,7 +1485,7 @@ function registerInstallCommand(program2) {
1206
1485
  process.exit(1);
1207
1486
  }
1208
1487
  spinner.text = "Installing startup agent...";
1209
- const platform = os3.platform();
1488
+ const platform = os4.platform();
1210
1489
  if (platform === "win32") {
1211
1490
  await installWindows();
1212
1491
  } else if (platform === "darwin") {
@@ -1242,7 +1521,7 @@ function registerUninstallCommand(program2) {
1242
1521
  program2.command("uninstall").description("Remove stint agent from system startup").action(async () => {
1243
1522
  const spinner = ora10("Removing startup agent...").start();
1244
1523
  try {
1245
- const platform = os3.platform();
1524
+ const platform = os4.platform();
1246
1525
  if (platform === "win32") {
1247
1526
  await uninstallWindows();
1248
1527
  } else if (platform === "darwin") {
@@ -1271,14 +1550,68 @@ import ora11 from "ora";
1271
1550
  import chalk11 from "chalk";
1272
1551
  import { exec as exec2 } from "child_process";
1273
1552
  import { promisify as promisify2 } from "util";
1553
+ import { join } from "path";
1554
+ import { readFileSync as readFileSync2 } from "fs";
1555
+ import { fileURLToPath as fileURLToPath2 } from "url";
1556
+ import { dirname as dirname2 } from "path";
1274
1557
  var execAsync2 = promisify2(exec2);
1558
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1559
+ function getChannelConfig() {
1560
+ try {
1561
+ const packagePath = join(__dirname2, "..", "..", "package.json");
1562
+ const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
1563
+ return packageJson.stint;
1564
+ } catch {
1565
+ return {
1566
+ channels: {
1567
+ stable: {
1568
+ pattern: "^\\d+\\.\\d+\\.\\d+$",
1569
+ description: "Production-ready releases"
1570
+ }
1571
+ },
1572
+ defaultChannel: "stable"
1573
+ };
1574
+ }
1575
+ }
1576
+ function getVersionPattern(channel) {
1577
+ const config2 = getChannelConfig();
1578
+ return config2.channels[channel]?.pattern || config2.channels[config2.defaultChannel].pattern;
1579
+ }
1580
+ async function getLatestVersionForChannel(channel) {
1581
+ const pattern = getVersionPattern(channel);
1582
+ const regex = new RegExp(pattern);
1583
+ const { stdout } = await execAsync2("npm view @gowelle/stint-agent versions --json");
1584
+ const versions = JSON.parse(stdout);
1585
+ const channelVersions = versions.filter((v) => regex.test(v)).sort((a, b) => {
1586
+ const [aMajor, aMinor, aPatch] = a.split(".").map((part) => parseInt(part.split("-")[0]));
1587
+ const [bMajor, bMinor, bPatch] = b.split(".").map((part) => parseInt(part.split("-")[0]));
1588
+ if (aMajor !== bMajor) return bMajor - aMajor;
1589
+ if (aMinor !== bMinor) return bMinor - aMinor;
1590
+ return bPatch - aPatch;
1591
+ });
1592
+ if (channelVersions.length === 0) {
1593
+ throw new Error(`No versions found for channel: ${channel}`);
1594
+ }
1595
+ return channelVersions[0];
1596
+ }
1275
1597
  function registerUpdateCommand(program2) {
1276
- program2.command("update").description("Update stint agent to the latest version").action(async () => {
1598
+ program2.command("update").description("Update stint agent to the latest version").option("-c, --channel <channel>", "Release channel (stable, beta, nightly)").action(async (command) => {
1277
1599
  const spinner = ora11("Checking for updates...").start();
1278
1600
  try {
1279
1601
  const currentVersion = program2.version();
1280
- const { stdout: latestVersion } = await execAsync2("npm view @gowelle/stint-agent version");
1281
- const cleanLatestVersion = latestVersion.trim();
1602
+ const config2 = getChannelConfig();
1603
+ const channel = (command.opts().channel || config2.defaultChannel).toLowerCase();
1604
+ if (!config2.channels[channel]) {
1605
+ spinner.fail(`Invalid channel: ${channel}`);
1606
+ console.log(chalk11.gray("\nAvailable channels:"));
1607
+ Object.entries(config2.channels).forEach(([name, info]) => {
1608
+ console.log(chalk11.gray(` ${name.padEnd(10)} ${info.description}`));
1609
+ });
1610
+ console.log();
1611
+ process.exit(1);
1612
+ }
1613
+ spinner.text = `Checking for updates in ${channel} channel...`;
1614
+ const cleanLatestVersion = await getLatestVersionForChannel(channel);
1282
1615
  if (currentVersion === cleanLatestVersion) {
1283
1616
  spinner.succeed("Already up to date");
1284
1617
  console.log(chalk11.gray(`
@@ -1289,7 +1622,7 @@ Current version: ${currentVersion}`));
1289
1622
  }
1290
1623
  spinner.info(`Update available: ${currentVersion} \u2192 ${cleanLatestVersion}`);
1291
1624
  spinner.text = "Installing update...";
1292
- await execAsync2("npm install -g @gowelle/stint-agent@latest");
1625
+ await execAsync2(`npm install -g @gowelle/stint-agent@${cleanLatestVersion}`);
1293
1626
  spinner.succeed(`Updated to version ${cleanLatestVersion}`);
1294
1627
  const { valid, pid } = validatePidFile();
1295
1628
  if (valid && pid) {
@@ -1327,20 +1660,193 @@ Current version: ${currentVersion}`));
1327
1660
  });
1328
1661
  }
1329
1662
 
1663
+ // src/commands/doctor.ts
1664
+ import ora12 from "ora";
1665
+ import chalk12 from "chalk";
1666
+ import { exec as exec3 } from "child_process";
1667
+ import { promisify as promisify3 } from "util";
1668
+ import process7 from "process";
1669
+ var execAsync3 = promisify3(exec3);
1670
+ function registerDoctorCommand(program2) {
1671
+ program2.command("doctor").description("Run diagnostics to check environment health").action(async () => {
1672
+ const spinner = ora12("Running diagnostics...").start();
1673
+ let hasErrors = false;
1674
+ try {
1675
+ const checks = [
1676
+ {
1677
+ name: "Git Installation",
1678
+ check: async () => {
1679
+ try {
1680
+ const { stdout } = await execAsync3("git --version");
1681
+ return {
1682
+ success: true,
1683
+ message: `Git ${stdout.trim()} found`
1684
+ };
1685
+ } catch {
1686
+ return {
1687
+ success: false,
1688
+ message: "Git not found in PATH",
1689
+ details: [
1690
+ "Please install Git from https://git-scm.com",
1691
+ "Ensure git is added to your system PATH"
1692
+ ]
1693
+ };
1694
+ }
1695
+ }
1696
+ },
1697
+ {
1698
+ name: "Git Configuration",
1699
+ check: async () => {
1700
+ try {
1701
+ const { stdout: userName } = await execAsync3("git config --global user.name");
1702
+ const { stdout: userEmail } = await execAsync3("git config --global user.email");
1703
+ if (!userName.trim() || !userEmail.trim()) {
1704
+ return {
1705
+ success: false,
1706
+ message: "Git user configuration missing",
1707
+ details: [
1708
+ 'Run: git config --global user.name "Your Name"',
1709
+ 'Run: git config --global user.email "your@email.com"'
1710
+ ]
1711
+ };
1712
+ }
1713
+ return {
1714
+ success: true,
1715
+ message: `Git configured for ${userName.trim()} <${userEmail.trim()}>`
1716
+ };
1717
+ } catch {
1718
+ return {
1719
+ success: false,
1720
+ message: "Failed to read Git configuration"
1721
+ };
1722
+ }
1723
+ }
1724
+ },
1725
+ {
1726
+ name: "Authentication",
1727
+ check: async () => {
1728
+ try {
1729
+ const user = await authService.validateToken();
1730
+ if (!user) {
1731
+ return {
1732
+ success: false,
1733
+ message: "Not authenticated",
1734
+ details: ['Run "stint login" to authenticate']
1735
+ };
1736
+ }
1737
+ return {
1738
+ success: true,
1739
+ message: `Authenticated as ${user.email}`
1740
+ };
1741
+ } catch {
1742
+ return {
1743
+ success: false,
1744
+ message: "Authentication validation failed",
1745
+ details: ['Run "stint login" to re-authenticate']
1746
+ };
1747
+ }
1748
+ }
1749
+ },
1750
+ {
1751
+ name: "API Connectivity",
1752
+ check: async () => {
1753
+ try {
1754
+ await apiService.ping();
1755
+ return {
1756
+ success: true,
1757
+ message: "API connection successful"
1758
+ };
1759
+ } catch (error) {
1760
+ return {
1761
+ success: false,
1762
+ message: "API connection failed",
1763
+ details: [error.message]
1764
+ };
1765
+ }
1766
+ }
1767
+ },
1768
+ {
1769
+ name: "WebSocket Connectivity",
1770
+ check: async () => {
1771
+ try {
1772
+ await websocketService.connect();
1773
+ const isConnected = websocketService.isConnected();
1774
+ websocketService.disconnect();
1775
+ if (!isConnected) {
1776
+ return {
1777
+ success: false,
1778
+ message: "WebSocket connection failed",
1779
+ details: ["Connection established but not ready"]
1780
+ };
1781
+ }
1782
+ return {
1783
+ success: true,
1784
+ message: "WebSocket connection successful"
1785
+ };
1786
+ } catch (error) {
1787
+ return {
1788
+ success: false,
1789
+ message: "WebSocket connection failed",
1790
+ details: [error.message]
1791
+ };
1792
+ }
1793
+ }
1794
+ }
1795
+ ];
1796
+ console.log(chalk12.blue("\n\u{1F50D} Running environment diagnostics...\n"));
1797
+ for (const check of checks) {
1798
+ spinner.text = `Checking ${check.name.toLowerCase()}...`;
1799
+ try {
1800
+ const result = await check.check();
1801
+ if (result.success) {
1802
+ console.log(`${chalk12.green("\u2713")} ${chalk12.bold(check.name)}: ${result.message}`);
1803
+ } else {
1804
+ hasErrors = true;
1805
+ console.log(`${chalk12.red("\u2716")} ${chalk12.bold(check.name)}: ${result.message}`);
1806
+ if (result.details) {
1807
+ result.details.forEach((detail) => {
1808
+ console.log(chalk12.gray(` ${detail}`));
1809
+ });
1810
+ }
1811
+ }
1812
+ } catch (error) {
1813
+ hasErrors = true;
1814
+ console.log(`${chalk12.red("\u2716")} ${chalk12.bold(check.name)}: Check failed - ${error.message}`);
1815
+ }
1816
+ }
1817
+ spinner.stop();
1818
+ console.log();
1819
+ if (hasErrors) {
1820
+ console.log(chalk12.yellow("Some checks failed. Please address the issues above."));
1821
+ process7.exit(1);
1822
+ } else {
1823
+ console.log(chalk12.green("All checks passed! Your environment is healthy."));
1824
+ }
1825
+ } catch (error) {
1826
+ spinner.fail("Diagnostics failed");
1827
+ logger.error("doctor", "Diagnostics failed", error);
1828
+ console.error(chalk12.red(`
1829
+ \u2716 Error: ${error.message}
1830
+ `));
1831
+ process7.exit(1);
1832
+ }
1833
+ });
1834
+ }
1835
+
1330
1836
  // src/index.ts
1331
- var AGENT_VERSION = "1.1.0";
1837
+ var AGENT_VERSION = "1.2.0";
1332
1838
  var program = new Command();
1333
1839
  program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version(AGENT_VERSION, "-v, --version", "output the current version").addHelpText("after", `
1334
- ${chalk12.bold("Examples:")}
1335
- ${chalk12.cyan("$")} stint login ${chalk12.gray("# Authenticate with Stint")}
1336
- ${chalk12.cyan("$")} stint install ${chalk12.gray("# Install agent to run on startup")}
1337
- ${chalk12.cyan("$")} stint link ${chalk12.gray("# Link current directory to a project")}
1338
- ${chalk12.cyan("$")} stint daemon start ${chalk12.gray("# Start background daemon")}
1339
- ${chalk12.cyan("$")} stint status ${chalk12.gray("# Check status")}
1340
- ${chalk12.cyan("$")} stint commits ${chalk12.gray("# List pending commits")}
1840
+ ${chalk13.bold("Examples:")}
1841
+ ${chalk13.cyan("$")} stint login ${chalk13.gray("# Authenticate with Stint")}
1842
+ ${chalk13.cyan("$")} stint install ${chalk13.gray("# Install agent to run on startup")}
1843
+ ${chalk13.cyan("$")} stint link ${chalk13.gray("# Link current directory to a project")}
1844
+ ${chalk13.cyan("$")} stint daemon start ${chalk13.gray("# Start background daemon")}
1845
+ ${chalk13.cyan("$")} stint status ${chalk13.gray("# Check status")}
1846
+ ${chalk13.cyan("$")} stint commits ${chalk13.gray("# List pending commits")}
1341
1847
 
1342
- ${chalk12.bold("Documentation:")}
1343
- For more information, visit: ${chalk12.blue("https://stint.codes/docs")}
1848
+ ${chalk13.bold("Documentation:")}
1849
+ For more information, visit: ${chalk13.blue("https://stint.codes/docs")}
1344
1850
  `);
1345
1851
  registerLoginCommand(program);
1346
1852
  registerLogoutCommand(program);
@@ -1354,6 +1860,7 @@ registerCommitCommands(program);
1354
1860
  registerInstallCommand(program);
1355
1861
  registerUninstallCommand(program);
1356
1862
  registerUpdateCommand(program);
1863
+ registerDoctorCommand(program);
1357
1864
  program.exitOverride();
1358
1865
  try {
1359
1866
  await program.parseAsync(process.argv);
@@ -1361,7 +1868,7 @@ try {
1361
1868
  const commanderError = error;
1362
1869
  if (commanderError.code !== "commander.help" && commanderError.code !== "commander.version" && commanderError.code !== "commander.helpDisplayed") {
1363
1870
  logger.error("cli", "Command execution failed", error);
1364
- console.error(chalk12.red(`
1871
+ console.error(chalk13.red(`
1365
1872
  \u2716 Error: ${error.message}
1366
1873
  `));
1367
1874
  process.exit(1);