deploro 0.2.1 → 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 +251 -23
  2. package/package.json +1 -1
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)";
@@ -598,6 +813,8 @@ program.command("create <name>").description("Create a new project (and set it a
598
813
  program.command("use <project>").description("Set the active project (accepts id, slug, or name)").action(useProjectCommand);
599
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);
600
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);
601
818
  db.command("tables").description("List tables in the active project").action(dbTablesCommand);
602
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);
603
820
  db.command("query <sql>").description("Run a SQL query (project admin)").option("--json", "Output raw JSON").action(dbQueryCommand);
@@ -606,13 +823,24 @@ db.command("insert <table>").description("Insert a row").requiredOption("--data
606
823
  db.command("update <table> <id>").description("Update a row by id").requiredOption("--data <json>", "Fields to update as a JSON object").action(dbUpdateCommand);
607
824
  db.command("delete-row <table> <id>").description("Delete a row by id").option("--yes", "Skip confirmation prompt").action(dbDeleteRowCommand);
608
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);
609
832
  var auth = program.command("auth").description("Auth management commands");
610
- 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);
611
834
  auth.command("users").description("List platform users").action(authUsersCommand);
612
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);
613
840
  var hosting = program.command("hosting").description("Hosting commands");
614
841
  hosting.command("status").description("Show Worker and VPS health").action(hostingStatusCommand);
615
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);
616
844
  hosting.command("set-env <key> <value>").description("Set a Worker secret").action(hostingSetEnvCommand);
617
845
  var deploy = program.command("deploy").description("Deployment commands");
618
846
  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.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": {