deploro 0.2.1 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +254 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { createRequire } from "module";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/commands/login.ts
@@ -148,6 +149,11 @@ async function logoutCommand() {
148
149
  }
149
150
 
150
151
  // src/lib/api.ts
152
+ function errorMessage(status, body, statusText) {
153
+ const parts = [body.error ?? statusText];
154
+ if (body.detail && body.detail !== body.error) parts.push(body.detail);
155
+ return `${status} ${parts.join(" \u2014 ")}`;
156
+ }
151
157
  async function apiRequest(path, options, configOverride) {
152
158
  const config = configOverride ?? requireConfig();
153
159
  const headers = {
@@ -159,7 +165,7 @@ async function apiRequest(path, options, configOverride) {
159
165
  const res = await fetch(`${config.baseUrl}${path}`, { ...options, headers });
160
166
  if (!res.ok) {
161
167
  const body = await res.json().catch(() => ({ error: res.statusText }));
162
- throw new Error(`${res.status} ${body.error ?? res.statusText}`);
168
+ throw new Error(errorMessage(res.status, body, res.statusText));
163
169
  }
164
170
  return res.json();
165
171
  }
@@ -175,7 +181,7 @@ async function apiRequestNoProject(path, options) {
175
181
  });
176
182
  if (!res.ok) {
177
183
  const body = await res.json().catch(() => ({ error: res.statusText }));
178
- throw new Error(`${res.status} ${body.error ?? res.statusText}`);
184
+ throw new Error(errorMessage(res.status, body, res.statusText));
179
185
  }
180
186
  return res.json();
181
187
  }
@@ -226,6 +232,44 @@ function json(data) {
226
232
  }
227
233
 
228
234
  // src/commands/db.ts
