@argus-vrt/web 0.1.0 → 0.1.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/README.md +65 -109
- package/dist-cli/index.js +482 -0
- package/package.json +26 -14
- package/.cta.json +0 -15
- package/DEPLOYMENT.md +0 -154
- package/Dockerfile +0 -51
- package/docker-compose.prod.yml +0 -38
- package/docker-compose.yml +0 -15
- package/drizzle/0000_slim_makkari.sql +0 -61
- package/drizzle/meta/0000_snapshot.json +0 -452
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/public/favicon.ico +0 -0
- package/public/logo-argus.svg +0 -8
- package/public/logo-variants/logo-argus-a.svg +0 -9
- package/public/logo-variants/logo-argus-modern.svg +0 -11
- package/public/logo-variants/logo-argus-peacock.svg +0 -8
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/robots.txt +0 -3
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +0 -1
- package/scripts/backfill-kind.ts +0 -148
- package/src/api-plugin.ts +0 -169
- package/src/components/image/ImageCompare.tsx +0 -188
- package/src/components/story/StoryFlatList.tsx +0 -67
- package/src/components/story/StoryGroupedTree.tsx +0 -273
- package/src/components/story/StoryTree.tsx +0 -185
- package/src/components/ui/Drawer.tsx +0 -110
- package/src/components/ui/SearchInput.tsx +0 -95
- package/src/components/ui/StatusBadge.tsx +0 -59
- package/src/components/ui/ViewModeToggle.tsx +0 -39
- package/src/db/index.ts +0 -27
- package/src/db/schema.ts +0 -151
- package/src/hooks/useDebounce.ts +0 -23
- package/src/hooks/useStoryTree.ts +0 -205
- package/src/lib/utils.ts +0 -55
- package/src/logo.svg +0 -12
- package/src/routeTree.gen.ts +0 -177
- package/src/router.tsx +0 -17
- package/src/routes/__root.tsx +0 -174
- package/src/routes/branches/$name.tsx +0 -171
- package/src/routes/branches/index.tsx +0 -104
- package/src/routes/index.tsx +0 -178
- package/src/routes/tests/$id.tsx +0 -417
- package/src/routes/tests/index.tsx +0 -128
- package/src/routes/upload.tsx +0 -108
- package/src/styles.css +0 -213
- package/tsconfig.json +0 -28
- package/vite.config.ts +0 -30
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// cli/commands/init.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
|
|
10
|
+
import { resolve as resolve2 } from "path";
|
|
11
|
+
|
|
12
|
+
// cli/utils/config.ts
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { resolve } from "path";
|
|
15
|
+
var DOCKER_IMAGE = "ghcr.io/maxcwolf/argus-web";
|
|
16
|
+
var DEFAULT_DIR = "./argus";
|
|
17
|
+
var DEFAULT_PORT = 3e3;
|
|
18
|
+
var DEFAULT_DB_PASSWORD = "argus";
|
|
19
|
+
var DEFAULT_SCREENSHOTS_PATH = "./argus-data/images";
|
|
20
|
+
function findArgusDir(dir) {
|
|
21
|
+
const target = dir ? resolve(dir) : resolve(process.cwd(), DEFAULT_DIR);
|
|
22
|
+
if (!existsSync(resolve(target, "docker-compose.yml"))) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`No docker-compose.yml found in ${target}.
|
|
25
|
+
Run 'argus-web init' first to set up Argus.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// cli/templates/docker-compose.ts
|
|
32
|
+
function generateDockerCompose(options) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push("services:");
|
|
35
|
+
lines.push(" web:");
|
|
36
|
+
lines.push(` image: ${DOCKER_IMAGE}:latest`);
|
|
37
|
+
lines.push(" container_name: argus-web");
|
|
38
|
+
if (!options.includeNginx) {
|
|
39
|
+
lines.push(" ports:");
|
|
40
|
+
lines.push(` - "\${PORT:-${options.port}}:3000"`);
|
|
41
|
+
} else {
|
|
42
|
+
lines.push(" expose:");
|
|
43
|
+
lines.push(' - "3000"');
|
|
44
|
+
}
|
|
45
|
+
lines.push(" environment:");
|
|
46
|
+
if (options.includeDb) {
|
|
47
|
+
lines.push(
|
|
48
|
+
" - DATABASE_URL=postgresql://argus:${DB_PASSWORD:-argus}@db:5432/argus"
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
lines.push(" - DATABASE_URL=${DATABASE_URL}");
|
|
52
|
+
}
|
|
53
|
+
lines.push(" - NODE_ENV=production");
|
|
54
|
+
if (options.includeDb) {
|
|
55
|
+
lines.push(" depends_on:");
|
|
56
|
+
lines.push(" db:");
|
|
57
|
+
lines.push(" condition: service_healthy");
|
|
58
|
+
}
|
|
59
|
+
lines.push(" volumes:");
|
|
60
|
+
lines.push(
|
|
61
|
+
` - \${SCREENSHOTS_PATH:-${options.screenshotsPath}}:/screenshots:ro`
|
|
62
|
+
);
|
|
63
|
+
lines.push(" restart: unless-stopped");
|
|
64
|
+
if (options.includeDb) {
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(" db:");
|
|
67
|
+
lines.push(" image: postgres:16-alpine");
|
|
68
|
+
lines.push(" container_name: argus-db");
|
|
69
|
+
lines.push(" environment:");
|
|
70
|
+
lines.push(" POSTGRES_USER: argus");
|
|
71
|
+
lines.push(" POSTGRES_PASSWORD: ${DB_PASSWORD:-argus}");
|
|
72
|
+
lines.push(" POSTGRES_DB: argus");
|
|
73
|
+
lines.push(" volumes:");
|
|
74
|
+
lines.push(" - postgres_data:/var/lib/postgresql/data");
|
|
75
|
+
lines.push(" healthcheck:");
|
|
76
|
+
lines.push(' test: ["CMD-SHELL", "pg_isready -U argus"]');
|
|
77
|
+
lines.push(" interval: 5s");
|
|
78
|
+
lines.push(" timeout: 5s");
|
|
79
|
+
lines.push(" retries: 5");
|
|
80
|
+
lines.push(" restart: unless-stopped");
|
|
81
|
+
}
|
|
82
|
+
if (options.includeNginx) {
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(" nginx:");
|
|
85
|
+
lines.push(" image: nginx:alpine");
|
|
86
|
+
lines.push(" container_name: argus-nginx");
|
|
87
|
+
lines.push(" ports:");
|
|
88
|
+
lines.push(` - "\${PORT:-${options.port}}:80"`);
|
|
89
|
+
if (options.https !== "none") {
|
|
90
|
+
lines.push(' - "443:443"');
|
|
91
|
+
}
|
|
92
|
+
lines.push(" volumes:");
|
|
93
|
+
lines.push(" - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro");
|
|
94
|
+
if (options.https === "letsencrypt") {
|
|
95
|
+
lines.push(" - certbot_data:/etc/letsencrypt:ro");
|
|
96
|
+
lines.push(" - certbot_www:/var/www/certbot:ro");
|
|
97
|
+
} else if (options.https === "custom") {
|
|
98
|
+
lines.push(" - ./certs:/etc/nginx/certs:ro");
|
|
99
|
+
}
|
|
100
|
+
lines.push(" depends_on:");
|
|
101
|
+
lines.push(" - web");
|
|
102
|
+
lines.push(" restart: unless-stopped");
|
|
103
|
+
if (options.https === "letsencrypt") {
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(" certbot:");
|
|
106
|
+
lines.push(" image: certbot/certbot");
|
|
107
|
+
lines.push(" container_name: argus-certbot");
|
|
108
|
+
lines.push(" volumes:");
|
|
109
|
+
lines.push(" - certbot_data:/etc/letsencrypt");
|
|
110
|
+
lines.push(" - certbot_www:/var/www/certbot");
|
|
111
|
+
lines.push(
|
|
112
|
+
" entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const volumes = [];
|
|
117
|
+
if (options.includeDb) volumes.push(" postgres_data:");
|
|
118
|
+
if (options.https === "letsencrypt") {
|
|
119
|
+
volumes.push(" certbot_data:");
|
|
120
|
+
volumes.push(" certbot_www:");
|
|
121
|
+
}
|
|
122
|
+
if (volumes.length > 0) {
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push("volumes:");
|
|
125
|
+
lines.push(...volumes);
|
|
126
|
+
}
|
|
127
|
+
return lines.join("\n") + "\n";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// cli/templates/env.ts
|
|
131
|
+
function generateEnv(options) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
lines.push("# Argus Web Dashboard Configuration");
|
|
134
|
+
lines.push("# Generated by argus-web init");
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("# Server port");
|
|
137
|
+
lines.push(`PORT=${options.port}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
if (options.includeDb) {
|
|
140
|
+
lines.push("# Database password (used by both PostgreSQL container and web app)");
|
|
141
|
+
lines.push(`DB_PASSWORD=${options.dbPassword}`);
|
|
142
|
+
} else {
|
|
143
|
+
lines.push("# PostgreSQL connection string");
|
|
144
|
+
lines.push(`DATABASE_URL=${options.dbConnectionString || "postgresql://user:password@host:5432/argus"}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("# Path to screenshots directory (mounted read-only into the container)");
|
|
148
|
+
lines.push(`SCREENSHOTS_PATH=${options.screenshotsPath}`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// cli/templates/nginx.ts
|
|
154
|
+
function generateNginxConf(options) {
|
|
155
|
+
const lines = [];
|
|
156
|
+
if (options.https !== "none") {
|
|
157
|
+
lines.push("server {");
|
|
158
|
+
lines.push(" listen 80;");
|
|
159
|
+
lines.push(` server_name ${options.domain};`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
if (options.https === "letsencrypt") {
|
|
162
|
+
lines.push(" location /.well-known/acme-challenge/ {");
|
|
163
|
+
lines.push(" root /var/www/certbot;");
|
|
164
|
+
lines.push(" }");
|
|
165
|
+
lines.push("");
|
|
166
|
+
}
|
|
167
|
+
lines.push(" location / {");
|
|
168
|
+
lines.push(" return 301 https://$server_name$request_uri;");
|
|
169
|
+
lines.push(" }");
|
|
170
|
+
lines.push("}");
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push("server {");
|
|
173
|
+
lines.push(" listen 443 ssl http2;");
|
|
174
|
+
lines.push(` server_name ${options.domain};`);
|
|
175
|
+
lines.push("");
|
|
176
|
+
if (options.https === "letsencrypt") {
|
|
177
|
+
lines.push(` ssl_certificate /etc/letsencrypt/live/${options.domain}/fullchain.pem;`);
|
|
178
|
+
lines.push(` ssl_certificate_key /etc/letsencrypt/live/${options.domain}/privkey.pem;`);
|
|
179
|
+
} else {
|
|
180
|
+
lines.push(" ssl_certificate /etc/nginx/certs/fullchain.pem;");
|
|
181
|
+
lines.push(" ssl_certificate_key /etc/nginx/certs/privkey.pem;");
|
|
182
|
+
}
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(" # SSL settings");
|
|
185
|
+
lines.push(" ssl_protocols TLSv1.2 TLSv1.3;");
|
|
186
|
+
lines.push(" ssl_prefer_server_ciphers off;");
|
|
187
|
+
lines.push("");
|
|
188
|
+
} else {
|
|
189
|
+
lines.push("server {");
|
|
190
|
+
lines.push(" listen 80;");
|
|
191
|
+
lines.push(` server_name ${options.domain};`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
lines.push(" client_max_body_size 50M;");
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push(" location / {");
|
|
197
|
+
lines.push(" proxy_pass http://web:3000;");
|
|
198
|
+
lines.push(" proxy_http_version 1.1;");
|
|
199
|
+
lines.push(" proxy_set_header Upgrade $http_upgrade;");
|
|
200
|
+
lines.push(" proxy_set_header Connection 'upgrade';");
|
|
201
|
+
lines.push(" proxy_set_header Host $host;");
|
|
202
|
+
lines.push(" proxy_set_header X-Real-IP $remote_addr;");
|
|
203
|
+
lines.push(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;");
|
|
204
|
+
lines.push(" proxy_set_header X-Forwarded-Proto $scheme;");
|
|
205
|
+
lines.push(" proxy_cache_bypass $http_upgrade;");
|
|
206
|
+
lines.push(" }");
|
|
207
|
+
lines.push("}");
|
|
208
|
+
return lines.join("\n") + "\n";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// cli/utils/docker.ts
|
|
212
|
+
import { execa } from "execa";
|
|
213
|
+
async function isDockerInstalled() {
|
|
214
|
+
try {
|
|
215
|
+
await execa("docker", ["--version"]);
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function isDockerRunning() {
|
|
222
|
+
try {
|
|
223
|
+
await execa("docker", ["info"], { stdio: "ignore" });
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function dockerCompose(args, cwd, options) {
|
|
230
|
+
return execa("docker", ["compose", ...args], {
|
|
231
|
+
cwd,
|
|
232
|
+
stdio: "inherit",
|
|
233
|
+
...options
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function ensureDocker() {
|
|
237
|
+
if (!await isDockerInstalled()) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"Docker is not installed. Please install Docker first:\nhttps://docs.docker.com/get-docker/"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!await isDockerRunning()) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Docker is not running. Please start Docker Desktop or the Docker daemon."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// cli/commands/init.ts
|
|
250
|
+
async function initCommand(options) {
|
|
251
|
+
p.intro(chalk.bold("Argus Web Dashboard Setup"));
|
|
252
|
+
const outputDir = resolve2(options.dir || DEFAULT_DIR);
|
|
253
|
+
if (existsSync2(resolve2(outputDir, "docker-compose.yml"))) {
|
|
254
|
+
const overwrite = await p.confirm({
|
|
255
|
+
message: `Configuration already exists in ${outputDir}. Overwrite?`,
|
|
256
|
+
initialValue: false
|
|
257
|
+
});
|
|
258
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
259
|
+
p.cancel("Setup cancelled.");
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const hasDocker = await isDockerInstalled();
|
|
264
|
+
if (!hasDocker) {
|
|
265
|
+
p.log.warn(
|
|
266
|
+
"Docker is not installed. You can still generate config files, but you'll need Docker to run Argus."
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const answers = await p.group(
|
|
270
|
+
{
|
|
271
|
+
deployMethod: () => p.select({
|
|
272
|
+
message: "Deployment method",
|
|
273
|
+
options: [
|
|
274
|
+
{ value: "compose", label: "Docker Compose", hint: "recommended" },
|
|
275
|
+
{ value: "manual", label: "Config files only", hint: "generate files without starting" }
|
|
276
|
+
]
|
|
277
|
+
}),
|
|
278
|
+
includeDb: () => p.select({
|
|
279
|
+
message: "PostgreSQL setup",
|
|
280
|
+
options: [
|
|
281
|
+
{ value: true, label: "Include PostgreSQL container", hint: "recommended" },
|
|
282
|
+
{ value: false, label: "External PostgreSQL", hint: "enter connection string" }
|
|
283
|
+
]
|
|
284
|
+
}),
|
|
285
|
+
dbConnectionString: ({ results }) => {
|
|
286
|
+
if (results.includeDb) return Promise.resolve(void 0);
|
|
287
|
+
return p.text({
|
|
288
|
+
message: "PostgreSQL connection string",
|
|
289
|
+
placeholder: "postgresql://user:password@host:5432/argus",
|
|
290
|
+
validate: (value) => {
|
|
291
|
+
if (!value) return "Connection string is required";
|
|
292
|
+
if (!value.startsWith("postgresql://") && !value.startsWith("postgres://")) {
|
|
293
|
+
return "Must start with postgresql:// or postgres://";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
dbPassword: ({ results }) => {
|
|
299
|
+
if (!results.includeDb) return Promise.resolve(DEFAULT_DB_PASSWORD);
|
|
300
|
+
return p.text({
|
|
301
|
+
message: "Database password",
|
|
302
|
+
defaultValue: DEFAULT_DB_PASSWORD,
|
|
303
|
+
placeholder: DEFAULT_DB_PASSWORD
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
port: () => p.text({
|
|
307
|
+
message: "Port",
|
|
308
|
+
defaultValue: String(DEFAULT_PORT),
|
|
309
|
+
placeholder: String(DEFAULT_PORT),
|
|
310
|
+
validate: (value) => {
|
|
311
|
+
const num = parseInt(value, 10);
|
|
312
|
+
if (isNaN(num) || num < 1 || num > 65535) return "Must be a valid port (1-65535)";
|
|
313
|
+
}
|
|
314
|
+
}),
|
|
315
|
+
domain: () => p.text({
|
|
316
|
+
message: "Domain (leave empty for localhost)",
|
|
317
|
+
placeholder: "argus.yourcompany.com"
|
|
318
|
+
}),
|
|
319
|
+
https: ({ results }) => {
|
|
320
|
+
if (!results.domain) return Promise.resolve("none");
|
|
321
|
+
return p.select({
|
|
322
|
+
message: "HTTPS configuration",
|
|
323
|
+
options: [
|
|
324
|
+
{ value: "letsencrypt", label: "Let's Encrypt", hint: "automatic certificates" },
|
|
325
|
+
{ value: "custom", label: "Custom certificate", hint: "provide your own certs" },
|
|
326
|
+
{ value: "none", label: "No HTTPS", hint: "HTTP only" }
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
includeNginx: ({ results }) => {
|
|
331
|
+
if (!results.domain) return Promise.resolve(false);
|
|
332
|
+
return p.select({
|
|
333
|
+
message: "Reverse proxy",
|
|
334
|
+
options: [
|
|
335
|
+
{ value: true, label: "Include Nginx container", hint: "recommended with domain" },
|
|
336
|
+
{ value: false, label: "No reverse proxy", hint: "you manage this yourself" }
|
|
337
|
+
]
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
screenshotsPath: () => p.text({
|
|
341
|
+
message: "Screenshots storage path",
|
|
342
|
+
defaultValue: DEFAULT_SCREENSHOTS_PATH,
|
|
343
|
+
placeholder: DEFAULT_SCREENSHOTS_PATH
|
|
344
|
+
})
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
onCancel: () => {
|
|
348
|
+
p.cancel("Setup cancelled.");
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
const s = p.spinner();
|
|
354
|
+
s.start("Generating configuration files");
|
|
355
|
+
mkdirSync(outputDir, { recursive: true });
|
|
356
|
+
const port = parseInt(answers.port, 10) || DEFAULT_PORT;
|
|
357
|
+
const composeOptions = {
|
|
358
|
+
port,
|
|
359
|
+
includeDb: answers.includeDb,
|
|
360
|
+
dbConnectionString: answers.dbConnectionString,
|
|
361
|
+
dbPassword: answers.dbPassword || DEFAULT_DB_PASSWORD,
|
|
362
|
+
includeNginx: answers.includeNginx,
|
|
363
|
+
domain: answers.domain,
|
|
364
|
+
https: answers.https,
|
|
365
|
+
screenshotsPath: answers.screenshotsPath || DEFAULT_SCREENSHOTS_PATH
|
|
366
|
+
};
|
|
367
|
+
writeFileSync(
|
|
368
|
+
resolve2(outputDir, "docker-compose.yml"),
|
|
369
|
+
generateDockerCompose(composeOptions)
|
|
370
|
+
);
|
|
371
|
+
const envOptions = {
|
|
372
|
+
port,
|
|
373
|
+
includeDb: answers.includeDb,
|
|
374
|
+
dbConnectionString: answers.dbConnectionString,
|
|
375
|
+
dbPassword: answers.dbPassword || DEFAULT_DB_PASSWORD,
|
|
376
|
+
screenshotsPath: answers.screenshotsPath || DEFAULT_SCREENSHOTS_PATH
|
|
377
|
+
};
|
|
378
|
+
writeFileSync(resolve2(outputDir, ".env"), generateEnv(envOptions));
|
|
379
|
+
if (answers.includeNginx && answers.domain) {
|
|
380
|
+
writeFileSync(
|
|
381
|
+
resolve2(outputDir, "nginx.conf"),
|
|
382
|
+
generateNginxConf({
|
|
383
|
+
domain: answers.domain,
|
|
384
|
+
https: answers.https
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
s.stop("Configuration files generated");
|
|
389
|
+
p.note(
|
|
390
|
+
[
|
|
391
|
+
`${chalk.dim("Directory:")} ${outputDir}`,
|
|
392
|
+
`${chalk.dim("Files:")} docker-compose.yml, .env${answers.includeNginx ? ", nginx.conf" : ""}`,
|
|
393
|
+
"",
|
|
394
|
+
`${chalk.dim("Next steps:")}`,
|
|
395
|
+
` cd ${options.dir || DEFAULT_DIR}`,
|
|
396
|
+
" argus-web start"
|
|
397
|
+
].join("\n"),
|
|
398
|
+
"Setup complete"
|
|
399
|
+
);
|
|
400
|
+
if (answers.https === "letsencrypt" && answers.domain) {
|
|
401
|
+
p.log.info(
|
|
402
|
+
`Run this to obtain your initial SSL certificate:
|
|
403
|
+
docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d ${answers.domain}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
p.outro(chalk.green("Happy testing!"));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// cli/commands/start.ts
|
|
410
|
+
import chalk2 from "chalk";
|
|
411
|
+
async function startCommand(options) {
|
|
412
|
+
const cwd = findArgusDir(options.dir);
|
|
413
|
+
await ensureDocker();
|
|
414
|
+
console.log(chalk2.blue("Starting Argus..."));
|
|
415
|
+
await dockerCompose(["up", "-d"], cwd);
|
|
416
|
+
console.log(chalk2.green("Argus is running."));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// cli/commands/stop.ts
|
|
420
|
+
import chalk3 from "chalk";
|
|
421
|
+
async function stopCommand(options) {
|
|
422
|
+
const cwd = findArgusDir(options.dir);
|
|
423
|
+
await ensureDocker();
|
|
424
|
+
console.log(chalk3.blue("Stopping Argus..."));
|
|
425
|
+
await dockerCompose(["down"], cwd);
|
|
426
|
+
console.log(chalk3.green("Argus stopped."));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// cli/commands/logs.ts
|
|
430
|
+
async function logsCommand(options) {
|
|
431
|
+
const cwd = findArgusDir(options.dir);
|
|
432
|
+
await ensureDocker();
|
|
433
|
+
const args = ["logs", "-f"];
|
|
434
|
+
if (options.service) {
|
|
435
|
+
args.push(options.service);
|
|
436
|
+
}
|
|
437
|
+
await dockerCompose(args, cwd);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// cli/commands/status.ts
|
|
441
|
+
import chalk4 from "chalk";
|
|
442
|
+
async function statusCommand(options) {
|
|
443
|
+
const cwd = findArgusDir(options.dir);
|
|
444
|
+
await ensureDocker();
|
|
445
|
+
console.log(chalk4.blue("Argus status:\n"));
|
|
446
|
+
await dockerCompose(["ps"], cwd);
|
|
447
|
+
try {
|
|
448
|
+
const result = await dockerCompose(
|
|
449
|
+
["exec", "web", "wget", "--spider", "--quiet", "http://localhost:3000"],
|
|
450
|
+
cwd,
|
|
451
|
+
{ stdio: "pipe" }
|
|
452
|
+
);
|
|
453
|
+
if (result.exitCode === 0) {
|
|
454
|
+
console.log(chalk4.green("\nWeb dashboard is healthy."));
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
console.log(chalk4.yellow("\nWeb dashboard health check failed \u2014 container may still be starting."));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// cli/commands/upgrade.ts
|
|
462
|
+
import chalk5 from "chalk";
|
|
463
|
+
async function upgradeCommand(options) {
|
|
464
|
+
const cwd = findArgusDir(options.dir);
|
|
465
|
+
await ensureDocker();
|
|
466
|
+
console.log(chalk5.blue("Pulling latest images..."));
|
|
467
|
+
await dockerCompose(["pull"], cwd);
|
|
468
|
+
console.log(chalk5.blue("Restarting with new images..."));
|
|
469
|
+
await dockerCompose(["up", "-d"], cwd);
|
|
470
|
+
console.log(chalk5.green("Argus upgraded successfully."));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// cli/index.ts
|
|
474
|
+
var program = new Command();
|
|
475
|
+
program.name("argus-web").description("Set up and manage the Argus web dashboard").version("0.1.0");
|
|
476
|
+
program.command("init").description("Interactive setup wizard \u2014 generates docker-compose.yml, .env, and nginx.conf").option("-d, --dir <path>", "Output directory", "./argus").action(initCommand);
|
|
477
|
+
program.command("start").description("Start Argus (docker compose up -d)").option("-d, --dir <path>", "Argus directory").action(startCommand);
|
|
478
|
+
program.command("stop").description("Stop Argus (docker compose down)").option("-d, --dir <path>", "Argus directory").action(stopCommand);
|
|
479
|
+
program.command("logs").description("View logs (docker compose logs -f)").option("-d, --dir <path>", "Argus directory").option("-s, --service <name>", "Service name (web, db, nginx)").action(logsCommand);
|
|
480
|
+
program.command("status").description("Check container status and health").option("-d, --dir <path>", "Argus directory").action(statusCommand);
|
|
481
|
+
program.command("upgrade").description("Pull latest images and restart").option("-d, --dir <path>", "Argus directory").action(upgradeCommand);
|
|
482
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@argus-vrt/web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"argus-web": "./dist-cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist-cli/"
|
|
11
|
+
],
|
|
6
12
|
"repository": {
|
|
7
13
|
"type": "git",
|
|
8
14
|
"url": "https://github.com/maxcwolf/argus.git",
|
|
@@ -13,7 +19,8 @@
|
|
|
13
19
|
},
|
|
14
20
|
"scripts": {
|
|
15
21
|
"dev": "vite dev --port 3000",
|
|
16
|
-
"build": "vite build",
|
|
22
|
+
"build": "vite build && tsup --config tsup.cli.config.ts",
|
|
23
|
+
"build:cli": "tsup --config tsup.cli.config.ts",
|
|
17
24
|
"preview": "vite preview",
|
|
18
25
|
"test": "vitest run",
|
|
19
26
|
"db:generate": "drizzle-kit generate",
|
|
@@ -24,36 +31,41 @@
|
|
|
24
31
|
"docker:down": "docker compose down"
|
|
25
32
|
},
|
|
26
33
|
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^0.10.0",
|
|
35
|
+
"chalk": "^5.3.0",
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"execa": "^9.5.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
27
40
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
28
41
|
"@tailwindcss/vite": "^4.0.6",
|
|
42
|
+
"@tanstack/devtools-vite": "^0.3.11",
|
|
29
43
|
"@tanstack/react-devtools": "^0.7.0",
|
|
30
44
|
"@tanstack/react-router": "^1.132.0",
|
|
31
45
|
"@tanstack/react-router-devtools": "^1.132.0",
|
|
32
46
|
"@tanstack/react-router-ssr-query": "^1.131.7",
|
|
33
47
|
"@tanstack/react-start": "^1.132.0",
|
|
34
48
|
"@tanstack/router-plugin": "^1.132.0",
|
|
35
|
-
"clsx": "^2.1.1",
|
|
36
|
-
"drizzle-orm": "^0.45.1",
|
|
37
|
-
"lucide-react": "^0.561.0",
|
|
38
|
-
"postgres": "^3.4.8",
|
|
39
|
-
"react": "^19.2.0",
|
|
40
|
-
"react-dom": "^19.2.0",
|
|
41
|
-
"tailwind-merge": "^2.6.0",
|
|
42
|
-
"tailwindcss": "^4.0.6",
|
|
43
|
-
"vite-tsconfig-paths": "^6.0.2"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@tanstack/devtools-vite": "^0.3.11",
|
|
47
49
|
"@testing-library/dom": "^10.4.0",
|
|
48
50
|
"@testing-library/react": "^16.2.0",
|
|
49
51
|
"@types/node": "^22.10.2",
|
|
50
52
|
"@types/react": "^19.2.0",
|
|
51
53
|
"@types/react-dom": "^19.2.0",
|
|
52
54
|
"@vitejs/plugin-react": "^5.0.4",
|
|
55
|
+
"clsx": "^2.1.1",
|
|
53
56
|
"drizzle-kit": "^0.31.8",
|
|
57
|
+
"drizzle-orm": "^0.45.1",
|
|
54
58
|
"jsdom": "^27.0.0",
|
|
59
|
+
"lucide-react": "^0.561.0",
|
|
60
|
+
"postgres": "^3.4.8",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"react-dom": "^19.2.0",
|
|
63
|
+
"tailwind-merge": "^2.6.0",
|
|
64
|
+
"tailwindcss": "^4.0.6",
|
|
65
|
+
"tsup": "^8.0.0",
|
|
55
66
|
"typescript": "^5.7.2",
|
|
56
67
|
"vite": "^7.1.7",
|
|
68
|
+
"vite-tsconfig-paths": "^6.0.2",
|
|
57
69
|
"vitest": "^3.0.5",
|
|
58
70
|
"web-vitals": "^5.1.0"
|
|
59
71
|
}
|
package/.cta.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"projectName": "web",
|
|
3
|
-
"mode": "file-router",
|
|
4
|
-
"typescript": true,
|
|
5
|
-
"tailwind": true,
|
|
6
|
-
"packageManager": "yarn",
|
|
7
|
-
"git": false,
|
|
8
|
-
"install": true,
|
|
9
|
-
"addOnOptions": {},
|
|
10
|
-
"version": 1,
|
|
11
|
-
"framework": "react-cra",
|
|
12
|
-
"chosenAddOns": [
|
|
13
|
-
"start"
|
|
14
|
-
]
|
|
15
|
-
}
|