@argus-vrt/web 0.1.1 → 0.2.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 CHANGED
@@ -22,6 +22,20 @@ The `init` wizard generates a `docker-compose.yml`, `.env`, and optionally `ngin
22
22
 
23
23
  - [Docker](https://docs.docker.com/get-docker/) installed and running
24
24
  - Node.js >= 20 (for running the CLI via npx)
25
+ - A [GitHub OAuth App](https://github.com/settings/developers) for authentication
26
+
27
+ ## Authentication
28
+
29
+ Argus requires GitHub OAuth for login. All browser routes are protected — unauthenticated users are redirected to GitHub to sign in.
30
+
31
+ The `/api/upload` endpoint used by CI/CD is authenticated separately via API key (`Authorization: Bearer <key>` header), so pipelines don't need GitHub credentials.
32
+
33
+ ### Setting Up GitHub OAuth
34
+
35
+ 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) → **New OAuth App**
36
+ 2. Set the **Authorization callback URL** to `https://your-domain.com/auth/github/callback` (or `http://localhost:3000/auth/github/callback` for local development)
37
+ 3. Save the **Client ID** and generate a **Client Secret**
38
+ 4. Provide these values when running `npx @argus-vrt/web init`
25
39
 
26
40
  ## Commands
27
41
 
@@ -35,6 +49,11 @@ Interactive setup wizard. Prompts for:
35
49
  - **HTTPS** — Let's Encrypt, custom certificate, or none
36
50
  - **Reverse proxy** — include an Nginx container or manage yourself
37
51
  - **Screenshots path** — where uploaded screenshots are stored
52
+ - **GitHub Client ID** — from your GitHub OAuth App
53
+ - **GitHub Client Secret** — from your GitHub OAuth App
54
+ - **API key** — for CI/CD uploads (auto-generated by default)
55
+
56
+ A **session secret** is auto-generated and does not require input.
38
57
 
39
58
  Generates configuration files into `./argus/` (or `--dir <path>`).
40
59
 
@@ -63,13 +82,17 @@ Show container status and run a health check on the web dashboard.
63
82
 
64
83
  Pull the latest Docker images and restart containers.
65
84
 
85
+ ### `npx @argus-vrt/web setup-ssl <domain>`
86
+
87
+ Obtain a Let's Encrypt SSL certificate for your domain. Requires the Nginx and Certbot containers (select Let's Encrypt during `init`).
88
+
66
89
  ```bash
67
- npx @argus-vrt/web upgrade
90
+ npx @argus-vrt/web setup-ssl argus.yourcompany.com
68
91
  ```
69
92
 
70
93
  ## Options
71
94
 
72
- All management commands (`start`, `stop`, `logs`, `status`, `upgrade`) accept:
95
+ All management commands (`start`, `stop`, `logs`, `status`, `upgrade`, `setup-ssl`) accept:
73
96
 
74
97
  | Flag | Description |
75
98
  |------|-------------|
@@ -77,11 +100,12 @@ All management commands (`start`, `stop`, `logs`, `status`, `upgrade`) accept:
77
100
 
78
101
  ## Connecting the CLI
79
102
 
80
- Once the dashboard is running, point the testing CLI at it by adding `apiUrl` to your project's `.argus.json`:
103
+ Once the dashboard is running, point the testing CLI at it by adding `apiUrl` and `apiKey` to your project's `.argus.json`:
81
104
 
82
105
  ```json
83
106
  {
84
- "apiUrl": "http://localhost:3000"
107
+ "apiUrl": "http://localhost:3000",
108
+ "apiKey": "your-api-key-from-init"
85
109
  }
86
110
  ```
87
111
 
@@ -91,9 +115,18 @@ Then upload results after running tests:
91
115
  npx argus test
