deploro 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +286 -25
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -148,6 +148,11 @@ async function logoutCommand() {
148
148
  }
149
149
 
150
150
  // src/lib/api.ts
151
+ function errorMessage(status, body, statusText) {
152
+ const parts = [body.error ?? statusText];
153
+ if (body.detail && body.detail !== body.error) parts.push(body.detail);
154
+ return `${status} ${parts.join(" \u2014 ")}`;
155
+ }
151
156
  async function apiRequest(path, options, configOverride) {
152
157
  const config = configOverride ?? requireConfig();
153
158
  const headers = {
@@ -159,7 +164,7 @@ async function apiRequest(path, options, configOverride) {
159
164
  const res = await fetch(`${config.baseUrl}${path}`, { ...options, headers });
160
165
  if (!res.ok) {
161
166
  const body = await res.json().catch(() => ({ error: res.statusText }));
162
- throw new Error(`${res.status} ${body.error ?? res.statusText}`);
167
+ throw new Error(errorMessage(res.status, body, res.statusText));
163
168
  }
164
169
  return res.json();
165
170
  }
@@ -175,7 +180,7 @@ async function apiRequestNoProject(path, options) {
175
180
  });
176
181
  if (!res.ok) {
177
182
  const body = await res.json().catch(() => ({ error: res.statusText }));
178
- throw new Error(`${res.status} ${body.error ?? res.statusText}`);
183
+ throw new Error(errorMessage(res.status, body, res.statusText));
179
184
  }
180
185
  return res.json();
181
186
  }
@@ -226,6 +231,44 @@ function json(data) {
226
231
  }
227
232
 
228
233
  // src/commands/db.ts
