run402 1.13.0 → 1.13.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/lib/deploy.mjs +15 -2
- package/lib/manifest.mjs +45 -0
- package/lib/manifest.test.mjs +90 -0
- package/lib/projects.mjs +22 -1
- package/lib/sites.mjs +12 -2
- package/package.json +1 -1
package/lib/deploy.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
2
3
|
import { API, allowanceAuthHeaders, findProject } from "./config.mjs";
|
|
4
|
+
import { resolveFilePathsInManifest } from "./manifest.mjs";
|
|
3
5
|
|
|
4
6
|
const HELP = `run402 deploy — Deploy to an existing project on Run402
|
|
5
7
|
|
|
@@ -25,13 +27,22 @@ Manifest format (JSON):
|
|
|
25
27
|
"name": "my-fn",
|
|
26
28
|
"code": "export default async (req) => new Response('ok')"
|
|
27
29
|
}],
|
|
28
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
{ "file": "index.html", "data": "<html>...</html>" },
|
|
32
|
+
{ "file": "style.css", "path": "./dist/style.css" }
|
|
33
|
+
],
|
|
29
34
|
"subdomain": "my-app"
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
project_id is required (provision first with 'run402 provision').
|
|
33
38
|
All other fields are optional.
|
|
34
39
|
|
|
40
|
+
Files can use either inline "data" or a local "path":
|
|
41
|
+
{ "file": "index.html", "data": "<html>...</html>" } ← inline content
|
|
42
|
+
{ "file": "style.css", "path": "./dist/style.css" } ← read from disk
|
|
43
|
+
Paths are resolved relative to the manifest file's directory.
|
|
44
|
+
Binary files (images, fonts, etc.) are auto-detected and base64-encoded.
|
|
45
|
+
|
|
35
46
|
RLS templates:
|
|
36
47
|
user_owns_rows — users see only their rows (requires owner_column per table)
|
|
37
48
|
public_read — anyone reads, authenticated users write
|
|
@@ -70,7 +81,9 @@ export async function run(args) {
|
|
|
70
81
|
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
const
|
|
84
|
+
const raw = opts.manifest ? readFileSync(opts.manifest, "utf-8") : await readStdin();
|
|
85
|
+
const manifest = JSON.parse(raw);
|
|
86
|
+
if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
|
|
74
87
|
|
|
75
88
|
// --project flag overrides manifest's project_id
|
|
76
89
|
if (opts.project) manifest.project_id = opts.project;
|
package/lib/manifest.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve, extname } from "path";
|
|
3
|
+
|
|
4
|
+
const TEXT_EXTS = new Set([
|
|
5
|
+
".html", ".htm", ".css", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
|
|
6
|
+
".json", ".svg", ".xml", ".txt", ".md", ".yaml", ".yml", ".toml", ".csv",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `path` fields in a manifest's files array.
|
|
11
|
+
*
|
|
12
|
+
* For each entry that has `path` instead of `data`, reads the file from disk
|
|
13
|
+
* and sets `data` + `encoding`. Paths are resolved relative to `baseDir`.
|
|
14
|
+
*
|
|
15
|
+
* Entries with `data` already set are left untouched.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} manifest Parsed manifest JSON (mutated in place)
|
|
18
|
+
* @param {string} baseDir Directory to resolve relative paths from
|
|
19
|
+
* @returns {object} The same manifest object
|
|
20
|
+
*/
|
|
21
|
+
export function resolveFilePathsInManifest(manifest, baseDir) {
|
|
22
|
+
if (!Array.isArray(manifest.files)) return manifest;
|
|
23
|
+
|
|
24
|
+
for (const entry of manifest.files) {
|
|
25
|
+
if (!entry.path || entry.data !== undefined) continue;
|
|
26
|
+
|
|
27
|
+
const abs = resolve(baseDir, entry.path);
|
|
28
|
+
const ext = extname(abs).toLowerCase();
|
|
29
|
+
const isText = TEXT_EXTS.has(ext);
|
|
30
|
+
|
|
31
|
+
if (isText) {
|
|
32
|
+
entry.data = readFileSync(abs, "utf-8");
|
|
33
|
+
} else {
|
|
34
|
+
entry.data = readFileSync(abs).toString("base64");
|
|
35
|
+
entry.encoding = "base64";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If no explicit file (deploy target name), use the path value
|
|
39
|
+
if (!entry.file) entry.file = entry.path;
|
|
40
|
+
|
|
41
|
+
delete entry.path;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return manifest;
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { resolveFilePathsInManifest } from "./manifest.mjs";
|
|
7
|
+
|
|
8
|
+
let tempDir;
|
|
9
|
+
|
|
10
|
+
before(() => {
|
|
11
|
+
tempDir = mkdtempSync(join(tmpdir(), "manifest-test-"));
|
|
12
|
+
// Create test files
|
|
13
|
+
writeFileSync(join(tempDir, "index.html"), "<!DOCTYPE html><html><body>Hello</body></html>");
|
|
14
|
+
writeFileSync(join(tempDir, "style.css"), "body { margin: 0; }");
|
|
15
|
+
writeFileSync(join(tempDir, "logo.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); // PNG header
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
after(() => {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("resolveFilePathsInManifest", () => {
|
|
23
|
+
it("resolves path to inline data for text files", () => {
|
|
24
|
+
const manifest = {
|
|
25
|
+
files: [{ file: "index.html", path: "index.html" }],
|
|
26
|
+
};
|
|
27
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
28
|
+
assert.equal(manifest.files[0].data, "<!DOCTYPE html><html><body>Hello</body></html>");
|
|
29
|
+
assert.equal(manifest.files[0].path, undefined, "path field should be removed");
|
|
30
|
+
assert.equal(manifest.files[0].encoding, undefined, "text files should not set encoding");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("auto-detects binary files and base64-encodes them", () => {
|
|
34
|
+
const manifest = {
|
|
35
|
+
files: [{ file: "logo.png", path: "logo.png" }],
|
|
36
|
+
};
|
|
37
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
38
|
+
assert.equal(manifest.files[0].encoding, "base64");
|
|
39
|
+
// Verify it's valid base64 that decodes to our PNG header
|
|
40
|
+
const buf = Buffer.from(manifest.files[0].data, "base64");
|
|
41
|
+
assert.equal(buf[0], 0x89);
|
|
42
|
+
assert.equal(buf[1], 0x50); // 'P'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("leaves entries with existing data untouched", () => {
|
|
46
|
+
const manifest = {
|
|
47
|
+
files: [{ file: "index.html", data: "<h1>inline</h1>" }],
|
|
48
|
+
};
|
|
49
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
50
|
+
assert.equal(manifest.files[0].data, "<h1>inline</h1>");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("mixes path and data entries", () => {
|
|
54
|
+
const manifest = {
|
|
55
|
+
files: [
|
|
56
|
+
{ file: "index.html", data: "<h1>inline</h1>" },
|
|
57
|
+
{ file: "style.css", path: "style.css" },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
61
|
+
assert.equal(manifest.files[0].data, "<h1>inline</h1>");
|
|
62
|
+
assert.equal(manifest.files[1].data, "body { margin: 0; }");
|
|
63
|
+
assert.equal(manifest.files[1].path, undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("uses path as file name when file is omitted", () => {
|
|
67
|
+
const manifest = {
|
|
68
|
+
files: [{ path: "style.css" }],
|
|
69
|
+
};
|
|
70
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
71
|
+
assert.equal(manifest.files[0].file, "style.css");
|
|
72
|
+
assert.equal(manifest.files[0].data, "body { margin: 0; }");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles manifest with no files array", () => {
|
|
76
|
+
const manifest = { migrations: "CREATE TABLE t (id int)" };
|
|
77
|
+
resolveFilePathsInManifest(manifest, tempDir);
|
|
78
|
+
assert.equal(manifest.files, undefined, "should not add a files array");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("throws on missing file", () => {
|
|
82
|
+
const manifest = {
|
|
83
|
+
files: [{ file: "missing.html", path: "does-not-exist.html" }],
|
|
84
|
+
};
|
|
85
|
+
assert.throws(
|
|
86
|
+
() => resolveFilePathsInManifest(manifest, tempDir),
|
|
87
|
+
/ENOENT/,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/lib/projects.mjs
CHANGED
|
@@ -16,7 +16,8 @@ Subcommands:
|
|
|
16
16
|
usage <id> Show compute/storage usage for a project
|
|
17
17
|
schema <id> Inspect the database schema
|
|
18
18
|
rls <id> <template> <tables_json> Apply Row-Level Security policies
|
|
19
|
-
delete <id> Delete a project and remove it from local state
|
|
19
|
+
delete <id> Delete a project and remove it from local state${process.env.RUN402_ADMIN ? `
|
|
20
|
+
pin <id> [admin] Pin a project (prevents expiry/GC)` : ""}
|
|
20
21
|
|
|
21
22
|
Examples:
|
|
22
23
|
run402 projects quote
|
|
@@ -142,6 +143,25 @@ async function use(projectId) {
|
|
|
142
143
|
console.log(JSON.stringify({ status: "ok", active_project_id: projectId }));
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
async function pin(projectId) {
|
|
147
|
+
if (!projectId) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects pin <project_id>" })); process.exit(1); }
|
|
148
|
+
const authHeaders = allowanceAuthHeaders(`/projects/v1/admin/${projectId}/pin`);
|
|
149
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/pin`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { ...authHeaders },
|
|
152
|
+
});
|
|
153
|
+
const data = await res.json();
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
if (res.status === 403 && data.admin_required) {
|
|
156
|
+
console.error(JSON.stringify({ status: "error", message: "This command requires admin access." }));
|
|
157
|
+
} else {
|
|
158
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(JSON.stringify(data, null, 2));
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
async function deleteProject(projectId) {
|
|
146
166
|
const p = findProject(projectId);
|
|
147
167
|
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
@@ -171,6 +191,7 @@ export async function run(sub, args) {
|
|
|
171
191
|
case "schema": await schema(args[0]); break;
|
|
172
192
|
case "rls": await rls(args[0], args[1], args[2]); break;
|
|
173
193
|
case "delete": await deleteProject(args[0]); break;
|
|
194
|
+
case "pin": await pin(args[0]); break;
|
|
174
195
|
default:
|
|
175
196
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
176
197
|
console.log(HELP);
|
package/lib/sites.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
2
3
|
import { API, allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
|
|
4
|
+
import { resolveFilePathsInManifest } from "./manifest.mjs";
|
|
3
5
|
|
|
4
6
|
const HELP = `run402 sites — Deploy and manage static sites
|
|
5
7
|
|
|
@@ -22,10 +24,16 @@ Manifest format (JSON):
|
|
|
22
24
|
{
|
|
23
25
|
"files": [
|
|
24
26
|
{ "file": "index.html", "data": "<html>...</html>" },
|
|
25
|
-
{ "file": "style.css", "
|
|
27
|
+
{ "file": "style.css", "path": "./dist/style.css" }
|
|
26
28
|
]
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
Files can use either inline "data" or a local "path":
|
|
32
|
+
{ "file": "index.html", "data": "<html>...</html>" } ← inline content
|
|
33
|
+
{ "file": "style.css", "path": "./dist/style.css" } ← read from disk
|
|
34
|
+
Paths are resolved relative to the manifest file's directory.
|
|
35
|
+
Binary files (images, fonts, etc.) are auto-detected and base64-encoded.
|
|
36
|
+
|
|
29
37
|
Examples:
|
|
30
38
|
run402 sites deploy --manifest site.json
|
|
31
39
|
run402 sites status dpl_abc123
|
|
@@ -51,7 +59,9 @@ async function deploy(args) {
|
|
|
51
59
|
if (args[i] === "--target" && args[i + 1]) opts.target = args[++i];
|
|
52
60
|
}
|
|
53
61
|
const projectId = resolveProjectId(opts.project);
|
|
54
|
-
const
|
|
62
|
+
const raw = opts.manifest ? readFileSync(opts.manifest, "utf-8") : await readStdin();
|
|
63
|
+
const manifest = JSON.parse(raw);
|
|
64
|
+
if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
|
|
55
65
|
const body = { files: manifest.files, project: projectId };
|
|
56
66
|
if (opts.target) body.target = opts.target;
|
|
57
67
|
|
package/package.json
CHANGED