92
116
  ```
93
117
 
118
+ Or upload manually with a header:
119
+
120
+ ```bash
121
+ curl -X POST https://your-domain.com/api/upload \
122
+ -H "Authorization: Bearer your-api-key" \
123
+ -H "Content-Type: application/json" \
124
+ -d '{"branch":"main","commitHash":"abc123","stories":[...]}'
125
+ ```
126
+
94
127
  ## Environment Variables
95
128
 
96
- The generated `.env` file supports:
129
+ The generated `.env` file includes:
97
130
 
98
131
  | Variable | Default | Description |
99
132
  |----------|---------|-------------|
@@ -101,6 +134,10 @@ The generated `.env` file supports:
101
134
  | `DATABASE_URL` | (auto) | PostgreSQL connection string (external DB only) |
102
135
  | `DB_PASSWORD` | `argus` | Database password (container DB only) |
103
136
  | `SCREENSHOTS_PATH` | `./argus-data/images` | Path to screenshots directory |
137
+ | `GITHUB_CLIENT_ID` | — | GitHub OAuth App client ID |
138
+ | `GITHUB_CLIENT_SECRET` | — | GitHub OAuth App client secret |
139
+ | `SESSION_SECRET` | (auto-generated) | Encryption key for session cookies |
140
+ | `ARGUS_API_KEY` | (auto-generated) | API key for CI/CD upload authentication |
104
141
 
105
142
  ## Docker Image
106
143
 
package/dist-cli/index.js CHANGED
@@ -5,11 +5,13 @@ import { Command } from "commander";
5
5
 
6
6
  // cli/commands/init.ts
7
7
  import * as p from "@clack/prompts";
8
- import chalk from "chalk";
8
+ import chalk3 from "chalk";
9
+ import { randomBytes } from "crypto";
9
10
  import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
10
11
  import { resolve as resolve2 } from "path";
11
12
 
12
13
  // cli/utils/config.ts
14
+ import chalk from "chalk";
13
15
  import { existsSync } from "fs";
14
16
  import { resolve } from "path";
15
17
  var DOCKER_IMAGE = "ghcr.io/maxcwolf/argus-web";
@@ -20,10 +22,11 @@ var DEFAULT_SCREENSHOTS_PATH = "./argus-data/images";
20
22
  function findArgusDir(dir) {
21
23
  const target = dir ? resolve(dir) : resolve(process.cwd(), DEFAULT_DIR);
22
24
  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.`
25
+ console.error(
26
+ `${chalk.red("Error:")} No docker-compose.yml found in ${target}
27
+ Run ${chalk.cyan("argus-web init")} first to set up Argus.`
26
28
  );
29
+ process.exit(1);
27
30
  }
28
31
  return target;
29
32
  }
@@ -51,6 +54,10 @@ function generateDockerCompose(options) {
51
54
  lines.push(" - DATABASE_URL=${DATABASE_URL}");
52
55
  }
53
56
  lines.push(" - NODE_ENV=production");