234
+ async function dbCreateCommand() {
235
+ const config = requireProject();
236
+ const data = await apiRequestNoProject(
237
+ `/api/projects/${config.projectId}/database`,
238
+ { method: "POST" }
239
+ );
240
+ console.log(`\u2713 Database ready: ${data.database_name}`);
241
+ }
242
+ async function dbInfoCommand() {
243
+ const config = requireProject();
244
+ const { projects } = await apiRequestNoProject("/api/projects");
245
+ const project = projects.find((p) => p.id === config.projectId);
246
+ if (!project) {
247
+ console.error("Active project not found or not visible to this token. Run: deploro projects");
248
+ process.exit(1);
249
+ }
250
+ 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");
251
+ console.log(` Project : ${project.slug}`);
252
+ console.log(` Database name : ${project.database_name ?? "(not provisioned \u2014 run: deploro db create)"}`);
253
+ console.log("");
254
+ if (project.database_name) {
255
+ console.log(
256
+ "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"
257
+ );
258
+ console.log(
259
+ "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"
260
+ );
261
+ console.log(` Base URL : ${config.baseUrl}/api/projects/${config.projectId}/studio`);
262
+ console.log(` Spec : ${config.baseUrl}/api/projects/${config.projectId}/studio/spec (OpenAPI 3.1)`);
263
+ console.log(` Auth : Authorization: Bearer <project-scoped PAT>`);
264
+ console.log(` create one with: deploro token create <name> --project ${project.slug}`);
265
+ console.log("");
266
+ console.log("To hand that to your deployed Worker as secrets:");
267
+ console.log(` deploro hosting set-env DEPLORO_API_URL "${config.baseUrl}/api/projects/${config.projectId}/studio"`);
268
+ console.log(` deploro hosting set-env DEPLORO_API_TOKEN "<pat>"`);
269
+ console.log("");
270
+ }
271
+ }
229
272
  async function dbTablesCommand() {
230
273
  requireProject();
231
274
  const data = await apiRequest("/api/db/tables");
@@ -319,6 +362,9 @@ async function dbDropTableCommand(tableName, opts) {
319
362
  }
320
363
 
321
364
  // src/commands/auth.ts
365
+ function isPatToken(accessToken) {
366
+ return accessToken.startsWith("gallium_pat_") || accessToken.startsWith("deploro_pat_");
367
+ }
322
368
  async function authUsersCommand() {
323
369
  requireConfig();
324
370
  const data = await apiRequest("/api/auth/users");
@@ -345,30 +391,188 @@ async function authInviteCommand(email, opts) {
345
391
  }
346
392
  }
347
393
  async function authWhoamiCommand() {
394
+ requireConfig();
395
+ const data = await apiRequestNoProject("/api/auth/whoami");
396
+ if (data.authMethod === "pat") {
397
+ console.log("Authenticated via personal access token.");
398
+ console.log(` User id : ${data.userId}`);
399
+ console.log(` Role : ${data.role}`);
400
+ console.log(` Scopes : ${(data.scopes ?? []).join(", ") || "(none)"}`);
401
+ console.log(` Scoped to ${data.projects?.length ?? 0} project(s):`);
402
+ if (data.projects && data.projects.length > 0) {
403
+ table(data.projects.map((p) => ({ id: p.id, slug: p.slug, name: p.name })));
404
+ } else {
405
+ console.log(" (none \u2014 this token cannot access any project)");
406
+ }
407
+ } else {
408
+ console.log("Authenticated via browser session.");
409
+ console.log(` User id : ${data.userId}`);
410
+ console.log(` Role : ${data.role}`);
411
+ }
412
+ }
413
+ function requireSessionAuth(config) {
414
+ if (isPatToken(config.accessToken)) {
415
+ console.error(
416
+ "Managing tokens requires a browser-session login, not a personal access token.\nRun 'deploro login' without --token (email + password) and try again."
417
+ );
418
+ process.exit(1);
419
+ }
420
+ }
421
+ async function tokenListCommand() {
348
422
  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;
423
+ requireSessionAuth(config);
424
+ const data = await apiRequestNoProject("/api/auth/tokens");
425
+ table(
426
+ data.tokens.map((t) => ({
427
+ id: t.id,
428
+ name: t.name,
429
+ prefix: `${t.token_prefix}\u2026`,
430
+ scopes: t.scopes.join(","),
431
+ projects: t.project_ids.length,
432
+ expires: new Date(t.expires_at).toLocaleDateString(),
433
+ status: t.revoked_at ? "revoked" : "active"
434
+ }))
435
+ );
436
+ }
437
+ async function tokenCreateCommand(name, opts) {
438
+ const config = requireConfig();
439
+ requireSessionAuth(config);
440
+ const scopes = !opts.read && !opts.write ? ["read"] : [opts.read && "read", opts.write && "write"].filter(Boolean);
441
+ let projectIds;
442
+ if (opts.project) {
443
+ const list = await apiRequestNoProject(
444
+ "/api/projects"
445
+ );
446
+ const wanted = opts.project.split(",").map((s) => s.trim().toLowerCase());
447
+ projectIds = list.projects.filter((p) => wanted.includes(p.id) || wanted.includes(p.slug.toLowerCase()) || wanted.includes(p.name.toLowerCase())).map((p) => p.id);
448
+ if (projectIds.length === 0) {
449
+ console.error(`No project matches "${opts.project}". Run: deploro projects`);
450
+ process.exit(1);
364
451
  }
365
- console.error("Session is expired or invalid. Run: deploro login");
452
+ } else if (config.projectId) {
453
+ projectIds = [config.projectId];
454
+ } else {
455
+ console.error("No project specified. Pass --project <slug> or set an active project with 'deploro use'.");
366
456
  process.exit(1);
367
457
  }
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)`);
458
+ const days = Number(opts.days ?? 90);
459
+ const data = await apiRequestNoProject(
460
+ "/api/auth/tokens",
461
+ { method: "POST", body: JSON.stringify({ name, scopes, projectIds, expiresInDays: days }) }
462
+ );
463
+ console.log("\u2713 Token created \u2014 copy it now, it will not be shown again:");
464
+ console.log(`
465
+ ${data.token}
466
+ `);
467
+ console.log(` Scopes : ${scopes.join(", ")}`);
468
+ console.log(` Projects: ${projectIds.length}`);
469
+ console.log(` Expires : in ${data.expiresInDays} days`);
470
+ console.log(`
471
+ Use it with: deploro login --token ${data.token}`);
472
+ }
473
+ async function tokenRevokeCommand(id, opts) {
474
+ const config = requireConfig();
475
+ requireSessionAuth(config);
476
+ await confirmOrDie(`Revoke token ${id}?`, opts.yes);
477
+ await apiRequestNoProject(`/api/auth/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
478
+ console.log(`\u2713 Token ${id} revoked`);
479
+ }
480
+
481
+ // src/commands/migrate.ts
482
+ import { readFileSync as readFileSync2 } from "fs";
483
+ function readSqlOpt(inline, file, label) {
484
+ if (file) {
485
+ try {
486
+ return readFileSync2(file, "utf-8");
487
+ } catch (err) {
488
+ console.error(`Could not read --${label}-file "${file}": ${err instanceof Error ? err.message : String(err)}`);
489
+ process.exit(1);
490
+ }
491
+ }
492
+ return inline;
493
+ }
494
+ async function migrateCreateCommand(name, opts) {
495
+ requireProject();
496
+ const sqlUp = readSqlOpt(opts.up, opts.upFile, "up");
497
+ if (!sqlUp) {
498
+ console.error('Migration SQL is required: pass --up "<sql>" or --up-file <path>');
499
+ process.exit(1);
500
+ }
501
+ const sqlDown = readSqlOpt(opts.down, opts.downFile, "down");
502
+ const config = requireProject();
503
+ const data = await apiRequest(
504
+ `/api/projects/${config.projectId}/migrations`,
505
+ { method: "POST", body: JSON.stringify({ name, sql_up: sqlUp, sql_down: sqlDown }) }
506
+ );
507
+ console.log(`\u2713 Migration created: ${data.migration.name} (${data.migration.id})`);
508
+ console.log(` Apply it with: deploro migrate apply ${data.migration.id}`);
509
+ }
510
+ async function migrateListCommand() {
511
+ const config = requireProject();
512
+ const data = await apiRequest(`/api/projects/${config.projectId}/migrations`);
513
+ table(
514
+ data.migrations.map((m) => ({
515
+ id: m.id,
516
+ name: m.name,
517
+ status: m.applied_at ? "applied" : "pending",
518
+ applied_at: m.applied_at ? new Date(m.applied_at).toLocaleString() : "\u2014",
519
+ created_at: new Date(m.created_at).toLocaleDateString()
520
+ }))
521
+ );
522
+ }
523
+ async function resolveMigration(idOrName) {
524
+ const config = requireProject();
525
+ const data = await apiRequest(`/api/projects/${config.projectId}/migrations`);
526
+ const matches = data.migrations.filter((m) => m.id === idOrName || m.name === idOrName);
527
+ if (matches.length === 0) {
528
+ console.error(`No migration matches "${idOrName}". Run: deploro migrate list`);
529
+ process.exit(1);
530
+ }
531
+ if (matches.length > 1) {
532
+ console.error(`"${idOrName}" is ambiguous (matches ${matches.length} migrations by name) \u2014 use the id.`);
533
+ process.exit(1);
534
+ }
535
+ return { config, migration: matches[0] };
536
+ }
537
+ async function migrateApplyCommand(idOrName) {
538
+ const { config, migration } = await resolveMigration(idOrName);
539
+ if (migration.applied_at) {
540
+ console.error(`Migration "${migration.name}" is already applied.`);
541
+ process.exit(1);
542
+ }
543
+ const data = await apiRequest(
544
+ `/api/projects/${config.projectId}/migrations/${migration.id}/apply`,
545
+ { method: "POST" }
546
+ );
547
+ console.log(`\u2713 Applied: ${data.migration.name}`);
548
+ console.log(` Applied at: ${data.migration.applied_at ? new Date(data.migration.applied_at).toLocaleString() : "(unknown)"}`);
549
+ }
550
+ async function migrateRollbackCommand(idOrName, opts) {
551
+ const { config, migration } = await resolveMigration(idOrName);
552
+ if (!migration.applied_at) {
553
+ console.error(`Migration "${migration.name}" has not been applied.`);
554
+ process.exit(1);
555
+ }
556
+ if (!migration.sql_down) {
557
+ console.error(`Migration "${migration.name}" has no rollback SQL defined.`);
558
+ process.exit(1);
559
+ }
560
+ await confirmOrDie(`Roll back "${migration.name}"? This runs its sql_down against the project database.`, opts.yes);
561
+ const data = await apiRequest(
562
+ `/api/projects/${config.projectId}/migrations/${migration.id}/rollback`,
563
+ { method: "POST" }
564
+ );
565
+ console.log(`\u2713 Rolled back: ${data.migration.name}`);
566
+ }
567
+ async function migrateDeleteCommand(idOrName, opts) {
568
+ const { config, migration } = await resolveMigration(idOrName);
569
+ if (migration.applied_at) {
570
+ console.error(`"${migration.name}" is applied \u2014 roll it back first: deploro migrate rollback ${migration.id}`);
571
+ process.exit(1);
572
+ }
573
+ await confirmOrDie(`Delete unapplied migration "${migration.name}"?`, opts.yes);
574
+ await apiRequest(`/api/projects/${config.projectId}/migrations/${migration.id}`, { method: "DELETE" });
575
+ console.log(`\u2713 Migration "${migration.name}" deleted`);
372
576
  }