235
+ async function dbCreateCommand() {
236
+ const config = requireProject();
237
+ const data = await apiRequestNoProject(
238
+ `/api/projects/${config.projectId}/database`,
239
+ { method: "POST" }
240
+ );
241
+ console.log(`\u2713 Database ready: ${data.database_name}`);
242
+ }
243
+ async function dbInfoCommand() {
244
+ const config = requireProject();
245
+ const { projects } = await apiRequestNoProject("/api/projects");
246
+ const project = projects.find((p) => p.id === config.projectId);
247
+ if (!project) {
248
+ console.error("Active project not found or not visible to this token. Run: deploro projects");
249
+ process.exit(1);
250
+ }
251
+ console.log("\n\u2500\u2500 DATABASE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
252
+ console.log(` Project : ${project.slug}`);
253
+ console.log(` Database name : ${project.database_name ?? "(not provisioned \u2014 run: deploro db create)"}`);
254
+ console.log("");
255
+ if (project.database_name) {
256
+ console.log(
257
+ "Direct PostgreSQL access (host/port/user/password) is not available.\nThe database lives on an internal network reachable only by Deploro's own\nSQL proxy \u2014 and Cloudflare Workers can't open raw TCP sockets regardless of\nnetwork reachability, so a deployed Worker can never use a connection string\neven if one were issued. There is no DATABASE_URL / HYPERDRIVE binding to\nconfigure on your deployed app; none is provisioned automatically.\n"
258
+ );
259
+ console.log(
260
+ "For a deployed Worker to read/write this project's data at runtime, call\nthe per-project REST API instead (every table gets one automatically):\n"
261
+ );
262
+ console.log(` Base URL : ${config.baseUrl}/api/projects/${config.projectId}/studio`);
263
+ console.log(` Spec : ${config.baseUrl}/api/projects/${config.projectId}/studio/spec (OpenAPI 3.1)`);
264
+ console.log(` Auth : Authorization: Bearer <project-scoped PAT>`);
265
+ console.log(` create one with: deploro token create <name> --project ${project.slug}`);
266
+ console.log("");
267
+ console.log("To hand that to your deployed Worker as secrets:");
268
+ console.log(` deploro hosting set-env DEPLORO_API_URL "${config.baseUrl}/api/projects/${config.projectId}/studio"`);
269
+ console.log(` deploro hosting set-env DEPLORO_API_TOKEN "<pat>"`);
270
+ console.log("");
271
+ }
272
+ }
229
273
  async function dbTablesCommand() {
230
274
  requireProject();
231
275
  const data = await apiRequest("/api/db/tables");
@@ -319,6 +363,9 @@ async function dbDropTableCommand(tableName, opts) {
319
363
  }
320
364
 
321
365
  // src/commands/auth.ts
366
+ function isPatToken(accessToken) {
367
+ return accessToken.startsWith("gallium_pat_") || accessToken.startsWith("deploro_pat_");
368
+ }
322
369
  async function authUsersCommand() {
323
370
  requireConfig();
324
371
  const data = await apiRequest("/api/auth/users");
@@ -345,30 +392,188 @@ async function authInviteCommand(email, opts) {
345
392
  }
346
393
  }
347
394
  async function authWhoamiCommand() {
395
+ requireConfig();
396
+ const data = await apiRequestNoProject("/api/auth/whoami");
397
+ if (data.authMethod === "pat") {
398
+ console.log("Authenticated via personal access token.");
399
+ console.log(` User id : ${data.userId}`);
400
+ console.log(` Role : ${data.role}`);
401
+ console.log(` Scopes : ${(data.scopes ?? []).join(", ") || "(none)"}`);
402
+ console.log(` Scoped to ${data.projects?.length ?? 0} project(s):`);
403
+ if (data.projects && data.projects.length > 0) {
404
+ table(data.projects.map((p) => ({ id: p.id, slug: p.slug, name: p.name })));
405
+ } else {
406
+ console.log(" (none \u2014 this token cannot access any project)");
407
+ }
408
+ } else {
409
+ console.log("Authenticated via browser session.");
410
+ console.log(` User id : ${data.userId}`);
411
+ console.log(` Role : ${data.role}`);
412
+ }
413
+ }
414
+ function requireSessionAuth(config) {
415
+ if (isPatToken(config.accessToken)) {
416
+ console.error(
417
+ "Managing tokens requires a browser-session login, not a personal access token.\nRun 'deploro login' without --token (email + password) and try again."
418
+ );
419
+ process.exit(1);
420
+ }
421
+ }
422
+ async function tokenListCommand() {
348
423
  const config = requireConfig();
349
- if (!config.accessToken.startsWith("gallium_pat_") && !config.accessToken.startsWith("deploro_pat_")) {
350
- const res = await fetch(`${config.baseUrl}/api/auth/session`, {
351
- headers: {
352
- Authorization: `Bearer ${config.accessToken}`,
353
- Cookie: `deploro_session=${config.accessToken}`
354
- }
355
- });
356
- const body = await res.json().catch(() => null);
357
- if (body?.data?.user) {
358
- const u = body.data.user;
359
- table([{ id: u.id, name: u.name, email: u.email, role: u.role }]);
360
- if (body.data.session?.expiresAt) {
361
- console.log(`Session expires: ${new Date(body.data.session.expiresAt).toLocaleString()}`);
362
- }
363
- return;
424
+ requireSessionAuth(config);
425
+ const data = await apiRequestNoProject("/api/auth/tokens");
426
+ table(
427
+ data.tokens.map((t) => ({
428
+ id: t.id,
429
+ name: t.name,
430
+ prefix: `${t.token_prefix}\u2026`,
431
+ scopes: t.scopes.join(","),
432
+ projects: t.project_ids.length,
433
+ expires: new Date(t.expires_at).toLocaleDateString(),
434
+ status: t.revoked_at ? "revoked" : "active"
435
+ }))
436
+ );
437
+ }
438
+ async function tokenCreateCommand(name, opts) {
439
+ const config = requireConfig();
440
+ requireSessionAuth(config);
441
+ const scopes = !opts.read && !opts.write ? ["read"] : [opts.read && "read", opts.write && "write"].filter(Boolean);
442
+ let projectIds;
443
+ if (opts.project) {
444
+ const list = await apiRequestNoProject(
445
+ "/api/projects"
446
+ );
447
+ const wanted = opts.project.split(",").map((s) => s.trim().toLowerCase());
448
+ projectIds = list.projects.filter((p) => wanted.includes(p.id) || wanted.includes(p.slug.toLowerCase()) || wanted.includes(p.name.toLowerCase())).map((p) => p.id);
449
+ if (projectIds.length === 0) {
450
+ console.error(`No project matches "${opts.project}". Run: deploro projects`);
451
+ process.exit(1);
364
452
  }
365
- console.error("Session is expired or invalid. Run: deploro login");
453
+ } else if (config.projectId) {
454
+ projectIds = [config.projectId];
455
+ } else {
456
+ console.error("No project specified. Pass --project <slug> or set an active project with 'deploro use'.");
366
457
  process.exit(1);
367
458
  }
368
- const data = await apiRequestNoProject("/api/projects");
369
- console.log("Authenticated via personal access token.");
370
- console.log(` Worker : ${config.baseUrl}`);
371
- console.log(` Projects : ${data.projects.length} visible (the token is scoped to the projects chosen at creation)`);
459
+ const days = Number(opts.days ?? 90);
460
+ const data = await apiRequestNoProject(
461
+ "/api/auth/tokens",
462
+ { method: "POST", body: JSON.stringify({ name, scopes, projectIds, expiresInDays: days }) }
463
+ );
464
+ console.log("\u2713 Token created \u2014 copy it now, it will not be shown again:");
465
+ console.log(`
466
+ ${data.token}
467
+ `);
468
+ console.log(` Scopes : ${scopes.join(", ")}`);
469
+ console.log(` Projects: ${projectIds.length}`);
470
+ console.log(` Expires : in ${data.expiresInDays} days`);
471
+ console.log(`
472
+ Use it with: deploro login --token ${data.token}`);
473
+ }
474
+ async function tokenRevokeCommand(id, opts) {
475
+ const config = requireConfig();
476
+ requireSessionAuth(config);
477
+ await confirmOrDie(`Revoke token ${id}?`, opts.yes);
478
+ await apiRequestNoProject(`/api/auth/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
479
+ console.log(`\u2713 Token ${id} revoked`);
480
+ }
481
+
482
+ // src/commands/migrate.ts
483
+ import { readFileSync as readFileSync2 } from "fs";
484
+ function readSqlOpt(inline, file, label) {
485
+ if (file) {
486
+ try {
487
+ return readFileSync2(file, "utf-8");
488
+ } catch (err) {
489
+ console.error(`Could not read --${label}-file "${file}": ${err instanceof Error ? err.message : String(err)}`);
490
+ process.exit(1);
491
+ }
492
+ }
493
+ return inline;
494
+ }
495
+ async function migrateCreateCommand(name, opts) {
496
+ requireProject();
497
+ const sqlUp = readSqlOpt(opts.up, opts.upFile, "up");
498
+ if (!sqlUp) {
499
+ console.error('Migration SQL is required: pass --up "<sql>" or --up-file <path>');
500
+ process.exit(1);
501
+ }
502
+ const sqlDown = readSqlOpt(opts.down, opts.downFile, "down");
503
+ const config = requireProject();
504
+ const data = await apiRequest(
505
+ `/api/projects/${config.projectId}/migrations`,
506
+ { method: "POST", body: JSON.stringify({ name, sql_up: sqlUp, sql_down: sqlDown }) }
507
+ );
508
+ console.log(`\u2713 Migration created: ${data.migration.name} (${data.migration.id})`);
509
+ console.log(` Apply it with: deploro migrate apply ${data.migration.id}`);
510
+ }
511
+ async function migrateListCommand() {
512
+ const config = requireProject();
513
+ const data = await apiRequest(`/api/projects/${config.projectId}/migrations`);
514
+ table(
515
+ data.migrations.map((m) => ({
516
+ id: m.id,
517
+ name: m.name,
518
+ status: m.applied_at ? "applied" : "pending",
519
+ applied_at: m.applied_at ? new Date(m.applied_at).toLocaleString() : "\u2014",
520
+ created_at: new Date(m.created_at).toLocaleDateString()
521
+ }))
522
+ );
523
+ }
524
+ async function resolveMigration(idOrName) {
525
+ const config = requireProject();
526
+ const data = await apiRequest(`/api/projects/${config.projectId}/migrations`);
527
+ const matches = data.migrations.filter((m) => m.id === idOrName || m.name === idOrName);
528
+ if (matches.length === 0) {
529
+ console.error(`No migration matches "${idOrName}". Run: deploro migrate list`);
530
+ process.exit(1);
531
+ }
532
+ if (matches.length > 1) {
533
+ console.error(`"${idOrName}" is ambiguous (matches ${matches.length} migrations by name) \u2014 use the id.`);
534
+ process.exit(1);
535
+ }
536
+ return { config, migration: matches[0] };
537
+ }
538
+ async function migrateApplyCommand(idOrName) {
539
+ const { config, migration } = await resolveMigration(idOrName);
540
+ if (migration.applied_at) {
541
+ console.error(`Migration "${migration.name}" is already applied.`);
542
+ process.exit(1);
543
+ }
544
+ const data = await apiRequest(
545
+ `/api/projects/${config.projectId}/migrations/${migration.id}/apply`,
546
+ { method: "POST" }
547
+ );
548
+ console.log(`\u2713 Applied: ${data.migration.name}`);
549
+ console.log(` Applied at: ${data.migration.applied_at ? new Date(data.migration.applied_at).toLocaleString() : "(unknown)"}`);
550
+ }
551
+ async function migrateRollbackCommand(idOrName, opts) {
552
+ const { config, migration } = await resolveMigration(idOrName);
553
+ if (!migration.applied_at) {
554
+ console.error(`Migration "${migration.name}" has not been applied.`);
555
+ process.exit(1);
556
+ }
557
+ if (!migration.sql_down) {
558
+ console.error(`Migration "${migration.name}" has no rollback SQL defined.`);
559
+ process.exit(1);
560
+ }
561
+ await confirmOrDie(`Roll back "${migration.name}"? This runs its sql_down against the project database.`, opts.yes);
562
+ const data = await apiRequest(
563
+ `/api/projects/${config.projectId}/migrations/${migration.id}/rollback`,
564
+ { method: "POST" }
565
+ );
566
+ console.log(`\u2713 Rolled back: ${data.migration.name}`);
567
+ }
568
+ async function migrateDeleteCommand(idOrName, opts) {
569
+ const { config, migration } = await resolveMigration(idOrName);
570
+ if (migration.applied_at) {
571
+ console.error(`"${migration.name}" is applied \u2014 roll it back first: deploro migrate rollback ${migration.id}`);
572
+ process.exit(1);
573
+ }
574
+ await confirmOrDie(`Delete unapplied migration "${migration.name}"?`, opts.yes);
575
+ await apiRequest(`/api/projects/${config.projectId}/migrations/${migration.id}`, { method: "DELETE" });
576
+ console.log(`\u2713 Migration "${migration.name}" deleted`);
372
577
  }
373
578
 
374
579
  // src/commands/hosting.ts
@@ -416,6 +621,17 @@ async function hostingSetEnvCommand(key, value) {
416
621
  });
417
622
  console.log(`\u2713 Secret '${key}' updated`);
418
623
  }
624
+ async function hostingEnvCommand() {
625
+ requireConfig();
626
+ const data = await apiRequest("/api/hosting/env");
627
+ if (data.secrets.length === 0) {
628
+ console.log("No secrets set on the active project's Worker.");
629
+ return;
630
+ }
631
+ console.log("\nSecrets set on the active project's Worker (values are never returned by the API):\n");
632
+ for (const s of data.secrets) console.log(` ${s.name}`);
633
+ console.log("");
634
+ }
419
635
  var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
420
636
  function spark(values) {
421
637
  if (values.length === 0) return "(no data)";
@@ -589,8 +805,9 @@ async function deployLogsCommand(deploymentId) {
589
805
  }
590
806
 
591
807
  // src/index.ts
808
+ var pkg = createRequire(import.meta.url)("../package.json");
592
809
  var program = new Command();
593
- program.name("deploro").description("Deploro developer platform CLI").version("0.2.1");
810
+ program.name("deploro").description("Deploro developer platform CLI").version(pkg.version);
594
811
  program.command("login").description("Log in to Deploro").option("--url <url>", "Worker base URL").option("--token <token>", "Personal access token from the Deploro dashboard").action(loginCommand);
595
812
  program.command("logout").description("Log out and clear local credentials").action(logoutCommand);
596
813
  program.command("projects").description("List projects").action(projectsListCommand);
@@ -598,6 +815,8 @@ program.command("create <name>").description("Create a new project (and set it a
598
815
  program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
599
816
  program.command("delete <project>").description("Delete a project and its database (accepts id, slug, or name)").option("-y, --yes", "Skip confirmation prompt").action(projectsDeleteCommand);
600
817
  var db = program.command("db").description("Database commands");
818
+ db.command("create").description("Provision the active project's database (idempotent)").action(dbCreateCommand);
819
+ db.command("info").description("Show database status and how to reach it from a deployed app").action(dbInfoCommand);
601
820
  db.command("tables").description("List tables in the active project").action(dbTablesCommand);
602
821
  db.command("rows <table>").description("List rows in a table").option("--limit <n>", "Rows per page (max 200)", "20").option("--page <n>", "Page number", "1").option("--json", "Output raw JSON").action(dbRowsCommand);
603
822
  db.command("query <sql>").description("Run a SQL query (project admin)").option("--json", "Output raw JSON").action(dbQueryCommand);
@@ -606,13 +825,24 @@ db.command("insert <table>").description("Insert a row").requiredOption("--data
606
825
  db.command("update <table> <id>").description("Update a row by id").requiredOption("--data <json>", "Fields to update as a JSON object").action(dbUpdateCommand);
607
826
  db.command("delete-row <table> <id>").description("Delete a row by id").option("--yes", "Skip confirmation prompt").action(dbDeleteRowCommand);
608
827
  db.command("drop <table>").description("Drop a table and all its data (project admin)").option("--yes", "Skip confirmation prompt").action(dbDropTableCommand);
828
+ var migrate = program.command("migrate").description("Database migration commands");
829
+ migrate.command("create <name>").description("Register a new migration (admin)").option("--up <sql>", "Migration SQL (inline)").option("--up-file <path>", "Read migration SQL from a file").option("--down <sql>", "Rollback SQL (inline)").option("--down-file <path>", "Read rollback SQL from a file").action(migrateCreateCommand);
830
+ migrate.command("list").description("List migrations for the active project").action(migrateListCommand);
831
+ migrate.command("apply <id-or-name>").description("Apply a pending migration (admin)").action(migrateApplyCommand);
832
+ migrate.command("rollback <id-or-name>").description("Roll back an applied migration (admin)").option("--yes", "Skip confirmation prompt").action(migrateRollbackCommand);
833
+ migrate.command("delete <id-or-name>").description("Delete an unapplied migration (admin)").option("--yes", "Skip confirmation prompt").action(migrateDeleteCommand);
609
834
  var auth = program.command("auth").description("Auth management commands");
610
- auth.command("whoami").description("Show the current identity").action(authWhoamiCommand);
835
+ auth.command("whoami").description("Show the current identity, and \u2014 for a token login \u2014 exactly what it's scoped to").action(authWhoamiCommand);
611
836
  auth.command("users").description("List platform users").action(authUsersCommand);
612
837
  auth.command("invite <email>").description("Invite a user by email").option("--admin", "Grant admin role").action(authInviteCommand);
838
+ var token = program.command("token").description("Personal access token commands");
839
+ token.command("list").description("List your personal access tokens").action(tokenListCommand);
840
+ token.command("create <name>").description("Create a new project-scoped personal access token").option("--read", "Grant read scope").option("--write", "Grant write scope").option("--project <slugs>", "Comma-separated project slugs/ids/names (defaults to the active project)").option("--days <n>", "Expiry in days (1-365)", "90").action(tokenCreateCommand);
841
+ token.command("revoke <id>").description("Revoke a personal access token").option("--yes", "Skip confirmation prompt").action(tokenRevokeCommand);
613
842
  var hosting = program.command("hosting").description("Hosting commands");
614
843
  hosting.command("status").description("Show Worker and VPS health").action(hostingStatusCommand);
615
844
  hosting.command("metrics").description("Show recent VPS CPU/memory/disk sparklines").action(hostingMetricsCommand);
845
+ hosting.command("env").description("List secret names set on the active project's Worker (values are never returned)").action(hostingEnvCommand);
616
846
  hosting.command("set-env <key> <value>").description("Set a Worker secret").action(hostingSetEnvCommand);
617
847
  var deploy = program.command("deploy").description("Deployment commands");
618
848
  deploy.command("trigger").description("Trigger a manual deployment").action(deployCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploro",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Command-line interface for the Deploro developer platform — manage projects, databases, deployments, and hosting from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {