run402 1.13.1 → 1.13.3

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/lib/deploy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { dirname, resolve } from "path";
3
3
  import { API, allowanceAuthHeaders, findProject } from "./config.mjs";
4
- import { resolveFilePathsInManifest } from "./manifest.mjs";
4
+ import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
5
5
 
6
6
  const HELP = `run402 deploy — Deploy to an existing project on Run402
7
7
 
@@ -17,7 +17,8 @@ Options:
17
17
  Manifest format (JSON):
18
18
  {
19
19
  "project_id": "prj_...",
20
- "migrations": "CREATE TABLE items (id serial PRIMARY KEY, title text NOT NULL, done boolean DEFAULT false)",
20
+ "migrations": "CREATE TABLE items (...)",
21
+ "migrations_file": "setup.sql",
21
22
  "rls": {
22
23
  "template": "public_read_write",
23
24
  "tables": [{ "table": "items" }]
@@ -37,6 +38,14 @@ Manifest format (JSON):
37
38
  project_id is required (provision first with 'run402 provision').
38
39
  All other fields are optional.
39
40
 
41
+ Migrations can be inline or read from a file:
42
+ "migrations": "CREATE TABLE ..." ← inline SQL
43
+ "migrations_file": "setup.sql" ← read from disk
44
+ Use migrations_file when your SQL contains JSONB literals or other
45
+ characters that are painful to escape inside a JSON string.
46
+ Paths are resolved relative to the manifest file's directory.
47
+ If both are present, migrations_file wins.
48
+
40
49
  Files can use either inline "data" or a local "path":
41
50
  { "file": "index.html", "data": "<html>...</html>" } ← inline content
42
51
  { "file": "style.css", "path": "./dist/style.css" } ← read from disk
@@ -83,7 +92,11 @@ export async function run(args) {
83
92
 
84
93
  const raw = opts.manifest ? readFileSync(opts.manifest, "utf-8") : await readStdin();
85
94
  const manifest = JSON.parse(raw);
86
- if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
95
+ if (opts.manifest) {
96
+ const baseDir = dirname(resolve(opts.manifest));
97
+ resolveMigrationsFile(manifest, baseDir);
98
+ resolveFilePathsInManifest(manifest, baseDir);
99
+ }
87
100
 
88
101
  // --project flag overrides manifest's project_id
89
102
  if (opts.project) manifest.project_id = opts.project;
package/lib/manifest.mjs CHANGED
@@ -6,6 +6,23 @@ const TEXT_EXTS = new Set([
6
6
  ".json", ".svg", ".xml", ".txt", ".md", ".yaml", ".yml", ".toml", ".csv",
7
7
  ]);
8
8
 
9
+ /**
10
+ * If the manifest has `migrations_file` instead of (or in addition to) `migrations`,
11
+ * read the SQL from that file path and set `migrations` to its contents.
12
+ * `migrations_file` is resolved relative to `baseDir`.
13
+ *
14
+ * @param {object} manifest Parsed manifest JSON (mutated in place)
15
+ * @param {string} baseDir Directory to resolve relative paths from
16
+ * @returns {object} The same manifest object
17
+ */
18
+ export function resolveMigrationsFile(manifest, baseDir) {
19
+ if (!manifest.migrations_file) return manifest;
20
+ const abs = resolve(baseDir, manifest.migrations_file);
21
+ manifest.migrations = readFileSync(abs, "utf-8");
22
+ delete manifest.migrations_file;
23
+ return manifest;
24
+ }
25
+
9
26
  /**
10
27
  * Resolve `path` fields in a manifest's files array.
11
28
  *
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
- import { resolveFilePathsInManifest } from "./manifest.mjs";
6
+ import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
7
7
 
8
8
  let tempDir;
9
9
 
@@ -13,6 +13,7 @@ before(() => {
13
13
  writeFileSync(join(tempDir, "index.html"), "<!DOCTYPE html><html><body>Hello</body></html>");
14
14
  writeFileSync(join(tempDir, "style.css"), "body { margin: 0; }");
15
15
  writeFileSync(join(tempDir, "logo.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); // PNG header
16
+ writeFileSync(join(tempDir, "setup.sql"), "CREATE TABLE items (id serial PRIMARY KEY, data jsonb);\nINSERT INTO items (data) VALUES ('[{\"x\":0.5}]');");
16
17
  });
17
18
 
18
19
  after(() => {
@@ -88,3 +89,40 @@ describe("resolveFilePathsInManifest", () => {
88
89
  );
89
90
  });
90
91
  });
92
+
93
+ describe("resolveMigrationsFile", () => {
94
+ it("reads SQL from migrations_file and sets migrations", () => {
95
+ const manifest = { migrations_file: "setup.sql" };
96
+ resolveMigrationsFile(manifest, tempDir);
97
+ assert.ok(manifest.migrations.includes("CREATE TABLE items"));
98
+ assert.ok(manifest.migrations.includes('[{"x":0.5}]'), "should preserve JSON literals without escaping issues");
99
+ assert.equal(manifest.migrations_file, undefined, "migrations_file should be removed");
100
+ });
101
+
102
+ it("overwrites inline migrations when migrations_file is present", () => {
103
+ const manifest = { migrations: "SELECT 1", migrations_file: "setup.sql" };
104
+ resolveMigrationsFile(manifest, tempDir);
105
+ assert.ok(manifest.migrations.includes("CREATE TABLE items"));
106
+ assert.equal(manifest.migrations_file, undefined);
107
+ });
108
+
109
+ it("leaves manifest untouched when no migrations_file", () => {
110
+ const manifest = { migrations: "SELECT 1" };
111
+ resolveMigrationsFile(manifest, tempDir);
112
+ assert.equal(manifest.migrations, "SELECT 1");
113
+ });
114
+
115
+ it("handles manifest with neither migrations nor migrations_file", () => {
116
+ const manifest = { files: [] };
117
+ resolveMigrationsFile(manifest, tempDir);
118
+ assert.equal(manifest.migrations, undefined);
119
+ });
120
+
121
+ it("throws on missing migrations file", () => {
122
+ const manifest = { migrations_file: "does-not-exist.sql" };
123
+ assert.throws(
124
+ () => resolveMigrationsFile(manifest, tempDir),
125
+ /ENOENT/,
126
+ );
127
+ });
128
+ });
package/lib/projects.mjs CHANGED
@@ -11,6 +11,7 @@ Subcommands:
11
11
  use <id> Set the active project (used as default for other commands)
12
12
  list List all your projects (IDs, URLs, active marker)
13
13
  info <id> Show project details: REST URL, keys
14
+ keys <id> Print anon_key and service_key as JSON
14
15
  sql <id> "<query>" Run a SQL query against a project's Postgres DB
15
16
  rest <id> <table> [params] Query a table via the REST API (PostgREST)
16
17
  usage <id> Show compute/storage usage for a project
@@ -108,6 +109,11 @@ async function info(projectId) {
108
109
  }, null, 2));
109
110
  }
110
111
 
112
+ async function keys(projectId) {
113
+ const p = findProject(projectId);
114
+ console.log(JSON.stringify({ project_id: projectId, anon_key: p.anon_key, service_key: p.service_key }, null, 2));
115
+ }
116
+
111
117
  async function sqlCmd(projectId, query) {
112
118
  const p = findProject(projectId);
113
119
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: query });
@@ -185,6 +191,7 @@ export async function run(sub, args) {
185
191
  case "use": await use(args[0]); break;
186
192
  case "list": await list(); break;
187
193
  case "info": await info(args[0]); break;
194
+ case "keys": await keys(args[0]); break;
188
195
  case "sql": await sqlCmd(args[0], args[1]); break;
189
196
  case "rest": await rest(args[0], args[1], args[2]); break;
190
197
  case "usage": await usage(args[0]); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.13.1",
3
+ "version": "1.13.3",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
5
5
  "type": "module",
6
6
  "bin": {