373
577
 
374
578
  // src/commands/hosting.ts
@@ -416,6 +620,17 @@ async function hostingSetEnvCommand(key, value) {
416
620
  });
417
621
  console.log(`\u2713 Secret '${key}' updated`);
418
622
  }
623
+ async function hostingEnvCommand() {
624
+ requireConfig();
625
+ const data = await apiRequest("/api/hosting/env");
626
+ if (data.secrets.length === 0) {
627
+ console.log("No secrets set on the active project's Worker.");
628
+ return;
629
+ }
630
+ console.log("\nSecrets set on the active project's Worker (values are never returned by the API):\n");
631
+ for (const s of data.secrets) console.log(` ${s.name}`);
632
+ console.log("");
633
+ }
419
634
  var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
420
635
  function spark(values) {
421
636
  if (values.length === 0) return "(no data)";
@@ -499,6 +714,35 @@ async function useProjectCommand(idOrSlug) {
499
714
  writeConfig({ ...config, projectId: matches[0].id });
500
715
  console.log(`\u2713 Active project: ${matches[0].name} (${matches[0].slug})`);
501
716
  }
717
+ async function projectsDeleteCommand(idOrSlug, opts) {
718
+ const config = requireConfig();
719
+ const data = await apiRequestNoProject("/api/projects");
720
+ const q = idOrSlug.toLowerCase();
721
+ const matches = data.projects.filter(
722
+ (p) => p.id === idOrSlug || p.slug.toLowerCase() === q || p.name.toLowerCase() === q
723
+ );
724
+ if (matches.length === 0) {
725
+ console.error(`No project matches "${idOrSlug}". Run: deploro projects`);
726
+ process.exit(1);
727
+ }
728
+ if (matches.length > 1) {
729
+ console.error(
730
+ `"${idOrSlug}" is ambiguous (matches: ${matches.map((m) => m.slug).join(", ")}). Use the full id.`
731
+ );
732
+ process.exit(1);
733
+ }
734
+ const project = matches[0];
735
+ await confirmOrDie(
736
+ `Delete project "${project.name}" (${project.slug}) and its database? This cannot be undone.`,
737
+ opts.yes
738
+ );
739
+ await apiRequestNoProject(`/api/projects/${project.id}`, { method: "DELETE" });
740
+ console.log(`\u2713 Project "${project.name}" deleted`);
741
+ if (config.projectId === project.id) {
742
+ writeConfig({ ...config, projectId: void 0 });
743
+ console.log(" (cleared as active project)");
744
+ }
745
+ }
502
746
  async function deployCommand() {
503
747
  const config = requireProject();
504
748
  const data = await apiRequest(
@@ -561,13 +805,16 @@ async function deployLogsCommand(deploymentId) {
561
805
 
562
806
  // src/index.ts
563
807
  var program = new Command();
564
- program.name("deploro").description("Deploro developer platform CLI").version("0.2.0");
808
+ program.name("deploro").description("Deploro developer platform CLI").version("0.2.1");
565
809
  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);
566
810
  program.command("logout").description("Log out and clear local credentials").action(logoutCommand);
567
811
  program.command("projects").description("List projects").action(projectsListCommand);
568
812
  program.command("create <name>").description("Create a new project (and set it active)").option("--slug <slug>", "Project slug (derived from name if omitted)").option("--repo <url>", "GitHub repo URL to link for deployments").option("--branch <branch>", "Git branch to deploy from").action(projectsCreateCommand);
569
813
  program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
814
+ program.command("delete <project>").description("Delete a project and its database (accepts id, slug, or name)").option("-y, --yes", "Skip confirmation prompt").action(projectsDeleteCommand);
570
815
  var db = program.command("db").description("Database commands");
816
+ db.command("create").description("Provision the active project's database (idempotent)").action(dbCreateCommand);
817
+ db.command("info").description("Show database status and how to reach it from a deployed app").action(dbInfoCommand);
571
818
  db.command("tables").description("List tables in the active project").action(dbTablesCommand);
572
819
  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);
573
820
  db.command("query <sql>").description("Run a SQL query (project admin)").option("--json", "Output raw JSON").action(dbQueryCommand);
@@ -576,16 +823,30 @@ db.command("insert <table>").description("Insert a row").requiredOption("--data
576
823
  db.command("update <table> <id>").description("Update a row by id").requiredOption("--data <json>", "Fields to update as a JSON object").action(dbUpdateCommand);
577
824
  db.command("delete-row <table> <id>").description("Delete a row by id").option("--yes", "Skip confirmation prompt").action(dbDeleteRowCommand);
578
825
  db.command("drop <table>").description("Drop a table and all its data (project admin)").option("--yes", "Skip confirmation prompt").action(dbDropTableCommand);
826
+ var migrate = program.command("migrate").description("Database migration commands");
827
+ 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);
828
+ migrate.command("list").description("List migrations for the active project").action(migrateListCommand);
829
+ migrate.command("apply <id-or-name>").description("Apply a pending migration (admin)").action(migrateApplyCommand);
830
+ migrate.command("rollback <id-or-name>").description("Roll back an applied migration (admin)").option("--yes", "Skip confirmation prompt").action(migrateRollbackCommand);
831
+ migrate.command("delete <id-or-name>").description("Delete an unapplied migration (admin)").option("--yes", "Skip confirmation prompt").action(migrateDeleteCommand);
579
832
  var auth = program.command("auth").description("Auth management commands");
580
- auth.command("whoami").description("Show the current identity").action(authWhoamiCommand);
833
+ auth.command("whoami").description("Show the current identity, and \u2014 for a token login \u2014 exactly what it's scoped to").action(authWhoamiCommand);
581
834
  auth.command("users").description("List platform users").action(authUsersCommand);
582
835
  auth.command("invite <email>").description("Invite a user by email").option("--admin", "Grant admin role").action(authInviteCommand);
836
+ var token = program.command("token").description("Personal access token commands");
837
+ token.command("list").description("List your personal access tokens").action(tokenListCommand);
838
+ 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);
839
+ token.command("revoke <id>").description("Revoke a personal access token").option("--yes", "Skip confirmation prompt").action(tokenRevokeCommand);
583
840
  var hosting = program.command("hosting").description("Hosting commands");
584
841
  hosting.command("status").description("Show Worker and VPS health").action(hostingStatusCommand);
585
842
  hosting.command("metrics").description("Show recent VPS CPU/memory/disk sparklines").action(hostingMetricsCommand);
843
+ hosting.command("env").description("List secret names set on the active project's Worker (values are never returned)").action(hostingEnvCommand);
586
844
  hosting.command("set-env <key> <value>").description("Set a Worker secret").action(hostingSetEnvCommand);
587
845
  var deploy = program.command("deploy").description("Deployment commands");
588
846
  deploy.command("trigger").description("Trigger a manual deployment").action(deployCommand);
589
847
  deploy.command("status").description("Show latest deployment status").action(deployStatusCommand);
590
848
  deploy.command("logs [deployment-id]").description("Show logs for a deployment (defaults to latest)").action(deployLogsCommand);
591
- program.parse();
849
+ program.parseAsync().catch((err) => {
850
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
851
+ process.exit(1);
852
+ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "deploro",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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": {
7
- "deploro": "./dist/index.js"
7
+ "deploro": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"