@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
commitQueue,
|
|
4
|
+
getPidFilePath,
|
|
5
|
+
gitService,
|
|
6
|
+
isProcessRunning,
|
|
7
|
+
killProcess,
|
|
8
|
+
projectService,
|
|
9
|
+
spawnDetached,
|
|
10
|
+
validatePidFile
|
|
11
|
+
} from "./chunk-PPODHVVP.js";
|
|
12
|
+
import {
|
|
13
|
+
apiService,
|
|
14
|
+
authService,
|
|
15
|
+
config,
|
|
16
|
+
logger
|
|
17
|
+
} from "./chunk-5DWSNHS6.js";
|
|
18
|
+
|
|
19
|
+
// src/index.ts
|
|
20
|
+
import "dotenv/config";
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import chalk10 from "chalk";
|
|
23
|
+
|
|
24
|
+
// src/commands/login.ts
|
|
25
|
+
import open from "open";
|
|
26
|
+
import ora from "ora";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
import { createServer } from "http";
|
|
29
|
+
import { URL } from "url";
|
|
30
|
+
function escapeHtml(text) {
|
|
31
|
+
if (!text) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
35
|
+
}
|
|
36
|
+
function startCallbackServer(expectedState, timeoutMs = 5 * 60 * 1e3) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const server = createServer();
|
|
39
|
+
let callbackResolve;
|
|
40
|
+
let callbackReject;
|
|
41
|
+
let timeout;
|
|
42
|
+
const callbackPromise = new Promise((resolve2, reject2) => {
|
|
43
|
+
callbackResolve = resolve2;
|
|
44
|
+
callbackReject = reject2;
|
|
45
|
+
});
|
|
46
|
+
server.on("request", (req, res) => {
|
|
47
|
+
if (req.url?.startsWith("/auth/callback")) {
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
50
|
+
const token = url.searchParams.get("token");
|
|
51
|
+
const state = url.searchParams.get("state");
|
|
52
|
+
const error = url.searchParams.get("error");
|
|
53
|
+
if (error) {
|
|
54
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
55
|
+
const escapedError = escapeHtml(errorDescription);
|
|
56
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
57
|
+
res.end(`
|
|
58
|
+
<html>
|
|
59
|
+
<head><title>Authentication Failed</title></head>
|
|
60
|
+
<body>
|
|
61
|
+
<h1>Authentication Failed</h1>
|
|
62
|
+
<p>${escapedError}</p>
|
|
63
|
+
<p>You can close this window.</p>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
66
|
+
`);
|
|
67
|
+
callbackReject(new Error(`OAuth error: ${errorDescription}`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (state !== expectedState) {
|
|
71
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
72
|
+
res.end(`
|
|
73
|
+
<html>
|
|
74
|
+
<head><title>Authentication Failed</title></head>
|
|
75
|
+
<body>
|
|
76
|
+
<h1>Authentication Failed</h1>
|
|
77
|
+
<p>Invalid state parameter. Security validation failed.</p>
|
|
78
|
+
<p>You can close this window.</p>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
`);
|
|
82
|
+
callbackReject(new Error("State parameter mismatch"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!token) {
|
|
86
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
87
|
+
res.end(`
|
|
88
|
+
<html>
|
|
89
|
+
<head><title>Authentication Failed</title></head>
|
|
90
|
+
<body>
|
|
91
|
+
<h1>Authentication Failed</h1>
|
|
92
|
+
<p>No token provided in callback.</p>
|
|
93
|
+
<p>You can close this window.</p>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`);
|
|
97
|
+
callbackReject(new Error("No token in callback"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
101
|
+
res.end(`
|
|
102
|
+
<html>
|
|
103
|
+
<head><title>Authentication Successful</title></head>
|
|
104
|
+
<body>
|
|
105
|
+
<h1>Authentication Successful!</h1>
|
|
106
|
+
<p>You have been successfully authenticated. You can close this window.</p>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`);
|
|
110
|
+
callbackResolve(token);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
113
|
+
res.end(`
|
|
114
|
+
<html>
|
|
115
|
+
<head><title>Error</title></head>
|
|
116
|
+
<body>
|
|
117
|
+
<h1>Error</h1>
|
|
118
|
+
<p>An error occurred processing the callback.</p>
|
|
119
|
+
<p>You can close this window.</p>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
|
122
|
+
`);
|
|
123
|
+
callbackReject(error);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
127
|
+
res.end("Not Found");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
server.on("error", (error) => {
|
|
131
|
+
if (error.code === "EADDRINUSE") {
|
|
132
|
+
reject(new Error("Port is already in use"));
|
|
133
|
+
} else {
|
|
134
|
+
reject(error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
server.listen(0, "127.0.0.1", () => {
|
|
138
|
+
const address = server.address();
|
|
139
|
+
const port = address.port;
|
|
140
|
+
timeout = setTimeout(() => {
|
|
141
|
+
server.close();
|
|
142
|
+
callbackReject(new Error("Authentication timeout: No callback received within 5 minutes"));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
callbackPromise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout)).finally(() => {
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
server.close();
|
|
147
|
+
}, 100);
|
|
148
|
+
});
|
|
149
|
+
resolve({
|
|
150
|
+
server,
|
|
151
|
+
port,
|
|
152
|
+
promise: callbackPromise
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function registerLoginCommand(program2) {
|
|
158
|
+
program2.command("login").description("Authenticate with Stint").action(async () => {
|
|
159
|
+
const spinner = ora("Starting authentication server...").start();
|
|
160
|
+
try {
|
|
161
|
+
const state = Math.random().toString(36).substring(7);
|
|
162
|
+
const machineId = authService.getMachineId();
|
|
163
|
+
const machineName = authService.getMachineName();
|
|
164
|
+
spinner.text = "Starting callback server...";
|
|
165
|
+
const { port, promise: callbackPromise } = await startCallbackServer(state);
|
|
166
|
+
const authUrl = new URL(`${config.getApiUrl()}/auth/agent`);
|
|
167
|
+
authUrl.searchParams.set("state", state);
|
|
168
|
+
authUrl.searchParams.set("machine_id", machineId);
|
|
169
|
+
authUrl.searchParams.set("machine_name", machineName);
|
|
170
|
+
authUrl.searchParams.set("redirect_uri", `http://localhost:${port}/auth/callback`);
|
|
171
|
+
spinner.text = "Opening browser for authentication...";
|
|
172
|
+
await open(authUrl.toString());
|
|
173
|
+
spinner.text = "Waiting for authentication...";
|
|
174
|
+
logger.info("login", `Login initiated, callback server listening on port ${port}`);
|
|
175
|
+
const token = await callbackPromise;
|
|
176
|
+
spinner.text = "Completing authentication...";
|
|
177
|
+
await completeLogin(token);
|
|
178
|
+
spinner.stop();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
spinner.fail("Authentication failed");
|
|
181
|
+
logger.error("login", "Login failed", error);
|
|
182
|
+
console.error(chalk.red(`
|
|
183
|
+
\u2716 Error: ${error.message}`));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function completeLogin(token) {
|
|
189
|
+
const spinner = ora("Saving authentication token...").start();
|
|
190
|
+
try {
|
|
191
|
+
await authService.saveToken(token);
|
|
192
|
+
spinner.text = "Validating token...";
|
|
193
|
+
const user = await authService.validateToken();
|
|
194
|
+
if (!user) {
|
|
195
|
+
throw new Error("Token validation failed");
|
|
196
|
+
}
|
|
197
|
+
spinner.succeed("Authentication successful!");
|
|
198
|
+
console.log(chalk.green(`
|
|
199
|
+
\u2713 Logged in as ${chalk.bold(user.email)}`));
|
|
200
|
+
console.log(chalk.gray(`Machine: ${authService.getMachineName()} (${authService.getMachineId()})
|
|
201
|
+
`));
|
|
202
|
+
logger.success("login", `Logged in as ${user.email}`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
spinner.fail("Authentication failed");
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/commands/logout.ts
|
|
210
|
+
import ora2 from "ora";
|
|
211
|
+
import chalk2 from "chalk";
|
|
212
|
+
function registerLogoutCommand(program2) {
|
|
213
|
+
program2.command("logout").description("Log out and remove stored credentials").action(async () => {
|
|
214
|
+
const spinner = ora2("Logging out...").start();
|
|
215
|
+
try {
|
|
216
|
+
const token = await authService.getToken();
|
|
217
|
+
if (!token) {
|
|
218
|
+
spinner.info("Not currently logged in");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await apiService.disconnect();
|
|
223
|
+
} catch {
|
|
224
|
+
logger.warn("logout", "Failed to disconnect session, continuing with logout");
|
|
225
|
+
}
|
|
226
|
+
await authService.clearToken();
|
|
227
|
+
spinner.succeed("Logged out successfully");
|
|
228
|
+
console.log(chalk2.gray("\nYour credentials have been removed from this machine.\n"));
|
|
229
|
+
logger.success("logout", "User logged out");
|
|
230
|
+
} catch (error) {
|
|
231
|
+
spinner.fail("Logout failed");
|
|
232
|
+
logger.error("logout", "Logout failed", error);
|
|
233
|
+
console.error(chalk2.red(`
|
|
234
|
+
\u2716 Error: ${error.message}`));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/commands/whoami.ts
|
|
241
|
+
import ora3 from "ora";
|
|
242
|
+
import chalk3 from "chalk";
|
|
243
|
+
function registerWhoamiCommand(program2) {
|
|
244
|
+
program2.command("whoami").description("Show current user and machine information").action(async () => {
|
|
245
|
+
const spinner = ora3("Checking authentication...").start();
|
|
246
|
+
try {
|
|
247
|
+
const token = await authService.getToken();
|
|
248
|
+
if (!token) {
|
|
249
|
+
spinner.info("Not logged in");
|
|
250
|
+
console.log(chalk3.yellow("\n\u26A0 You are not logged in."));
|
|
251
|
+
console.log(chalk3.gray('Run "stint login" to authenticate.\n'));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
spinner.text = "Validating credentials...";
|
|
255
|
+
const user = await authService.validateToken();
|
|
256
|
+
if (!user) {
|
|
257
|
+
spinner.fail("Authentication invalid");
|
|
258
|
+
console.log(chalk3.red("\n\u2716 Your authentication token is invalid or expired."));
|
|
259
|
+
console.log(chalk3.gray('Run "stint login" to re-authenticate.\n'));
|
|
260
|
+
await authService.clearToken();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
spinner.succeed("Authenticated");
|
|
264
|
+
console.log(chalk3.blue("\n\u{1F464} User Information:"));
|
|
265
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
266
|
+
console.log(`${chalk3.bold("Name:")} ${user.name}`);
|
|
267
|
+
console.log(`${chalk3.bold("Email:")} ${user.email}`);
|
|
268
|
+
console.log(`${chalk3.bold("User ID:")} ${user.id}`);
|
|
269
|
+
console.log(chalk3.blue("\n\u{1F4BB} Machine Information:"));
|
|
270
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
271
|
+
console.log(`${chalk3.bold("Name:")} ${authService.getMachineName()}`);
|
|
272
|
+
console.log(`${chalk3.bold("ID:")} ${authService.getMachineId()}`);
|
|
273
|
+
console.log();
|
|
274
|
+
logger.info("whoami", `User: ${user.email}, Machine: ${authService.getMachineName()}`);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
spinner.fail("Failed to retrieve information");
|
|
277
|
+
logger.error("whoami", "Command failed", error);
|
|
278
|
+
console.error(chalk3.red(`
|
|
279
|
+
\u2716 Error: ${error.message}`));
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/commands/link.ts
|
|
286
|
+
import { select } from "@inquirer/prompts";
|
|
287
|
+
import ora4 from "ora";
|
|
288
|
+
import chalk4 from "chalk";
|
|
289
|
+
import process2 from "process";
|
|
290
|
+
function registerLinkCommand(program2) {
|
|
291
|
+
program2.command("link").description("Link current directory to a Stint project").action(async () => {
|
|
292
|
+
const spinner = ora4("Checking directory...").start();
|
|
293
|
+
try {
|
|
294
|
+
const cwd = process2.cwd();
|
|
295
|
+
const existingLink = projectService.getLinkedProject(cwd);
|
|
296
|
+
if (existingLink) {
|
|
297
|
+
spinner.warn("Directory already linked");
|
|
298
|
+
console.log(chalk4.yellow(`
|
|
299
|
+
\u26A0 This directory is already linked to project ${existingLink.projectId}`));
|
|
300
|
+
console.log(chalk4.gray('Run "stint unlink" first if you want to link to a different project.\n'));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
spinner.text = "Verifying git repository...";
|
|
304
|
+
const isRepo = await gitService.isRepo(cwd);
|
|
305
|
+
if (!isRepo) {
|
|
306
|
+
spinner.fail("Not a git repository");
|
|
307
|
+
console.log(chalk4.red("\n\u2716 This directory is not a git repository."));
|
|
308
|
+
console.log(chalk4.gray("Please run this command in a git repository.\n"));
|
|
309
|
+
process2.exit(1);
|
|
310
|
+
}
|
|
311
|
+
spinner.text = "Fetching projects...";
|
|
312
|
+
const projects = await apiService.getLinkedProjects();
|
|
313
|
+
if (projects.length === 0) {
|
|
314
|
+
spinner.info("No projects available");
|
|
315
|
+
console.log(chalk4.yellow("\n\u26A0 No projects found in your Stint account."));
|
|
316
|
+
console.log(chalk4.gray("Create a project at https://stint.codes first.\n"));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
spinner.succeed("Ready to link");
|
|
320
|
+
const selectedProjectId = await select({
|
|
321
|
+
message: "Select a project to link:",
|
|
322
|
+
choices: projects.map((project) => ({
|
|
323
|
+
name: `${project.name}${project.description ? ` - ${project.description}` : ""}`,
|
|
324
|
+
value: project.id,
|
|
325
|
+
description: `ID: ${project.id}`
|
|
326
|
+
}))
|
|
327
|
+
});
|
|
328
|
+
const linkSpinner = ora4("Linking project...").start();
|
|
329
|
+
await projectService.linkProject(cwd, selectedProjectId);
|
|
330
|
+
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
|
331
|
+
linkSpinner.succeed("Project linked successfully!");
|
|
332
|
+
console.log(chalk4.green(`
|
|
333
|
+
\u2713 Linked to ${chalk4.bold(selectedProject?.name || selectedProjectId)}`));
|
|
334
|
+
console.log(chalk4.gray(`Directory: ${cwd}`));
|
|
335
|
+
console.log(chalk4.gray(`Project ID: ${selectedProjectId}
|
|
336
|
+
`));
|
|
337
|
+
logger.success("link", `Linked ${cwd} to project ${selectedProjectId}`);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
spinner.fail("Failed to link project");
|
|
340
|
+
logger.error("link", "Link command failed", error);
|
|
341
|
+
console.error(chalk4.red(`
|
|
342
|
+
\u2716 Error: ${error.message}
|
|
343
|
+
`));
|
|
344
|
+
process2.exit(1);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/commands/unlink.ts
|
|
350
|
+
import { confirm } from "@inquirer/prompts";
|
|
351
|
+
import ora5 from "ora";
|
|
352
|
+
import chalk5 from "chalk";
|
|
353
|
+
import process3 from "process";
|
|
354
|
+
function registerUnlinkCommand(program2) {
|
|
355
|
+
program2.command("unlink").description("Remove link from current directory").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
|
|
356
|
+
const spinner = ora5("Checking directory...").start();
|
|
357
|
+
try {
|
|
358
|
+
const cwd = process3.cwd();
|
|
359
|
+
const linkedProject = projectService.getLinkedProject(cwd);
|
|
360
|
+
if (!linkedProject) {
|
|
361
|
+
spinner.info("Not linked");
|
|
362
|
+
console.log(chalk5.yellow("\n\u26A0 This directory is not linked to any project.\n"));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
spinner.stop();
|
|
366
|
+
console.log(chalk5.blue("\n\u{1F4CB} Current Link:"));
|
|
367
|
+
console.log(chalk5.gray("\u2500".repeat(50)));
|
|
368
|
+
console.log(`${chalk5.bold("Project ID:")} ${linkedProject.projectId}`);
|
|
369
|
+
console.log(`${chalk5.bold("Linked At:")} ${new Date(linkedProject.linkedAt).toLocaleString()}`);
|
|
370
|
+
console.log();
|
|
371
|
+
if (!options.force) {
|
|
372
|
+
const shouldUnlink = await confirm({
|
|
373
|
+
message: "Are you sure you want to unlink this directory?",
|
|
374
|
+
default: false
|
|
375
|
+
});
|
|
376
|
+
if (!shouldUnlink) {
|
|
377
|
+
console.log(chalk5.gray("Cancelled.\n"));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const unlinkSpinner = ora5("Unlinking...").start();
|
|
382
|
+
await projectService.unlinkProject(cwd);
|
|
383
|
+
unlinkSpinner.succeed("Unlinked successfully");
|
|
384
|
+
console.log(chalk5.gray(`
|
|
385
|
+
Directory ${cwd} is no longer linked.
|
|
386
|
+
`));
|
|
387
|
+
logger.success("unlink", `Unlinked ${cwd}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
spinner.fail("Failed to unlink");
|
|
390
|
+
logger.error("unlink", "Unlink command failed", error);
|
|
391
|
+
console.error(chalk5.red(`
|
|
392
|
+
\u2716 Error: ${error.message}
|
|
393
|
+
`));
|
|
394
|
+
process3.exit(1);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/commands/status.ts
|
|
400
|
+
import ora6 from "ora";
|
|
401
|
+
import chalk6 from "chalk";
|
|
402
|
+
import process4 from "process";
|
|
403
|
+
function registerStatusCommand(program2) {
|
|
404
|
+
program2.command("status").description("Show linked project and connection status").action(async () => {
|
|
405
|
+
const spinner = ora6("Gathering status...").start();
|
|
406
|
+
try {
|
|
407
|
+
const cwd = process4.cwd();
|
|
408
|
+
const linkedProject = projectService.getLinkedProject(cwd);
|
|
409
|
+
const user = await authService.validateToken();
|
|
410
|
+
spinner.stop();
|
|
411
|
+
console.log(chalk6.blue("\n\u{1F4E6} Project Status:"));
|
|
412
|
+
console.log(chalk6.gray("\u2500".repeat(50)));
|
|
413
|
+
if (linkedProject) {
|
|
414
|
+
console.log(`${chalk6.bold("Status:")} ${chalk6.green("\u2713 Linked")}`);
|
|
415
|
+
console.log(`${chalk6.bold("Project ID:")} ${linkedProject.projectId}`);
|
|
416
|
+
console.log(`${chalk6.bold("Linked At:")} ${new Date(linkedProject.linkedAt).toLocaleString()}`);
|
|
417
|
+
} else {
|
|
418
|
+
console.log(`${chalk6.bold("Status:")} ${chalk6.yellow("Not linked")}`);
|
|
419
|
+
console.log(chalk6.gray('Run "stint link" to link this directory to a project.'));
|
|
420
|
+
}
|
|
421
|
+
console.log(chalk6.blue("\n\u{1F4C2} Git Repository:"));
|
|
422
|
+
console.log(chalk6.gray("\u2500".repeat(50)));
|
|
423
|
+
const isRepo = await gitService.isRepo(cwd);
|
|
424
|
+
if (isRepo) {
|
|
425
|
+
try {
|
|
426
|
+
const repoInfo = await gitService.getRepoInfo(cwd);
|
|
427
|
+
console.log(`${chalk6.bold("Branch:")} ${chalk6.cyan(repoInfo.currentBranch)}`);
|
|
428
|
+
console.log(`${chalk6.bold("Remote:")} ${repoInfo.remoteUrl || chalk6.gray("None")}`);
|
|
429
|
+
console.log(`${chalk6.bold("Last Commit:")} ${repoInfo.lastCommitSha.substring(0, 7)} - ${repoInfo.lastCommitMessage}`);
|
|
430
|
+
console.log(`${chalk6.bold("Commit Date:")} ${new Date(repoInfo.lastCommitDate).toLocaleString()}`);
|
|
431
|
+
const { staged, unstaged, untracked, ahead, behind } = repoInfo.status;
|
|
432
|
+
const totalChanges = staged.length + unstaged.length + untracked.length;
|
|
433
|
+
if (totalChanges > 0) {
|
|
434
|
+
console.log(`${chalk6.bold("Changes:")} ${chalk6.yellow(`${totalChanges} file(s)`)}`);
|
|
435
|
+
if (staged.length > 0) console.log(` ${chalk6.green("Staged:")} ${staged.length}`);
|
|
436
|
+
if (unstaged.length > 0) console.log(` ${chalk6.yellow("Unstaged:")} ${unstaged.length}`);
|
|
437
|
+
if (untracked.length > 0) console.log(` ${chalk6.gray("Untracked:")} ${untracked.length}`);
|
|
438
|
+
} else {
|
|
439
|
+
console.log(`${chalk6.bold("Changes:")} ${chalk6.green("Clean working tree")}`);
|
|
440
|
+
}
|
|
441
|
+
if (ahead > 0 || behind > 0) {
|
|
442
|
+
console.log(`${chalk6.bold("Sync Status:")} ${ahead > 0 ? chalk6.yellow(`\u2191${ahead}`) : ""} ${behind > 0 ? chalk6.yellow(`\u2193${behind}`) : ""}`);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
console.log(chalk6.red("Error reading repository information"));
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
console.log(chalk6.yellow("Not a git repository"));
|
|
449
|
+
}
|
|
450
|
+
console.log(chalk6.blue("\n\u{1F510} Authentication:"));
|
|
451
|
+
console.log(chalk6.gray("\u2500".repeat(50)));
|
|
452
|
+
if (user) {
|
|
453
|
+
console.log(`${chalk6.bold("Status:")} ${chalk6.green("\u2713 Authenticated")}`);
|
|
454
|
+
console.log(`${chalk6.bold("User:")} ${user.name} (${user.email})`);
|
|
455
|
+
console.log(`${chalk6.bold("Machine:")} ${authService.getMachineName()}`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(`${chalk6.bold("Status:")} ${chalk6.yellow("Not logged in")}`);
|
|
458
|
+
console.log(chalk6.gray('Run "stint login" to authenticate.'));
|
|
459
|
+
}
|
|
460
|
+
console.log(chalk6.blue("\n\u2699\uFE0F Daemon:"));
|
|
461
|
+
console.log(chalk6.gray("\u2500".repeat(50)));
|
|
462
|
+
console.log(`${chalk6.bold("Status:")} ${chalk6.gray("Not running (Phase 3)")}`);
|
|
463
|
+
console.log();
|
|
464
|
+
logger.info("status", "Status command executed");
|
|
465
|
+
} catch (error) {
|
|
466
|
+
spinner.fail("Failed to get status");
|
|
467
|
+
logger.error("status", "Status command failed", error);
|
|
468
|
+
console.error(chalk6.red(`
|
|
469
|
+
\u2716 Error: ${error.message}
|
|
470
|
+
`));
|
|
471
|
+
process4.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/commands/sync.ts
|
|
477
|
+
import ora7 from "ora";
|
|
478
|
+
import chalk7 from "chalk";
|
|
479
|
+
import process5 from "process";
|
|
480
|
+
function registerSyncCommand(program2) {
|
|
481
|
+
program2.command("sync").description("Manually sync repository information to server").action(async () => {
|
|
482
|
+
const spinner = ora7("Checking directory...").start();
|
|
483
|
+
try {
|
|
484
|
+
const cwd = process5.cwd();
|
|
485
|
+
const linkedProject = projectService.getLinkedProject(cwd);
|
|
486
|
+
if (!linkedProject) {
|
|
487
|
+
spinner.fail("Not linked");
|
|
488
|
+
console.log(chalk7.yellow("\n\u26A0 This directory is not linked to any project."));
|
|
489
|
+
console.log(chalk7.gray('Run "stint link" first to link this directory.\n'));
|
|
490
|
+
process5.exit(1);
|
|
491
|
+
}
|
|
492
|
+
spinner.text = "Gathering repository information...";
|
|
493
|
+
const repoInfo = await gitService.getRepoInfo(cwd);
|
|
494
|
+
spinner.text = "Syncing with server...";
|
|
495
|
+
await apiService.syncProject(linkedProject.projectId, repoInfo);
|
|
496
|
+
spinner.succeed("Sync completed successfully!");
|
|
497
|
+
console.log(chalk7.green("\n\u2713 Repository information synced"));
|
|
498
|
+
console.log(chalk7.gray("\u2500".repeat(50)));
|
|
499
|
+
console.log(`${chalk7.bold("Project ID:")} ${linkedProject.projectId}`);
|
|
500
|
+
console.log(`${chalk7.bold("Branch:")} ${repoInfo.currentBranch}`);
|
|
501
|
+
console.log(`${chalk7.bold("Commit:")} ${repoInfo.lastCommitSha.substring(0, 7)} - ${repoInfo.lastCommitMessage}`);
|
|
502
|
+
console.log(`${chalk7.bold("Remote:")} ${repoInfo.remoteUrl || chalk7.gray("None")}`);
|
|
503
|
+
console.log();
|
|
504
|
+
logger.success("sync", `Synced ${cwd} to project ${linkedProject.projectId}`);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
spinner.fail("Sync failed");
|
|
507
|
+
logger.error("sync", "Sync command failed", error);
|
|
508
|
+
console.error(chalk7.red(`
|
|
509
|
+
\u2716 Error: ${error.message}
|
|
510
|
+
`));
|
|
511
|
+
process5.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/commands/daemon.ts
|
|
517
|
+
import ora8 from "ora";
|
|
518
|
+
import chalk8 from "chalk";
|
|
519
|
+
import fs from "fs";
|
|
520
|
+
import path from "path";
|
|
521
|
+
import os from "os";
|
|
522
|
+
import { fileURLToPath } from "url";
|
|
523
|
+
import { dirname } from "path";
|
|
524
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
525
|
+
var __dirname = dirname(__filename);
|
|
526
|
+
function registerDaemonCommands(program2) {
|
|
527
|
+
const daemon = program2.command("daemon").description("Manage the Stint daemon");
|
|
528
|
+
daemon.command("start").description("Start the daemon in the background").action(async () => {
|
|
529
|
+
const spinner = ora8("Starting daemon...").start();
|
|
530
|
+
try {
|
|
531
|
+
const { valid, pid } = validatePidFile();
|
|
532
|
+
if (valid && pid) {
|
|
533
|
+
spinner.info("Daemon already running");
|
|
534
|
+
console.log(chalk8.yellow(`
|
|
535
|
+
\u26A0 Daemon is already running (PID: ${pid})
|
|
536
|
+
`));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
spinner.text = "Validating authentication...";
|
|
540
|
+
const user = await authService.validateToken();
|
|
541
|
+
if (!user) {
|
|
542
|
+
spinner.fail("Not authenticated");
|
|
543
|
+
console.log(chalk8.red("\n\u2716 You must be logged in to start the daemon."));
|
|
544
|
+
console.log(chalk8.gray('Run "stint login" first.\n'));
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
const runnerPath = path.join(__dirname, "daemon", "runner.js");
|
|
548
|
+
if (!fs.existsSync(runnerPath)) {
|
|
549
|
+
throw new Error(`Daemon runner not found at ${runnerPath}`);
|
|
550
|
+
}
|
|
551
|
+
spinner.text = "Spawning daemon process...";
|
|
552
|
+
const daemonPid = spawnDetached("node", [runnerPath]);
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
554
|
+
if (!isProcessRunning(daemonPid)) {
|
|
555
|
+
throw new Error("Daemon failed to start");
|
|
556
|
+
}
|
|
557
|
+
spinner.succeed("Daemon started successfully!");
|
|
558
|
+
console.log(chalk8.green(`
|
|
559
|
+
\u2713 Daemon is running in the background`));
|
|
560
|
+
console.log(chalk8.gray(`PID: ${daemonPid}`));
|
|
561
|
+
console.log(chalk8.gray(`Logs: ${path.join(os.homedir(), ".config", "stint", "logs", "daemon.log")}
|
|
562
|
+
`));
|
|
563
|
+
logger.success("daemon", `Daemon started with PID ${daemonPid}`);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
spinner.fail("Failed to start daemon");
|
|
566
|
+
logger.error("daemon", "Start command failed", error);
|
|
567
|
+
console.error(chalk8.red(`
|
|
568
|
+
\u2716 Error: ${error.message}
|
|
569
|
+
`));
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
daemon.command("stop").description("Stop the running daemon").action(async () => {
|
|
574
|
+
const spinner = ora8("Stopping daemon...").start();
|
|
575
|
+
try {
|
|
576
|
+
const { valid, pid } = validatePidFile();
|
|
577
|
+
if (!valid || !pid) {
|
|
578
|
+
spinner.info("Daemon not running");
|
|
579
|
+
console.log(chalk8.yellow("\n\u26A0 Daemon is not running.\n"));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
spinner.text = `Sending shutdown signal to process ${pid}...`;
|
|
583
|
+
killProcess(pid, "SIGTERM");
|
|
584
|
+
spinner.text = "Waiting for daemon to shutdown...";
|
|
585
|
+
let attempts = 0;
|
|
586
|
+
const maxAttempts = 10;
|
|
587
|
+
while (attempts < maxAttempts) {
|
|
588
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
589
|
+
if (!isProcessRunning(pid)) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
attempts++;
|
|
593
|
+
}
|
|
594
|
+
if (isProcessRunning(pid)) {
|
|
595
|
+
spinner.warn("Daemon did not stop gracefully, forcing shutdown...");
|
|
596
|
+
killProcess(pid, "SIGKILL");
|
|
597
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
598
|
+
}
|
|
599
|
+
spinner.succeed("Daemon stopped successfully");
|
|
600
|
+
console.log(chalk8.gray("\nDaemon has been stopped.\n"));
|
|
601
|
+
logger.success("daemon", "Daemon stopped");
|
|
602
|
+
} catch (error) {
|
|
603
|
+
spinner.fail("Failed to stop daemon");
|
|
604
|
+
logger.error("daemon", "Stop command failed", error);
|
|
605
|
+
console.error(chalk8.red(`
|
|
606
|
+
\u2716 Error: ${error.message}
|
|
607
|
+
`));
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
daemon.command("status").description("Check if the daemon is running").action(async () => {
|
|
612
|
+
const spinner = ora8("Checking daemon status...").start();
|
|
613
|
+
try {
|
|
614
|
+
const { valid, pid } = validatePidFile();
|
|
615
|
+
spinner.stop();
|
|
616
|
+
console.log(chalk8.blue("\n\u2699\uFE0F Daemon Status:"));
|
|
617
|
+
console.log(chalk8.gray("\u2500".repeat(50)));
|
|
618
|
+
if (valid && pid) {
|
|
619
|
+
console.log(`${chalk8.bold("Status:")} ${chalk8.green("\u2713 Running")}`);
|
|
620
|
+
console.log(`${chalk8.bold("PID:")} ${pid}`);
|
|
621
|
+
console.log(`${chalk8.bold("PID File:")} ${getPidFilePath()}`);
|
|
622
|
+
console.log(`${chalk8.bold("Logs:")} ${path.join(os.homedir(), ".config", "stint", "logs", "daemon.log")}`);
|
|
623
|
+
} else {
|
|
624
|
+
console.log(`${chalk8.bold("Status:")} ${chalk8.yellow("Not running")}`);
|
|
625
|
+
console.log(chalk8.gray('Run "stint daemon start" to start the daemon.'));
|
|
626
|
+
}
|
|
627
|
+
console.log();
|
|
628
|
+
logger.info("daemon", `Status check: ${valid ? "running" : "not running"}`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
spinner.fail("Failed to check status");
|
|
631
|
+
logger.error("daemon", "Status command failed", error);
|
|
632
|
+
console.error(chalk8.red(`
|
|
633
|
+
\u2716 Error: ${error.message}
|
|
634
|
+
`));
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
daemon.command("logs").description("Tail daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").action(async (options) => {
|
|
639
|
+
try {
|
|
640
|
+
const logFile = path.join(os.homedir(), ".config", "stint", "logs", "daemon.log");
|
|
641
|
+
if (!fs.existsSync(logFile)) {
|
|
642
|
+
console.log(chalk8.yellow("\n\u26A0 No daemon logs found."));
|
|
643
|
+
console.log(chalk8.gray("The daemon has not been started yet.\n"));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const lines = parseInt(options.lines, 10);
|
|
647
|
+
const content = fs.readFileSync(logFile, "utf8");
|
|
648
|
+
const logLines = content.split("\n").filter((line) => line.trim());
|
|
649
|
+
const lastLines = logLines.slice(-lines);
|
|
650
|
+
console.log(chalk8.blue(`
|
|
651
|
+
\u{1F4CB} Last ${lastLines.length} lines of daemon logs:
|
|
652
|
+
`));
|
|
653
|
+
console.log(chalk8.gray("\u2500".repeat(80)));
|
|
654
|
+
lastLines.forEach((line) => console.log(line));
|
|
655
|
+
console.log(chalk8.gray("\u2500".repeat(80)));
|
|
656
|
+
console.log(chalk8.gray(`
|
|
657
|
+
Log file: ${logFile}`));
|
|
658
|
+
console.log(chalk8.gray('Use "tail -f" to follow logs in real-time.\n'));
|
|
659
|
+
logger.info("daemon", "Logs command executed");
|
|
660
|
+
} catch (error) {
|
|
661
|
+
logger.error("daemon", "Logs command failed", error);
|
|
662
|
+
console.error(chalk8.red(`
|
|
663
|
+
\u2716 Error: ${error.message}
|
|
664
|
+
`));
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
daemon.command("restart").description("Restart the daemon").action(async () => {
|
|
669
|
+
console.log(chalk8.blue("\u{1F504} Restarting daemon...\n"));
|
|
670
|
+
const { valid, pid } = validatePidFile();
|
|
671
|
+
if (valid && pid) {
|
|
672
|
+
const stopSpinner = ora8("Stopping daemon...").start();
|
|
673
|
+
try {
|
|
674
|
+
killProcess(pid, "SIGTERM");
|
|
675
|
+
let attempts = 0;
|
|
676
|
+
while (attempts < 10 && isProcessRunning(pid)) {
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
678
|
+
attempts++;
|
|
679
|
+
}
|
|
680
|
+
if (isProcessRunning(pid)) {
|
|
681
|
+
killProcess(pid, "SIGKILL");
|
|
682
|
+
}
|
|
683
|
+
stopSpinner.succeed("Daemon stopped");
|
|
684
|
+
} catch (error) {
|
|
685
|
+
stopSpinner.fail("Failed to stop daemon");
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
690
|
+
const startSpinner = ora8("Starting daemon...").start();
|
|
691
|
+
try {
|
|
692
|
+
const user = await authService.validateToken();
|
|
693
|
+
if (!user) {
|
|
694
|
+
throw new Error("Not authenticated");
|
|
695
|
+
}
|
|
696
|
+
const runnerPath = path.join(__dirname, "daemon", "runner.js");
|
|
697
|
+
const daemonPid = spawnDetached("node", [runnerPath]);
|
|
698
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
699
|
+
if (!isProcessRunning(daemonPid)) {
|
|
700
|
+
throw new Error("Daemon failed to start");
|
|
701
|
+
}
|
|
702
|
+
startSpinner.succeed("Daemon started");
|
|
703
|
+
console.log(chalk8.green(`
|
|
704
|
+
\u2713 Daemon restarted successfully (PID: ${daemonPid})
|
|
705
|
+
`));
|
|
706
|
+
logger.success("daemon", `Daemon restarted with PID ${daemonPid}`);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
startSpinner.fail("Failed to start daemon");
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/commands/commit.ts
|
|
715
|
+
import ora9 from "ora";
|
|
716
|
+
import chalk9 from "chalk";
|
|
717
|
+
import process6 from "process";
|
|
718
|
+
function registerCommitCommands(program2) {
|
|
719
|
+
program2.command("commits").description("List pending commits for the current project").action(async () => {
|
|
720
|
+
const spinner = ora9("Loading pending commits...").start();
|
|
721
|
+
try {
|
|
722
|
+
const cwd = process6.cwd();
|
|
723
|
+
const linkedProject = projectService.getLinkedProject(cwd);
|
|
724
|
+
if (!linkedProject) {
|
|
725
|
+
spinner.fail("Not linked");
|
|
726
|
+
console.log(chalk9.yellow("\n\u26A0 This directory is not linked to any project."));
|
|
727
|
+
console.log(chalk9.gray('Run "stint link" first to link this directory.\n'));
|
|
728
|
+
process6.exit(1);
|
|
729
|
+
}
|
|
730
|
+
spinner.text = "Fetching pending commits...";
|
|
731
|
+
const commits = await apiService.getPendingCommits(linkedProject.projectId);
|
|
732
|
+
spinner.stop();
|
|
733
|
+
if (commits.length === 0) {
|
|
734
|
+
console.log(chalk9.blue("\n\u{1F4CB} No pending commits\n"));
|
|
735
|
+
console.log(chalk9.gray("All commits have been executed.\n"));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
console.log(chalk9.blue(`
|
|
739
|
+
\u{1F4CB} Pending Commits (${commits.length}):
|
|
740
|
+
`));
|
|
741
|
+
commits.forEach((commit) => {
|
|
742
|
+
const shortId = commit.id.substring(0, 7);
|
|
743
|
+
const date = new Date(commit.createdAt);
|
|
744
|
+
const timeAgo = getTimeAgo(date);
|
|
745
|
+
console.log(` ${chalk9.cyan(shortId)} ${commit.message.padEnd(40)} ${chalk9.gray(timeAgo)}`);
|
|
746
|
+
if (commit.files && commit.files.length > 0) {
|
|
747
|
+
console.log(chalk9.gray(` Files: ${commit.files.join(", ")}`));
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
console.log(chalk9.gray('\nRun "stint commit <id>" to execute a specific commit.\n'));
|
|
751
|
+
logger.info("commits", `Listed ${commits.length} pending commits`);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
spinner.fail("Failed to fetch commits");
|
|
754
|
+
logger.error("commits", "Failed to fetch commits", error);
|
|
755
|
+
console.error(chalk9.red(`
|
|
756
|
+
\u2716 Error: ${error.message}
|
|
757
|
+
`));
|
|
758
|
+
process6.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
program2.command("commit <id>").description("Execute a specific pending commit").action(async (id) => {
|
|
762
|
+
const spinner = ora9("Executing commit...").start();
|
|
763
|
+
try {
|
|
764
|
+
const cwd = process6.cwd();
|
|
765
|
+
const linkedProject = projectService.getLinkedProject(cwd);
|
|
766
|
+
if (!linkedProject) {
|
|
767
|
+
spinner.fail("Not linked");
|
|
768
|
+
console.log(chalk9.yellow("\n\u26A0 This directory is not linked to any project."));
|
|
769
|
+
console.log(chalk9.gray('Run "stint link" first to link this directory.\n'));
|
|
770
|
+
process6.exit(1);
|
|
771
|
+
}
|
|
772
|
+
spinner.text = "Fetching commit details...";
|
|
773
|
+
const commits = await apiService.getPendingCommits(linkedProject.projectId);
|
|
774
|
+
const commit = commits.find((c) => c.id.startsWith(id));
|
|
775
|
+
if (!commit) {
|
|
776
|
+
spinner.fail("Commit not found");
|
|
777
|
+
console.log(chalk9.red(`
|
|
778
|
+
\u2716 Commit ${id} not found in pending commits.
|
|
779
|
+
`));
|
|
780
|
+
console.log(chalk9.gray('Run "stint commits" to see available commits.\n'));
|
|
781
|
+
process6.exit(1);
|
|
782
|
+
}
|
|
783
|
+
spinner.text = `Executing commit: ${commit.message}`;
|
|
784
|
+
const project = {
|
|
785
|
+
id: linkedProject.projectId,
|
|
786
|
+
name: "Current Project",
|
|
787
|
+
// We don't have the name, but it's not critical
|
|
788
|
+
createdAt: "",
|
|
789
|
+
updatedAt: ""
|
|
790
|
+
};
|
|
791
|
+
const sha = await commitQueue.executeCommit(commit, project);
|
|
792
|
+
spinner.succeed("Commit executed successfully!");
|
|
793
|
+
console.log(chalk9.green("\n\u2713 Commit executed"));
|
|
794
|
+
console.log(chalk9.gray("\u2500".repeat(50)));
|
|
795
|
+
console.log(`${chalk9.bold("Commit ID:")} ${commit.id}`);
|
|
796
|
+
console.log(`${chalk9.bold("Message:")} ${commit.message}`);
|
|
797
|
+
console.log(`${chalk9.bold("SHA:")} ${sha}`);
|
|
798
|
+
console.log();
|
|
799
|
+
logger.success("commit", `Executed commit ${commit.id} -> ${sha}`);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
spinner.fail("Commit execution failed");
|
|
802
|
+
logger.error("commit", "Failed to execute commit", error);
|
|
803
|
+
console.error(chalk9.red(`
|
|
804
|
+
\u2716 Error: ${error.message}
|
|
805
|
+
`));
|
|
806
|
+
process6.exit(1);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
function getTimeAgo(date) {
|
|
811
|
+
const now = /* @__PURE__ */ new Date();
|
|
812
|
+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1e3);
|
|
813
|
+
if (seconds < 60) return "just now";
|
|
814
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
|
|
815
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
|
|
816
|
+
if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`;
|
|
817
|
+
return date.toLocaleDateString();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/index.ts
|
|
821
|
+
var program = new Command();
|
|
822
|
+
program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version("1.0.0").addHelpText("after", `
|
|
823
|
+
${chalk10.bold("Examples:")}
|
|
824
|
+
${chalk10.cyan("$")} stint login ${chalk10.gray("# Authenticate with Stint")}
|
|
825
|
+
${chalk10.cyan("$")} stint link ${chalk10.gray("# Link current directory to a project")}
|
|
826
|
+
${chalk10.cyan("$")} stint daemon start ${chalk10.gray("# Start background daemon")}
|
|
827
|
+
${chalk10.cyan("$")} stint status ${chalk10.gray("# Check status")}
|
|
828
|
+
${chalk10.cyan("$")} stint commits ${chalk10.gray("# List pending commits")}
|
|
829
|
+
|
|
830
|
+
${chalk10.bold("Documentation:")}
|
|
831
|
+
For more information, visit: ${chalk10.blue("https://stint.codes/docs")}
|
|
832
|
+
`);
|
|
833
|
+
registerLoginCommand(program);
|
|
834
|
+
registerLogoutCommand(program);
|
|
835
|
+
registerWhoamiCommand(program);
|
|
836
|
+
registerLinkCommand(program);
|
|
837
|
+
registerUnlinkCommand(program);
|
|
838
|
+
registerStatusCommand(program);
|
|
839
|
+
registerSyncCommand(program);
|
|
840
|
+
registerDaemonCommands(program);
|
|
841
|
+
registerCommitCommands(program);
|
|
842
|
+
program.exitOverride();
|
|
843
|
+
try {
|
|
844
|
+
await program.parseAsync(process.argv);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const commanderError = error;
|
|
847
|
+
if (commanderError.code !== "commander.help" && commanderError.code !== "commander.version") {
|
|
848
|
+
logger.error("cli", "Command execution failed", error);
|
|
849
|
+
console.error(chalk10.red(`
|
|
850
|
+
\u2716 Error: ${error.message}
|
|
851
|
+
`));
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
}
|