gitship-cli 0.0.1
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/bin.d.ts +2 -0
- package/dist/bin.js +949 -0
- package/dist/bin.js.map +1 -0
- package/package.json +26 -0
- package/src/bin.ts +1089 -0
- package/tsconfig.json +8 -0
package/src/bin.ts
ADDED
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { nanoid } from "nanoid";
|
|
9
|
+
import {
|
|
10
|
+
parseProjectConfig,
|
|
11
|
+
ProjectConfig,
|
|
12
|
+
Project,
|
|
13
|
+
Webhook,
|
|
14
|
+
} from "gitship-shared";
|
|
15
|
+
import http from "http";
|
|
16
|
+
import {
|
|
17
|
+
readAuthConfig,
|
|
18
|
+
writeAuthConfig,
|
|
19
|
+
getDb,
|
|
20
|
+
addProject,
|
|
21
|
+
getProject,
|
|
22
|
+
getProjects,
|
|
23
|
+
removeProject,
|
|
24
|
+
saveWebhook,
|
|
25
|
+
getWebhookByProjectId,
|
|
26
|
+
createDeployment,
|
|
27
|
+
getDeployment,
|
|
28
|
+
getDeployments,
|
|
29
|
+
getDeploymentLog,
|
|
30
|
+
getStats,
|
|
31
|
+
enqueueDeployment,
|
|
32
|
+
cancelDeployment,
|
|
33
|
+
CONFIG_PATH,
|
|
34
|
+
} from "gitship-core";
|
|
35
|
+
import { validateToken, listRepositories, listBranches, setupWebhook, openBrowser } from "gitship-core";
|
|
36
|
+
|
|
37
|
+
const program = new Command();
|
|
38
|
+
program
|
|
39
|
+
.name("deploykit")
|
|
40
|
+
.description("Lightweight GitHub-driven deployment toolkit")
|
|
41
|
+
.version("1.0.0");
|
|
42
|
+
|
|
43
|
+
// Helper to format duration
|
|
44
|
+
function formatDuration(ms: number | null): string {
|
|
45
|
+
if (ms === null || ms === undefined) return "-";
|
|
46
|
+
if (ms < 1000) return `${ms}ms`;
|
|
47
|
+
const secs = Math.floor(ms / 1000);
|
|
48
|
+
if (secs < 60) return `${secs}s`;
|
|
49
|
+
const mins = Math.floor(secs / 60);
|
|
50
|
+
const remainingSecs = secs % 60;
|
|
51
|
+
return `${mins}m${remainingSecs}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper to format status with colors
|
|
55
|
+
function formatStatus(status: string): string {
|
|
56
|
+
switch (status) {
|
|
57
|
+
case "QUEUED":
|
|
58
|
+
return chalk.yellow("QUEUED");
|
|
59
|
+
case "RUNNING":
|
|
60
|
+
return chalk.blue.bold("RUNNING");
|
|
61
|
+
case "SUCCESS":
|
|
62
|
+
return chalk.green("SUCCESS");
|
|
63
|
+
case "FAILED":
|
|
64
|
+
return chalk.red("FAILED");
|
|
65
|
+
case "CANCELLED":
|
|
66
|
+
return chalk.gray("CANCELLED");
|
|
67
|
+
default:
|
|
68
|
+
return status;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function startCallbackServer(clientId: string, clientSecret: string, port: number): Promise<string> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const server = http.createServer(async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const parsedUrl = new URL(req.url || "", `http://localhost:${port}`);
|
|
77
|
+
if (parsedUrl.pathname === "/callback") {
|
|
78
|
+
const code = parsedUrl.searchParams.get("code");
|
|
79
|
+
if (!code) {
|
|
80
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
81
|
+
res.end("<h1>Authentication Failed: Authorization code missing.</h1>");
|
|
82
|
+
reject(new Error("Callback missing code"));
|
|
83
|
+
server.close();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Accept: "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
client_id: clientId,
|
|
95
|
+
client_secret: clientSecret,
|
|
96
|
+
code,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const tokenData = (await tokenRes.json()) as any;
|
|
101
|
+
if (tokenData.error) {
|
|
102
|
+
throw new Error(tokenData.error_description || tokenData.error);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!tokenData.access_token) {
|
|
106
|
+
throw new Error("No access token returned from GitHub");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
110
|
+
res.end(`
|
|
111
|
+
<html>
|
|
112
|
+
<body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f6f8fa;">
|
|
113
|
+
<div style="display: inline-block; padding: 30px; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
|
|
114
|
+
<h1 style="color: #2da44e;">Authentication Successful!</h1>
|
|
115
|
+
<p style="color: #57606a;">You have successfully authenticated with DeployKit.</p>
|
|
116
|
+
<p style="color: #57606a;">You can close this tab and return to your terminal.</p>
|
|
117
|
+
</div>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
resolve(tokenData.access_token);
|
|
123
|
+
server.close();
|
|
124
|
+
} else {
|
|
125
|
+
res.writeHead(404);
|
|
126
|
+
res.end();
|
|
127
|
+
}
|
|
128
|
+
} catch (err: any) {
|
|
129
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
130
|
+
res.end(`<h1>Exchange failed: ${err.message}</h1>`);
|
|
131
|
+
reject(err);
|
|
132
|
+
server.close();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
server.listen(port, () => {
|
|
137
|
+
// Server listening
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
server.on("error", (err) => {
|
|
141
|
+
reject(err);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 1. AUTH GITHUB
|
|
147
|
+
program
|
|
148
|
+
.command("auth")
|
|
149
|
+
.description("Authentication commands")
|
|
150
|
+
.command("github")
|
|
151
|
+
.description("Authenticate with GitHub using a Personal Access Token or Browser OAuth")
|
|
152
|
+
.action(async () => {
|
|
153
|
+
try {
|
|
154
|
+
const existing = readAuthConfig() as any;
|
|
155
|
+
|
|
156
|
+
const { method } = await inquirer.prompt([
|
|
157
|
+
{
|
|
158
|
+
type: "list",
|
|
159
|
+
name: "method",
|
|
160
|
+
message: "Select authentication method:",
|
|
161
|
+
choices: [
|
|
162
|
+
{ name: "Browser OAuth Redirect Login (Recommended)", value: "browser" },
|
|
163
|
+
{ name: "Personal Access Token (Manual)", value: "pat" },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
if (method === "browser") {
|
|
169
|
+
console.log(chalk.cyan("\nDeployKit self-hosted Browser OAuth login flow."));
|
|
170
|
+
console.log(`Please ensure you have a GitHub OAuth App registered:`);
|
|
171
|
+
console.log(`1. Go to: ${chalk.bold("https://github.com/settings/developers")}`);
|
|
172
|
+
console.log(`2. Register a new OAuth application.`);
|
|
173
|
+
console.log(`3. Set ${chalk.bold("Homepage URL")} to: ${chalk.underline("http://localhost:4567")}`);
|
|
174
|
+
console.log(`4. Set ${chalk.bold("Authorization callback URL")} to: ${chalk.underline("http://localhost:4567/callback")}\n`);
|
|
175
|
+
|
|
176
|
+
const credentials = await inquirer.prompt([
|
|
177
|
+
{
|
|
178
|
+
type: "input",
|
|
179
|
+
name: "clientId",
|
|
180
|
+
message: "Enter your GitHub OAuth App Client ID:",
|
|
181
|
+
default: existing.github_oauth_client_id || "",
|
|
182
|
+
validate: (input) => (input.trim() ? true : "Client ID is required"),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: "password",
|
|
186
|
+
name: "clientSecret",
|
|
187
|
+
message: "Enter your GitHub OAuth App Client Secret:",
|
|
188
|
+
default: existing.github_oauth_client_secret || "",
|
|
189
|
+
mask: "*",
|
|
190
|
+
validate: (input) => (input.trim() ? true : "Client Secret is required"),
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const port = 4567;
|
|
195
|
+
const authorizeUrl = `https://github.com/login/oauth/authorize?client_id=${credentials.clientId}&redirect_uri=http://localhost:${port}/callback&scope=repo,admin:repo_hook`;
|
|
196
|
+
|
|
197
|
+
const serverPromise = startCallbackServer(credentials.clientId, credentials.clientSecret, port);
|
|
198
|
+
|
|
199
|
+
console.log(chalk.cyan(`\nOpening browser for GitHub authorization...`));
|
|
200
|
+
await openBrowser(authorizeUrl);
|
|
201
|
+
|
|
202
|
+
const spinner = ora("Waiting for redirection on localhost:4567...").start();
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const token = await serverPromise;
|
|
206
|
+
spinner.text = "Validating acquired OAuth token...";
|
|
207
|
+
|
|
208
|
+
const { username, name } = await validateToken(token);
|
|
209
|
+
spinner.succeed(`Token validated! Hello, ${name || username} (@${username}).`);
|
|
210
|
+
|
|
211
|
+
writeAuthConfig({
|
|
212
|
+
...existing,
|
|
213
|
+
github_token: token,
|
|
214
|
+
github_username: username,
|
|
215
|
+
github_oauth_client_id: credentials.clientId,
|
|
216
|
+
github_oauth_client_secret: credentials.clientSecret,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log(chalk.green("GitHub Browser OAuth authentication successful!"));
|
|
220
|
+
} catch (err: any) {
|
|
221
|
+
spinner.fail(`OAuth Authentication failed: ${err.message || err}`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const answers = await inquirer.prompt([
|
|
225
|
+
{
|
|
226
|
+
type: "password",
|
|
227
|
+
name: "token",
|
|
228
|
+
message: "Enter your GitHub Fine-Grained Personal Access Token:",
|
|
229
|
+
mask: "*",
|
|
230
|
+
validate: (input) => (input.trim() ? true : "Token cannot be empty"),
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const spinner = ora("Validating token...").start();
|
|
235
|
+
try {
|
|
236
|
+
const { username, name } = await validateToken(answers.token);
|
|
237
|
+
spinner.succeed(`Token validated! Hello, ${name || username} (@${username}).`);
|
|
238
|
+
|
|
239
|
+
// Save token
|
|
240
|
+
writeAuthConfig({
|
|
241
|
+
...existing,
|
|
242
|
+
github_token: answers.token,
|
|
243
|
+
github_username: username,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log(chalk.green("GitHub authentication token saved successfully."));
|
|
247
|
+
} catch (err: any) {
|
|
248
|
+
spinner.fail(`Validation failed: ${err.message || err}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (err: any) {
|
|
252
|
+
console.error(chalk.red(`Error during authentication: ${err.message}`));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 2. PROJECT INIT
|
|
257
|
+
program
|
|
258
|
+
.command("init")
|
|
259
|
+
.description("Initialize a new DeployKit project in the current directory")
|
|
260
|
+
.action(async () => {
|
|
261
|
+
try {
|
|
262
|
+
const auth = readAuthConfig();
|
|
263
|
+
if (!auth.github_token) {
|
|
264
|
+
console.error(chalk.red("Error: Not authenticated. Please run 'deploykit auth github' first."));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fetch user repos
|
|
269
|
+
const spinner = ora("Loading repositories from GitHub...").start();
|
|
270
|
+
let repos;
|
|
271
|
+
try {
|
|
272
|
+
repos = await listRepositories(auth.github_token);
|
|
273
|
+
spinner.succeed(`Loaded ${repos.length} repositories.`);
|
|
274
|
+
} catch (err: any) {
|
|
275
|
+
spinner.fail(`Failed to load repositories: ${err.message}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (repos.length === 0) {
|
|
280
|
+
console.error(chalk.yellow("No repositories found in your GitHub account."));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const repoChoices = repos.map((r) => ({
|
|
285
|
+
name: r.fullName,
|
|
286
|
+
value: r,
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
const { selectedRepo } = await inquirer.prompt([
|
|
290
|
+
{
|
|
291
|
+
type: "list",
|
|
292
|
+
name: "selectedRepo",
|
|
293
|
+
message: "Select a GitHub repository to deploy:",
|
|
294
|
+
choices: repoChoices,
|
|
295
|
+
},
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
// Fetch branches for that repo
|
|
299
|
+
const branchSpinner = ora(`Fetching branches for ${selectedRepo.fullName}...`).start();
|
|
300
|
+
let branches: string[] = [];
|
|
301
|
+
try {
|
|
302
|
+
branches = await listBranches(auth.github_token, selectedRepo.owner, selectedRepo.name);
|
|
303
|
+
branchSpinner.succeed(`Fetched ${branches.length} branches.`);
|
|
304
|
+
} catch (err: any) {
|
|
305
|
+
branchSpinner.fail(`Failed to fetch branches: ${err.message}`);
|
|
306
|
+
branches = ["main", "master"]; // Fallbacks
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const { selectedBranch } = await inquirer.prompt([
|
|
310
|
+
{
|
|
311
|
+
type: "list",
|
|
312
|
+
name: "selectedBranch",
|
|
313
|
+
message: "Select deployment branch:",
|
|
314
|
+
choices: branches,
|
|
315
|
+
default: branches.includes("main") ? "main" : branches[0],
|
|
316
|
+
},
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
// Auto-detect project type in current directory
|
|
320
|
+
let detectedType = "Node";
|
|
321
|
+
let installDefault = "npm ci";
|
|
322
|
+
let buildDefault = "npm run build";
|
|
323
|
+
let restartDefault = "npm start";
|
|
324
|
+
|
|
325
|
+
if (fs.existsSync(path.join(process.cwd(), "docker-compose.yml"))) {
|
|
326
|
+
detectedType = "Docker Compose";
|
|
327
|
+
installDefault = "";
|
|
328
|
+
buildDefault = "";
|
|
329
|
+
restartDefault = "docker-compose up -d --build";
|
|
330
|
+
} else if (fs.existsSync(path.join(process.cwd(), "Dockerfile"))) {
|
|
331
|
+
detectedType = "Docker";
|
|
332
|
+
installDefault = "";
|
|
333
|
+
buildDefault = "";
|
|
334
|
+
restartDefault = `docker build -t ${selectedRepo.name} . && docker run -d --name ${selectedRepo.name} -p 80:80 ${selectedRepo.name}`;
|
|
335
|
+
} else if (fs.existsSync(path.join(process.cwd(), "wrangler.toml"))) {
|
|
336
|
+
detectedType = "Cloudflare";
|
|
337
|
+
installDefault = "npm install";
|
|
338
|
+
buildDefault = "";
|
|
339
|
+
restartDefault = "npx wrangler deploy";
|
|
340
|
+
} else if (fs.existsSync(path.join(process.cwd(), "vercel.json"))) {
|
|
341
|
+
detectedType = "Vercel";
|
|
342
|
+
installDefault = "";
|
|
343
|
+
buildDefault = "";
|
|
344
|
+
restartDefault = "npx vercel --prod --yes";
|
|
345
|
+
} else if (fs.existsSync(path.join(process.cwd(), "ecosystem.config.js"))) {
|
|
346
|
+
detectedType = "PM2";
|
|
347
|
+
installDefault = "npm ci";
|
|
348
|
+
buildDefault = "npm run build";
|
|
349
|
+
restartDefault = `pm2 restart ${selectedRepo.name}`;
|
|
350
|
+
} else if (fs.existsSync(path.join(process.cwd(), "package.json"))) {
|
|
351
|
+
detectedType = "Node";
|
|
352
|
+
installDefault = "npm ci";
|
|
353
|
+
buildDefault = "npm run build";
|
|
354
|
+
restartDefault = `node dist/index.js`;
|
|
355
|
+
} else {
|
|
356
|
+
detectedType = "Generic/Unknown";
|
|
357
|
+
installDefault = "";
|
|
358
|
+
buildDefault = "";
|
|
359
|
+
restartDefault = "";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log(chalk.cyan(`\nAuto-detected project type: ${chalk.bold(detectedType)}`));
|
|
363
|
+
|
|
364
|
+
const details = await inquirer.prompt([
|
|
365
|
+
{
|
|
366
|
+
type: "input",
|
|
367
|
+
name: "projectName",
|
|
368
|
+
message: "Enter project name:",
|
|
369
|
+
default: selectedRepo.name,
|
|
370
|
+
validate: (input) =>
|
|
371
|
+
/^[a-zA-Z0-9_-]+$/.test(input)
|
|
372
|
+
? true
|
|
373
|
+
: "Project name can only contain letters, numbers, underscores, and dashes",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: "list",
|
|
377
|
+
name: "targetType",
|
|
378
|
+
message: "Select deployment target type:",
|
|
379
|
+
choices: [
|
|
380
|
+
{ name: "Local Server (Deploy on the current agent machine)", value: "local" },
|
|
381
|
+
{ name: "Remote Server (Deploy via SSH)", value: "ssh" },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "input",
|
|
386
|
+
name: "sshHost",
|
|
387
|
+
message: "Enter SSH Host (e.g. server.example.com or user@server.example.com):",
|
|
388
|
+
when: (answers) => answers.targetType === "ssh",
|
|
389
|
+
validate: (input) => (input.trim() ? true : "SSH Host is required"),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
type: "input",
|
|
393
|
+
name: "targetPath",
|
|
394
|
+
message: "Enter deployment path on target machine:",
|
|
395
|
+
default: (answers: any) =>
|
|
396
|
+
answers.targetType === "ssh"
|
|
397
|
+
? `/var/www/${answers.projectName}`
|
|
398
|
+
: path.join(process.cwd(), "dist-deploy"),
|
|
399
|
+
validate: (input) => (input.trim() ? true : "Target path is required"),
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
type: "input",
|
|
403
|
+
name: "installCmd",
|
|
404
|
+
message: "Install dependencies command:",
|
|
405
|
+
default: installDefault,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
type: "input",
|
|
409
|
+
name: "buildCmd",
|
|
410
|
+
message: "Build project command:",
|
|
411
|
+
default: buildDefault,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
type: "input",
|
|
415
|
+
name: "restartCmd",
|
|
416
|
+
message: "Restart application command:",
|
|
417
|
+
default: restartDefault,
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
// Construct config content
|
|
422
|
+
const config: ProjectConfig = {
|
|
423
|
+
project: details.projectName,
|
|
424
|
+
repository: {
|
|
425
|
+
owner: selectedRepo.owner,
|
|
426
|
+
repo: selectedRepo.name,
|
|
427
|
+
branch: selectedBranch,
|
|
428
|
+
},
|
|
429
|
+
target: {
|
|
430
|
+
type: details.targetType,
|
|
431
|
+
host: details.sshHost || undefined,
|
|
432
|
+
port: 22,
|
|
433
|
+
path: details.targetPath,
|
|
434
|
+
},
|
|
435
|
+
deploy: {
|
|
436
|
+
install: details.installCmd || undefined,
|
|
437
|
+
build: details.buildCmd || undefined,
|
|
438
|
+
restart: details.restartCmd || undefined,
|
|
439
|
+
},
|
|
440
|
+
logging: {
|
|
441
|
+
enabled: true,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Write yaml file
|
|
446
|
+
const { stringifyProjectConfig } = await import("gitship-shared");
|
|
447
|
+
const yamlStr = stringifyProjectConfig(config);
|
|
448
|
+
const configFilePath = path.join(process.cwd(), "deploykit.yml");
|
|
449
|
+
|
|
450
|
+
fs.writeFileSync(configFilePath, yamlStr, "utf-8");
|
|
451
|
+
console.log(chalk.green(`\nSuccess: Generated ${chalk.bold("deploykit.yml")} at ${configFilePath}\n`));
|
|
452
|
+
console.log(chalk.gray(yamlStr));
|
|
453
|
+
console.log(`Run ${chalk.bold("deploykit sync")} to configure the webhooks and link this project.`);
|
|
454
|
+
} catch (err: any) {
|
|
455
|
+
console.error(chalk.red(`Error during initialization: ${err.message}`));
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// 3. PROJECT SYNC
|
|
460
|
+
program
|
|
461
|
+
.command("sync")
|
|
462
|
+
.description("Sync deploykit.yml config with database and GitHub webhook")
|
|
463
|
+
.action(async () => {
|
|
464
|
+
try {
|
|
465
|
+
const configPath = path.join(process.cwd(), "deploykit.yml");
|
|
466
|
+
if (!fs.existsSync(configPath)) {
|
|
467
|
+
console.error(chalk.red("Error: deploykit.yml not found. Please run 'deploykit init' to generate it."));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const rawYaml = fs.readFileSync(configPath, "utf-8");
|
|
472
|
+
let newConfig: ProjectConfig;
|
|
473
|
+
try {
|
|
474
|
+
newConfig = parseProjectConfig(rawYaml);
|
|
475
|
+
} catch (err: any) {
|
|
476
|
+
console.error(chalk.red("Error: deploykit.yml validation failed."));
|
|
477
|
+
console.error(err.message || err);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check if project exists to print diff
|
|
482
|
+
const oldProject = getProject(newConfig.project);
|
|
483
|
+
if (oldProject) {
|
|
484
|
+
console.log(chalk.yellow(`\nProject "${newConfig.project}" already exists in the database. Comparing changes:`));
|
|
485
|
+
|
|
486
|
+
let hasChanges = false;
|
|
487
|
+
const compare = (field: string, oldVal: any, newVal: any) => {
|
|
488
|
+
if (oldVal !== newVal) {
|
|
489
|
+
console.log(` ${chalk.cyan(field)}: ${chalk.red(oldVal ?? "none")} -> ${chalk.green(newVal ?? "none")}`);
|
|
490
|
+
hasChanges = true;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
compare("Repository Owner", oldProject.owner, newConfig.repository.owner);
|
|
495
|
+
compare("Repository Name", oldProject.repo, newConfig.repository.repo);
|
|
496
|
+
compare("Branch", oldProject.branch, newConfig.repository.branch);
|
|
497
|
+
compare("Target Type", oldProject.target_type, newConfig.target.type);
|
|
498
|
+
compare("Target Host", oldProject.target_host, newConfig.target.host);
|
|
499
|
+
compare("Target Path", oldProject.target_path, newConfig.target.path);
|
|
500
|
+
compare("Install Command", oldProject.install_cmd, newConfig.deploy.install);
|
|
501
|
+
compare("Build Command", oldProject.build_cmd, newConfig.deploy.build);
|
|
502
|
+
compare("Restart Command", oldProject.restart_cmd, newConfig.deploy.restart);
|
|
503
|
+
|
|
504
|
+
if (!hasChanges) {
|
|
505
|
+
console.log(chalk.gray(" No configuration changes detected."));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const confirm = await inquirer.prompt([
|
|
509
|
+
{
|
|
510
|
+
type: "confirm",
|
|
511
|
+
name: "proceed",
|
|
512
|
+
message: "Do you want to apply these changes?",
|
|
513
|
+
default: true,
|
|
514
|
+
},
|
|
515
|
+
]);
|
|
516
|
+
|
|
517
|
+
if (!confirm.proceed) {
|
|
518
|
+
console.log("Sync cancelled.");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// We need agent URL to construct webhook endpoint on GitHub
|
|
524
|
+
const auth = readAuthConfig();
|
|
525
|
+
if (!auth.github_token) {
|
|
526
|
+
console.error(chalk.red("Error: Not authenticated. Please run 'deploykit auth github' first."));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Get or prompt for agent public URL
|
|
531
|
+
let agentUrl = (auth as any).agent_url;
|
|
532
|
+
if (!agentUrl) {
|
|
533
|
+
const answers = await inquirer.prompt([
|
|
534
|
+
{
|
|
535
|
+
type: "input",
|
|
536
|
+
name: "url",
|
|
537
|
+
message: "Enter the public base URL of your Server Agent (e.g. http://my-server.com:3000):",
|
|
538
|
+
validate: (input) => {
|
|
539
|
+
try {
|
|
540
|
+
new URL(input);
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
return "Please enter a valid URL (including protocol http/https)";
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
agentUrl = answers.url;
|
|
549
|
+
// Save the URL to config.json
|
|
550
|
+
writeAuthConfig({
|
|
551
|
+
...auth,
|
|
552
|
+
agent_url: agentUrl,
|
|
553
|
+
} as any);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const webhookUrl = `${agentUrl.replace(/\/$/, "")}/webhook/github`;
|
|
557
|
+
|
|
558
|
+
// Generate or reuse webhook secret
|
|
559
|
+
const webhookSecret = oldProject?.webhook_secret || `sec_${nanoid(15)}`;
|
|
560
|
+
|
|
561
|
+
const spinner = ora("Registering/Updating GitHub Webhook...").start();
|
|
562
|
+
let webhookInfo;
|
|
563
|
+
try {
|
|
564
|
+
webhookInfo = await setupWebhook(auth.github_token, {
|
|
565
|
+
owner: newConfig.repository.owner,
|
|
566
|
+
repo: newConfig.repository.repo,
|
|
567
|
+
webhookUrl,
|
|
568
|
+
secret: webhookSecret,
|
|
569
|
+
});
|
|
570
|
+
spinner.succeed(`GitHub Webhook configured successfully (ID: ${webhookInfo.id})`);
|
|
571
|
+
} catch (err: any) {
|
|
572
|
+
spinner.fail(`Failed to register GitHub Webhook: ${err.message || err}`);
|
|
573
|
+
console.error(chalk.yellow("Note: If the repository is private or your token doesn't have hooks write scope, this step will fail."));
|
|
574
|
+
|
|
575
|
+
// Ask if they want to proceed saving locally anyway
|
|
576
|
+
const forceSave = await inquirer.prompt([
|
|
577
|
+
{
|
|
578
|
+
type: "confirm",
|
|
579
|
+
name: "save",
|
|
580
|
+
message: "Save project settings locally anyway without configuring webhook on GitHub?",
|
|
581
|
+
default: false,
|
|
582
|
+
},
|
|
583
|
+
]);
|
|
584
|
+
|
|
585
|
+
if (!forceSave.save) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Mock webhook info
|
|
590
|
+
webhookInfo = { id: null, url: webhookUrl };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Add to database
|
|
594
|
+
const projectRecord: Project = {
|
|
595
|
+
id: oldProject?.id || `proj_${nanoid(8)}`,
|
|
596
|
+
name: newConfig.project,
|
|
597
|
+
owner: newConfig.repository.owner,
|
|
598
|
+
repo: newConfig.repository.repo,
|
|
599
|
+
branch: newConfig.repository.branch,
|
|
600
|
+
target_type: newConfig.target.type,
|
|
601
|
+
target_host: newConfig.target.host,
|
|
602
|
+
target_path: newConfig.target.path,
|
|
603
|
+
install_cmd: newConfig.deploy.install,
|
|
604
|
+
build_cmd: newConfig.deploy.build,
|
|
605
|
+
restart_cmd: newConfig.deploy.restart,
|
|
606
|
+
healthcheck_path: newConfig.deploy.healthcheck?.path,
|
|
607
|
+
healthcheck_port: newConfig.deploy.healthcheck?.port,
|
|
608
|
+
healthcheck_retries: newConfig.deploy.healthcheck?.retries,
|
|
609
|
+
healthcheck_interval_ms: newConfig.deploy.healthcheck?.interval_ms,
|
|
610
|
+
healthcheck_timeout_ms: newConfig.deploy.healthcheck?.timeout_ms,
|
|
611
|
+
webhook_secret: webhookSecret,
|
|
612
|
+
created_at: oldProject?.created_at || Date.now(),
|
|
613
|
+
updated_at: Date.now(),
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
addProject(projectRecord);
|
|
617
|
+
|
|
618
|
+
// Save webhook record
|
|
619
|
+
const webhookRecord: Webhook = {
|
|
620
|
+
id: oldProject ? (getWebhookByProjectId(oldProject.id)?.id || `wh_${nanoid(8)}`) : `wh_${nanoid(8)}`,
|
|
621
|
+
project_id: projectRecord.id,
|
|
622
|
+
github_webhook_id: webhookInfo.id,
|
|
623
|
+
url: webhookInfo.url,
|
|
624
|
+
secret: webhookSecret,
|
|
625
|
+
active: true,
|
|
626
|
+
created_at: Date.now(),
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
saveWebhook(webhookRecord);
|
|
630
|
+
|
|
631
|
+
console.log(chalk.green(`\nSuccess: Project "${newConfig.project}" synced and stored in local database.`));
|
|
632
|
+
console.log(`Webhook Endpoint URL: ${chalk.bold(webhookInfo.url)}`);
|
|
633
|
+
} catch (err: any) {
|
|
634
|
+
console.error(chalk.red(`Error during sync: ${err.message}`));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// 4. RUNS LIST
|
|
639
|
+
program
|
|
640
|
+
.command("runs")
|
|
641
|
+
.description("List past deployment runs")
|
|
642
|
+
.option("--last <n>", "Limit output to the last N runs", "20")
|
|
643
|
+
.action((options) => {
|
|
644
|
+
try {
|
|
645
|
+
const limit = parseInt(options.last);
|
|
646
|
+
const runs = getDeployments(undefined, limit);
|
|
647
|
+
|
|
648
|
+
if (runs.length === 0) {
|
|
649
|
+
console.log(chalk.yellow("No deployment runs found in the database."));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
console.log(chalk.bold("\nLast Deployments:"));
|
|
654
|
+
console.log(
|
|
655
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
656
|
+
);
|
|
657
|
+
console.log(
|
|
658
|
+
`${chalk.bold("ID").padEnd(14)} ${chalk.bold("Project").padEnd(15)} ${chalk.bold(
|
|
659
|
+
"Branch"
|
|
660
|
+
).padEnd(10)} ${chalk.bold("Commit").padEnd(8)} ${chalk.bold("Status").padEnd(20)} ${chalk.bold(
|
|
661
|
+
"Duration"
|
|
662
|
+
)}`
|
|
663
|
+
);
|
|
664
|
+
console.log(
|
|
665
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
for (const run of runs) {
|
|
669
|
+
const project = getProject(run.project_id);
|
|
670
|
+
const projectName = project ? project.name : run.project_id;
|
|
671
|
+
const commitSha = run.commit_sha ? run.commit_sha.substring(0, 7) : "latest";
|
|
672
|
+
const durationStr = formatDuration(run.total_duration_ms);
|
|
673
|
+
|
|
674
|
+
console.log(
|
|
675
|
+
`${run.id.padEnd(14)} ${projectName.substring(0, 14).padEnd(15)} ${run.branch.substring(0, 9).padEnd(
|
|
676
|
+
10
|
|
677
|
+
)} ${commitSha.padEnd(8)} ${formatStatus(run.status).padEnd(20)} ${durationStr}`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
} catch (err: any) {
|
|
682
|
+
console.error(chalk.red(`Error loading runs: ${err.message}`));
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// 5. RUN LOGS
|
|
687
|
+
program
|
|
688
|
+
.command("logs <id>")
|
|
689
|
+
.description("View the output logs of a specific deployment run")
|
|
690
|
+
.option("-f, --follow", "Follow/stream the log output in real-time")
|
|
691
|
+
.action(async (id, options) => {
|
|
692
|
+
try {
|
|
693
|
+
const run = getDeployment(id);
|
|
694
|
+
if (!run) {
|
|
695
|
+
console.error(chalk.red(`Error: Deployment run "${id}" not found.`));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const project = getProject(run.project_id);
|
|
700
|
+
const projectName = project ? project.name : run.project_id;
|
|
701
|
+
console.log(chalk.bold(`Logs for Deployment #${id} (${projectName} - ${run.branch}):`));
|
|
702
|
+
console.log(chalk.gray("--------------------------------------------------------------------------------"));
|
|
703
|
+
|
|
704
|
+
let lastPrintedLength = 0;
|
|
705
|
+
|
|
706
|
+
const printNewLogs = () => {
|
|
707
|
+
const fullLog = getDeploymentLog(id);
|
|
708
|
+
if (fullLog && fullLog.length > lastPrintedLength) {
|
|
709
|
+
const newChunk = fullLog.substring(lastPrintedLength);
|
|
710
|
+
process.stdout.write(newChunk);
|
|
711
|
+
lastPrintedLength = fullLog.length;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// Print whatever exists right now
|
|
716
|
+
printNewLogs();
|
|
717
|
+
|
|
718
|
+
if (options.follow && (run.status === "RUNNING" || run.status === "QUEUED")) {
|
|
719
|
+
const timer = setInterval(() => {
|
|
720
|
+
const currentRun = getDeployment(id);
|
|
721
|
+
if (!currentRun) {
|
|
722
|
+
clearInterval(timer);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
printNewLogs();
|
|
727
|
+
|
|
728
|
+
if (currentRun.status !== "RUNNING" && currentRun.status !== "QUEUED") {
|
|
729
|
+
clearInterval(timer);
|
|
730
|
+
// final print diff
|
|
731
|
+
printNewLogs();
|
|
732
|
+
console.log(chalk.gray("\n--------------------------------------------------------------------------------"));
|
|
733
|
+
console.log(`Deployment finished with status: ${formatStatus(currentRun.status)}`);
|
|
734
|
+
console.log(`Total duration: ${formatDuration(currentRun.total_duration_ms)}`);
|
|
735
|
+
}
|
|
736
|
+
}, 500);
|
|
737
|
+
|
|
738
|
+
// Keep process open
|
|
739
|
+
process.on("SIGINT", () => {
|
|
740
|
+
clearInterval(timer);
|
|
741
|
+
console.log(chalk.yellow("\nLog streaming stopped by user."));
|
|
742
|
+
process.exit(0);
|
|
743
|
+
});
|
|
744
|
+
} else {
|
|
745
|
+
console.log(chalk.gray("\n--------------------------------------------------------------------------------"));
|
|
746
|
+
console.log(`Deployment status: ${formatStatus(run.status)}`);
|
|
747
|
+
console.log(`Total duration: ${formatDuration(run.total_duration_ms)}`);
|
|
748
|
+
}
|
|
749
|
+
} catch (err: any) {
|
|
750
|
+
console.error(chalk.red(`Error loading logs: ${err.message}`));
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// 6. STATISTICS
|
|
755
|
+
program
|
|
756
|
+
.command("stats")
|
|
757
|
+
.description("View project deployment metrics and statistics")
|
|
758
|
+
.option("--project <name>", "Filter stats by a specific project name")
|
|
759
|
+
.action((options) => {
|
|
760
|
+
try {
|
|
761
|
+
let projectId: string | undefined;
|
|
762
|
+
let projectTitle = "All Projects";
|
|
763
|
+
|
|
764
|
+
if (options.project) {
|
|
765
|
+
const p = getProject(options.project);
|
|
766
|
+
if (!p) {
|
|
767
|
+
console.error(chalk.red(`Error: Project "${options.project}" not found.`));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
projectId = p.id;
|
|
771
|
+
projectTitle = `Project: ${p.name}`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const stats = getStats(projectId);
|
|
775
|
+
|
|
776
|
+
console.log(chalk.bold(`\n=== Deployment Statistics [${projectTitle}] ===\n`));
|
|
777
|
+
console.log(`Total Deployments: ${chalk.cyan(stats.totalDeployments)}`);
|
|
778
|
+
|
|
779
|
+
const successColor = stats.successRate > 90 ? chalk.green : stats.successRate > 70 ? chalk.yellow : chalk.red;
|
|
780
|
+
console.log(`Success Rate: ${successColor(`${stats.successRate}%`)}`);
|
|
781
|
+
|
|
782
|
+
console.log(`Average Deployment Time: ${chalk.cyan(formatDuration(stats.avgDeployTimeMs))}`);
|
|
783
|
+
console.log(`Average Build Step Time: ${chalk.cyan(formatDuration(stats.avgBuildTimeMs))}`);
|
|
784
|
+
console.log(`Fastest Deployment: ${chalk.green(formatDuration(stats.fastestDeployMs))}`);
|
|
785
|
+
console.log(`Slowest Deployment: ${chalk.red(formatDuration(stats.slowestDeployMs))}`);
|
|
786
|
+
console.log();
|
|
787
|
+
} catch (err: any) {
|
|
788
|
+
console.error(chalk.red(`Error loading stats: ${err.message}`));
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// 7. ROLLBACK
|
|
793
|
+
program
|
|
794
|
+
.command("rollback <deployment-id>")
|
|
795
|
+
.description("Rollback project to a previous successful deployment")
|
|
796
|
+
.action(async (id) => {
|
|
797
|
+
try {
|
|
798
|
+
const dep = getDeployment(id);
|
|
799
|
+
if (!dep) {
|
|
800
|
+
console.error(chalk.red(`Error: Deployment run "${id}" not found.`));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (dep.status !== "SUCCESS") {
|
|
805
|
+
console.error(chalk.red(`Error: Cannot rollback to deployment "${id}" because its status is ${dep.status}. Only SUCCESS status is rollback target.`));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const project = getProject(dep.project_id);
|
|
810
|
+
if (!project) {
|
|
811
|
+
console.error(chalk.red(`Error: Associated project "${dep.project_id}" not found.`));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
console.log(chalk.yellow(`\nInitiating rollback of project "${project.name}" to deployment #${id}`));
|
|
816
|
+
console.log(`Target Commit: ${dep.commit_sha ? dep.commit_sha.substring(0, 7) : "latest"} - "${dep.commit_message || ""}"`);
|
|
817
|
+
console.log(`Target Branch: ${dep.branch}`);
|
|
818
|
+
|
|
819
|
+
const confirm = await inquirer.prompt([
|
|
820
|
+
{
|
|
821
|
+
type: "confirm",
|
|
822
|
+
name: "proceed",
|
|
823
|
+
message: "Are you sure you want to trigger this rollback?",
|
|
824
|
+
default: true,
|
|
825
|
+
},
|
|
826
|
+
]);
|
|
827
|
+
|
|
828
|
+
if (!confirm.proceed) {
|
|
829
|
+
console.log("Rollback cancelled.");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const spinner = ora("Enqueuing rollback deployment...").start();
|
|
834
|
+
try {
|
|
835
|
+
const rollbackDep = await enqueueDeployment(
|
|
836
|
+
project.id,
|
|
837
|
+
dep.branch,
|
|
838
|
+
dep.commit_sha,
|
|
839
|
+
`Rollback to #${id}`,
|
|
840
|
+
`Rollback (triggered via CLI)`,
|
|
841
|
+
id
|
|
842
|
+
);
|
|
843
|
+
spinner.succeed(`Rollback enqueued successfully!`);
|
|
844
|
+
console.log(`New Deployment ID: ${chalk.bold(rollbackDep.id)}`);
|
|
845
|
+
console.log(`Run ${chalk.bold(`deploykit logs ${rollbackDep.id} -f`)} to watch execution.`);
|
|
846
|
+
} catch (err: any) {
|
|
847
|
+
spinner.fail(`Failed to enqueue rollback: ${err.message || err}`);
|
|
848
|
+
}
|
|
849
|
+
} catch (err: any) {
|
|
850
|
+
console.error(chalk.red(`Error during rollback: ${err.message}`));
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// 8. QUEUE & CANCEL
|
|
855
|
+
program
|
|
856
|
+
.command("queue")
|
|
857
|
+
.description("List deployments that are currently QUEUED or RUNNING")
|
|
858
|
+
.action(() => {
|
|
859
|
+
try {
|
|
860
|
+
const allDeploys = getDeployments();
|
|
861
|
+
const activeDeploys = allDeploys.filter(d => d.status === "QUEUED" || d.status === "RUNNING");
|
|
862
|
+
|
|
863
|
+
if (activeDeploys.length === 0) {
|
|
864
|
+
console.log(chalk.green("No active or queued deployments. The queue is empty."));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log(chalk.bold("\nActive Queue:"));
|
|
869
|
+
console.log(
|
|
870
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
871
|
+
);
|
|
872
|
+
console.log(
|
|
873
|
+
`${chalk.bold("ID").padEnd(14)} ${chalk.bold("Project").padEnd(15)} ${chalk.bold(
|
|
874
|
+
"Branch"
|
|
875
|
+
).padEnd(10)} ${chalk.bold("Commit").padEnd(8)} ${chalk.bold("Status")}`
|
|
876
|
+
);
|
|
877
|
+
console.log(
|
|
878
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
for (const run of activeDeploys) {
|
|
882
|
+
const project = getProject(run.project_id);
|
|
883
|
+
const projectName = project ? project.name : run.project_id;
|
|
884
|
+
const commitSha = run.commit_sha ? run.commit_sha.substring(0, 7) : "latest";
|
|
885
|
+
console.log(
|
|
886
|
+
`${run.id.padEnd(14)} ${projectName.substring(0, 14).padEnd(15)} ${run.branch.substring(0, 9).padEnd(
|
|
887
|
+
10
|
|
888
|
+
)} ${commitSha.padEnd(8)} ${formatStatus(run.status)}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
console.log();
|
|
892
|
+
} catch (err: any) {
|
|
893
|
+
console.error(chalk.red(`Error reading queue: ${err.message}`));
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
program
|
|
898
|
+
.command("cancel <id>")
|
|
899
|
+
.description("Cancel a queued or running deployment")
|
|
900
|
+
.action((id) => {
|
|
901
|
+
try {
|
|
902
|
+
const spinner = ora(`Requesting cancellation for deployment ${id}...`).start();
|
|
903
|
+
const res = cancelDeployment(id);
|
|
904
|
+
if (res.success) {
|
|
905
|
+
spinner.succeed(res.message);
|
|
906
|
+
} else {
|
|
907
|
+
spinner.fail(res.message);
|
|
908
|
+
}
|
|
909
|
+
} catch (err: any) {
|
|
910
|
+
console.error(chalk.red(`Error cancelling deployment: ${err.message}`));
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// 9. PROJECT COMMANDS
|
|
915
|
+
const projectsCmd = program
|
|
916
|
+
.command("projects")
|
|
917
|
+
.description("List all configured projects in the database")
|
|
918
|
+
.action(() => {
|
|
919
|
+
try {
|
|
920
|
+
const projects = getProjects();
|
|
921
|
+
if (projects.length === 0) {
|
|
922
|
+
console.log(chalk.yellow("No projects found in the database. Run 'deploykit init' to create one."));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
console.log(chalk.bold("\nConfigured Projects:"));
|
|
927
|
+
console.log(
|
|
928
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
929
|
+
);
|
|
930
|
+
console.log(
|
|
931
|
+
`${chalk.bold("Name").padEnd(15)} ${chalk.bold("Repository").padEnd(30)} ${chalk.bold(
|
|
932
|
+
"Branch"
|
|
933
|
+
).padEnd(12)} ${chalk.bold("Target Path")}`
|
|
934
|
+
);
|
|
935
|
+
console.log(
|
|
936
|
+
chalk.gray("--------------------------------------------------------------------------------")
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
for (const project of projects) {
|
|
940
|
+
const repoStr = `${project.owner}/${project.repo}`;
|
|
941
|
+
console.log(
|
|
942
|
+
`${project.name.padEnd(15)} ${repoStr.substring(0, 29).padEnd(30)} ${project.branch.padEnd(12)} ${project.target_path}`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
console.log();
|
|
946
|
+
} catch (err: any) {
|
|
947
|
+
console.error(chalk.red(`Error loading projects: ${err.message}`));
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// project subcommand container for add/remove/inspect
|
|
952
|
+
const projectCmd = program.command("project").description("Manage individual projects");
|
|
953
|
+
|
|
954
|
+
projectCmd
|
|
955
|
+
.command("add <file>")
|
|
956
|
+
.description("Manually register a project from a deploykit.yml config file")
|
|
957
|
+
.action((file) => {
|
|
958
|
+
try {
|
|
959
|
+
const filePath = path.resolve(file);
|
|
960
|
+
if (!fs.existsSync(filePath)) {
|
|
961
|
+
console.error(chalk.red(`Error: File "${file}" not found.`));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const rawYaml = fs.readFileSync(filePath, "utf-8");
|
|
966
|
+
let config: ProjectConfig;
|
|
967
|
+
try {
|
|
968
|
+
config = parseProjectConfig(rawYaml);
|
|
969
|
+
} catch (err: any) {
|
|
970
|
+
console.error(chalk.red("Error: Config validation failed."));
|
|
971
|
+
console.error(err.message || err);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const secret = `sec_${nanoid(15)}`;
|
|
976
|
+
const projectRecord: Project = {
|
|
977
|
+
id: `proj_${nanoid(8)}`,
|
|
978
|
+
name: config.project,
|
|
979
|
+
owner: config.repository.owner,
|
|
980
|
+
repo: config.repository.repo,
|
|
981
|
+
branch: config.repository.branch,
|
|
982
|
+
target_type: config.target.type,
|
|
983
|
+
target_host: config.target.host,
|
|
984
|
+
target_path: config.target.path,
|
|
985
|
+
install_cmd: config.deploy.install,
|
|
986
|
+
build_cmd: config.deploy.build,
|
|
987
|
+
restart_cmd: config.deploy.restart,
|
|
988
|
+
healthcheck_path: config.deploy.healthcheck?.path,
|
|
989
|
+
healthcheck_port: config.deploy.healthcheck?.port,
|
|
990
|
+
healthcheck_retries: config.deploy.healthcheck?.retries,
|
|
991
|
+
healthcheck_interval_ms: config.deploy.healthcheck?.interval_ms,
|
|
992
|
+
healthcheck_timeout_ms: config.deploy.healthcheck?.timeout_ms,
|
|
993
|
+
webhook_secret: secret,
|
|
994
|
+
created_at: Date.now(),
|
|
995
|
+
updated_at: Date.now(),
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
addProject(projectRecord);
|
|
999
|
+
console.log(chalk.green(`Successfully registered project "${config.project}" manually.`));
|
|
1000
|
+
console.log(`Webhook Secret: ${chalk.bold(secret)}`);
|
|
1001
|
+
} catch (err: any) {
|
|
1002
|
+
console.error(chalk.red(`Error registering project: ${err.message}`));
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
projectCmd
|
|
1007
|
+
.command("remove <name>")
|
|
1008
|
+
.description("Remove a project from the database")
|
|
1009
|
+
.action(async (name) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const project = getProject(name);
|
|
1012
|
+
if (!project) {
|
|
1013
|
+
console.error(chalk.red(`Error: Project "${name}" not found.`));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const confirm = await inquirer.prompt([
|
|
1018
|
+
{
|
|
1019
|
+
type: "confirm",
|
|
1020
|
+
name: "proceed",
|
|
1021
|
+
message: `Are you sure you want to delete project "${name}"? This removes its configuration and deployment history!`,
|
|
1022
|
+
default: false,
|
|
1023
|
+
},
|
|
1024
|
+
]);
|
|
1025
|
+
|
|
1026
|
+
if (!confirm.proceed) {
|
|
1027
|
+
console.log("Cancelled.");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
removeProject(project.id);
|
|
1032
|
+
console.log(chalk.green(`Project "${name}" removed from database.`));
|
|
1033
|
+
} catch (err: any) {
|
|
1034
|
+
console.error(chalk.red(`Error removing project: ${err.message}`));
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
projectCmd
|
|
1039
|
+
.command("inspect <name>")
|
|
1040
|
+
.description("Inspect project configurations and webhook secrets")
|
|
1041
|
+
.action((name) => {
|
|
1042
|
+
try {
|
|
1043
|
+
const project = getProject(name);
|
|
1044
|
+
if (!project) {
|
|
1045
|
+
console.error(chalk.red(`Error: Project "${name}" not found.`));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const webhook = getWebhookByProjectId(project.id);
|
|
1050
|
+
|
|
1051
|
+
console.log(chalk.bold(`\n=== Project: ${project.name} ===`));
|
|
1052
|
+
console.log(`Database ID: ${project.id}`);
|
|
1053
|
+
console.log(`Repository: https://github.com/${project.owner}/${project.repo} (Branch: ${project.branch})`);
|
|
1054
|
+
console.log(`Target Type: ${project.target_type}`);
|
|
1055
|
+
if (project.target_type === "ssh") {
|
|
1056
|
+
console.log(`Target Host: ${project.target_host}`);
|
|
1057
|
+
}
|
|
1058
|
+
console.log(`Target Path: ${project.target_path}`);
|
|
1059
|
+
console.log(`Install Cmd: ${project.install_cmd || chalk.gray("none")}`);
|
|
1060
|
+
console.log(`Build Cmd: ${project.build_cmd || chalk.gray("none")}`);
|
|
1061
|
+
console.log(`Restart Cmd: ${project.restart_cmd || chalk.gray("none")}`);
|
|
1062
|
+
|
|
1063
|
+
console.log(chalk.bold(`\n--- Webhook Settings ---`));
|
|
1064
|
+
if (webhook) {
|
|
1065
|
+
console.log(`Webhook URL: ${webhook.url}`);
|
|
1066
|
+
console.log(`GitHub Hook ID: ${webhook.github_webhook_id || chalk.gray("none (registered locally)")}`);
|
|
1067
|
+
console.log(`HMAC Secret: ${webhook.secret}`);
|
|
1068
|
+
console.log(`Status: ${webhook.active ? chalk.green("active") : chalk.red("inactive")}`);
|
|
1069
|
+
} else {
|
|
1070
|
+
console.log(chalk.yellow("No webhook settings found in database. Run 'deploykit sync' to set it up."));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const projectRuns = getDeployments(project.id, 5);
|
|
1074
|
+
console.log(chalk.bold(`\n--- Recent Runs ---`));
|
|
1075
|
+
if (projectRuns.length === 0) {
|
|
1076
|
+
console.log(chalk.gray("No runs recorded yet."));
|
|
1077
|
+
} else {
|
|
1078
|
+
for (const run of projectRuns) {
|
|
1079
|
+
const durationStr = formatDuration(run.total_duration_ms);
|
|
1080
|
+
console.log(` #${run.id.substring(0, 10)} - [${formatStatus(run.status)}] - ${run.commit_sha?.substring(0, 7) || "latest"} - ${run.commit_message || "no message"} (${durationStr})`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
console.log();
|
|
1084
|
+
} catch (err: any) {
|
|
1085
|
+
console.error(chalk.red(`Error inspecting project: ${err.message}`));
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
program.parse(process.argv);
|