57
+ lines.push(" - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}");
58
+ lines.push(" - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}");
59
+ lines.push(" - SESSION_SECRET=${SESSION_SECRET}");
60
+ lines.push(" - ARGUS_API_KEY=${ARGUS_API_KEY}");
54
61
  if (options.includeDb) {
55
62
  lines.push(" depends_on:");
56
63
  lines.push(" db:");
@@ -147,6 +154,16 @@ function generateEnv(options) {
147
154
  lines.push("# Path to screenshots directory (mounted read-only into the container)");
148
155
  lines.push(`SCREENSHOTS_PATH=${options.screenshotsPath}`);
149
156
  lines.push("");
157
+ lines.push("# GitHub OAuth (create app at https://github.com/settings/developers)");
158
+ lines.push(`GITHUB_CLIENT_ID=${options.githubClientId}`);
159
+ lines.push(`GITHUB_CLIENT_SECRET=${options.githubClientSecret}`);
160
+ lines.push("");
161
+ lines.push("# Session encryption key (auto-generated)");
162
+ lines.push(`SESSION_SECRET=${options.sessionSecret}`);
163
+ lines.push("");
164
+ lines.push("# API key for CI/CD uploads (Authorization: Bearer <key>)");
165
+ lines.push(`ARGUS_API_KEY=${options.apiKey}`);
166
+ lines.push("");
150
167
  return lines.join("\n");
151
168
  }
152
169
 
@@ -209,6 +226,7 @@ function generateNginxConf(options) {
209
226
  }
210
227
 
211
228
  // cli/utils/docker.ts
229
+ import chalk2 from "chalk";
212
230
  import { execa } from "execa";
213
231
  async function isDockerInstalled() {
214
232
  try {
@@ -227,28 +245,41 @@ async function isDockerRunning() {
227
245
  }
228
246
  }
229
247
  async function dockerCompose(args, cwd, options) {
230
- return execa("docker", ["compose", ...args], {
231
- cwd,
232
- stdio: "inherit",
233
- ...options
234
- });
248
+ try {
249
+ return await execa("docker", ["compose", ...args], {
250
+ cwd,
251
+ stdio: "inherit",
252
+ ...options
253
+ });
254
+ } catch (error) {
255
+ const cmd = `docker compose ${args.join(" ")}`;
256
+ const exitCode = error && typeof error === "object" && "exitCode" in error ? error.exitCode : 1;
257
+ console.error(
258
+ `
259
+ ${chalk2.red("Error:")} ${chalk2.dim(cmd)} exited with code ${exitCode}`
260
+ );
261
+ process.exit(exitCode);
262
+ }
235
263
  }
236
264
  async function ensureDocker() {
237
265
  if (!await isDockerInstalled()) {
238
- throw new Error(
239
- "Docker is not installed. Please install Docker first:\nhttps://docs.docker.com/get-docker/"
266
+ console.error(
267
+ `${chalk2.red("Error:")} Docker is not installed.
268
+ Install it from: https://docs.docker.com/get-docker/`
240
269
  );
270
+ process.exit(1);
241
271
  }
242
272
  if (!await isDockerRunning()) {
243
- throw new Error(
244
- "Docker is not running. Please start Docker Desktop or the Docker daemon."
273
+ console.error(
274
+ `${chalk2.red("Error:")} Docker is not running. Please start Docker Desktop or the Docker daemon.`
245
275
  );
276
+ process.exit(1);
246
277
  }
247
278
  }
248
279
 
249
280
  // cli/commands/init.ts
250
281
  async function initCommand(options) {
251
- p.intro(chalk.bold("Argus Web Dashboard Setup"));
282
+ p.intro(chalk3.bold("Argus Web Dashboard Setup"));
252
283
  const outputDir = resolve2(options.dir || DEFAULT_DIR);
253
284
  if (existsSync2(resolve2(outputDir, "docker-compose.yml"))) {
254
285
  const overwrite = await p.confirm({
@@ -341,6 +372,23 @@ async function initCommand(options) {
341
372
  message: "Screenshots storage path",
342
373
  defaultValue: DEFAULT_SCREENSHOTS_PATH,
343
374
  placeholder: DEFAULT_SCREENSHOTS_PATH
375
+ }),
376
+ githubClientId: () => p.text({
377
+ message: "GitHub OAuth Client ID",
378
+ validate: (value) => {
379
+ if (!value) return "Client ID is required for authentication";
380
+ }
381
+ }),
382
+ githubClientSecret: () => p.password({
383
+ message: "GitHub OAuth Client Secret",
384
+ validate: (value) => {
385
+ if (!value) return "Client Secret is required for authentication";
386
+ }
387
+ }),
388
+ apiKey: () => p.text({
389
+ message: "API key for CI/CD uploads",
390
+ defaultValue: randomBytes(32).toString("hex"),
391
+ placeholder: "auto-generated"
344
392
  })
345
393
  },
346
394
  {
@@ -350,6 +398,18 @@ async function initCommand(options) {
350
398
  }
351
399
  }
352
400
  );
401
+ const sessionSecret = randomBytes(32).toString("hex");
402
+ const callbackUrl = answers.domain ? `https://${answers.domain}/auth/github/callback` : `http://localhost:${parseInt(answers.port, 10) || DEFAULT_PORT}/auth/github/callback`;
403
+ p.note(
404
+ [
405
+ `Create a GitHub OAuth App at:`,
406
+ ` ${chalk3.cyan("https://github.com/settings/developers")}`,
407
+ ``,
408
+ `Set the callback URL to:`,
409
+ ` ${chalk3.cyan(callbackUrl)}`
410
+ ].join("\n"),
411
+ "GitHub OAuth Setup"
412
+ );
353
413
  const s = p.spinner();
354
414
  s.start("Generating configuration files");
355
415
  mkdirSync(outputDir, { recursive: true });
@@ -373,7 +433,11 @@ async function initCommand(options) {
373
433
  includeDb: answers.includeDb,
374
434
  dbConnectionString: answers.dbConnectionString,
375
435
  dbPassword: answers.dbPassword || DEFAULT_DB_PASSWORD,
376
- screenshotsPath: answers.screenshotsPath || DEFAULT_SCREENSHOTS_PATH
436
+ screenshotsPath: answers.screenshotsPath || DEFAULT_SCREENSHOTS_PATH,
437
+ githubClientId: answers.githubClientId,
438
+ githubClientSecret: answers.githubClientSecret,
439
+ sessionSecret,
440
+ apiKey: answers.apiKey
377
441
  };
378
442
  writeFileSync(resolve2(outputDir, ".env"), generateEnv(envOptions));
379
443
  if (answers.includeNginx && answers.domain) {
@@ -386,44 +450,40 @@ async function initCommand(options) {
386
450
  );
387
451
  }
388
452
  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
- );
453
+ const dir = options.dir || DEFAULT_DIR;
454
+ const nextSteps = [
455
+ `${chalk3.dim("Directory:")} ${outputDir}`,
456
+ `${chalk3.dim("Files:")} docker-compose.yml, .env${answers.includeNginx ? ", nginx.conf" : ""}`,
457
+ "",
458
+ `${chalk3.dim("Next steps:")}`,
459
+ ` cd ${dir}`
460
+ ];
461
+ nextSteps.push(" argus-web start");
400
462
  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
- );
463
+ nextSteps.push(` argus-web setup-ssl ${answers.domain}`);
405
464
  }
406
- p.outro(chalk.green("Happy testing!"));
465
+ p.note(nextSteps.join("\n"), "Setup complete");
466
+ p.outro(chalk3.green("Happy testing!"));
407
467
  }
408
468
 
409
469
  // cli/commands/start.ts
410
- import chalk2 from "chalk";
470
+ import chalk4 from "chalk";
411
471
  async function startCommand(options) {
412
472
  const cwd = findArgusDir(options.dir);
413
473
  await ensureDocker();
414
- console.log(chalk2.blue("Starting Argus..."));
474
+ console.log(chalk4.blue("Starting Argus..."));
415
475
  await dockerCompose(["up", "-d"], cwd);
416
- console.log(chalk2.green("Argus is running."));
476
+ console.log(chalk4.green("Argus is running."));
417
477
  }
418
478
 
419
479
  // cli/commands/stop.ts
420
- import chalk3 from "chalk";
480
+ import chalk5 from "chalk";
421
481
  async function stopCommand(options) {
422
482
  const cwd = findArgusDir(options.dir);
423
483
  await ensureDocker();
424
- console.log(chalk3.blue("Stopping Argus..."));
484
+ console.log(chalk5.blue("Stopping Argus..."));
425
485
  await dockerCompose(["down"], cwd);
426
- console.log(chalk3.green("Argus stopped."));
486
+ console.log(chalk5.green("Argus stopped."));
427
487
  }
428
488
 
429
489
  // cli/commands/logs.ts
@@ -438,11 +498,11 @@ async function logsCommand(options) {
438
498
  }
439
499
 
440
500
  // cli/commands/status.ts
441
- import chalk4 from "chalk";
501
+ import chalk6 from "chalk";
442
502
  async function statusCommand(options) {
443
503
  const cwd = findArgusDir(options.dir);
444
504
  await ensureDocker();
445
- console.log(chalk4.blue("Argus status:\n"));
505
+ console.log(chalk6.blue("Argus status:\n"));
446
506
  await dockerCompose(["ps"], cwd);
447
507
  try {
448
508
  const result = await dockerCompose(
@@ -451,23 +511,48 @@ async function statusCommand(options) {
451
511
  { stdio: "pipe" }
452
512
  );
453
513
  if (result.exitCode === 0) {
454
- console.log(chalk4.green("\nWeb dashboard is healthy."));
514
+ console.log(chalk6.green("\nWeb dashboard is healthy."));
455
515
  }
456
516
  } catch {
457
- console.log(chalk4.yellow("\nWeb dashboard health check failed \u2014 container may still be starting."));
517
+ console.log(chalk6.yellow("\nWeb dashboard health check failed \u2014 container may still be starting."));
458
518
  }
459
519
  }
460
520
 
461
521
  // cli/commands/upgrade.ts
462
- import chalk5 from "chalk";
522
+ import chalk7 from "chalk";
463
523
  async function upgradeCommand(options) {
464
524
  const cwd = findArgusDir(options.dir);
465
525
  await ensureDocker();
466
- console.log(chalk5.blue("Pulling latest images..."));
526
+ console.log(chalk7.blue("Pulling latest images..."));
467
527
  await dockerCompose(["pull"], cwd);
468
- console.log(chalk5.blue("Restarting with new images..."));
528
+ console.log(chalk7.blue("Restarting with new images..."));
469
529
  await dockerCompose(["up", "-d"], cwd);
470
- console.log(chalk5.green("Argus upgraded successfully."));
530
+ console.log(chalk7.green("Argus upgraded successfully."));
531
+ }
532
+
533
+ // cli/commands/setup-ssl.ts
534
+ import chalk8 from "chalk";
535
+ async function setupSslCommand(domain, options) {
536
+ const cwd = findArgusDir(options.dir);
537
+ await ensureDocker();
538
+ console.log(chalk8.blue(`Obtaining SSL certificate for ${domain}...`));
539
+ await dockerCompose(
540
+ [
541
+ "run",
542
+ "--rm",
543
+ "certbot",
544
+ "certonly",
545
+ "--webroot",
546
+ "-w",
547
+ "/var/www/certbot",
548
+ "-d",
549
+ domain
550
+ ],
551
+ cwd
552
+ );
553
+ console.log(chalk8.blue("Restarting nginx..."));
554
+ await dockerCompose(["restart", "nginx"], cwd);
555
+ console.log(chalk8.green("SSL certificate installed successfully."));
471
556
  }
472
557
 
473
558
  // cli/index.ts
@@ -479,4 +564,5 @@ program.command("stop").description("Stop Argus (docker compose down)").option("
479
564
  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
565
  program.command("status").description("Check container status and health").option("-d, --dir <path>", "Argus directory").action(statusCommand);
481
566
  program.command("upgrade").description("Pull latest images and restart").option("-d, --dir <path>", "Argus directory").action(upgradeCommand);
567
+ program.command("setup-ssl").description("Obtain a Let's Encrypt SSL certificate").argument("<domain>", "Domain to obtain certificate for").option("-d, --dir <path>", "Argus directory").action(setupSslCommand);
482
568
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@argus-vrt/web",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,6 +55,7 @@
55
55
  "clsx": "^2.1.1",
56
56
  "drizzle-kit": "^0.31.8",
57
57
  "drizzle-orm": "^0.45.1",
58
+ "iron-session": "^8.0.4",
58
59
  "jsdom": "^27.0.0",
59
60
  "lucide-react": "^0.561.0",
60
61
  "postgres": "^3.4.8",