run402 1.53.0 → 1.54.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.
- package/core-dist/keystore.js +65 -21
- package/lib/apps.mjs +1 -1
- package/lib/domains.mjs +13 -4
- package/lib/email.mjs +5 -10
- package/lib/init.mjs +30 -6
- package/lib/projects.mjs +23 -6
- package/lib/sites.mjs +84 -1
- package/lib/subdomains.mjs +23 -7
- package/package.json +1 -1
- package/sdk/core-dist/keystore.js +65 -21
- package/sdk/dist/errors.d.ts +91 -0
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js +162 -6
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +4 -2
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +15 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/admin.d.ts +4 -1
- package/sdk/dist/namespaces/admin.d.ts.map +1 -1
- package/sdk/dist/namespaces/admin.js +1 -1
- package/sdk/dist/namespaces/admin.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts +27 -30
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/auth.d.ts.map +1 -1
- package/sdk/dist/namespaces/auth.js +10 -1
- package/sdk/dist/namespaces/auth.js.map +1 -1
- package/sdk/dist/namespaces/billing.d.ts +15 -6
- package/sdk/dist/namespaces/billing.d.ts.map +1 -1
- package/sdk/dist/namespaces/billing.js.map +1 -1
- package/sdk/dist/namespaces/contracts.d.ts +67 -15
- package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
- package/sdk/dist/namespaces/contracts.js +32 -8
- package/sdk/dist/namespaces/contracts.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts +5 -4
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +13 -7
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/domains.d.ts +5 -1
- package/sdk/dist/namespaces/domains.d.ts.map +1 -1
- package/sdk/dist/namespaces/domains.js +1 -1
- package/sdk/dist/namespaces/domains.js.map +1 -1
- package/sdk/dist/namespaces/email.d.ts +21 -13
- package/sdk/dist/namespaces/email.d.ts.map +1 -1
- package/sdk/dist/namespaces/email.js +5 -4
- package/sdk/dist/namespaces/email.js.map +1 -1
- package/sdk/dist/namespaces/functions.d.ts +2 -2
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +1 -1
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/namespaces/functions.types.d.ts +4 -0
- package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +3 -5
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +9 -1
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.d.ts +5 -1
- package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.js +1 -1
- package/sdk/dist/namespaces/secrets.js.map +1 -1
- package/sdk/dist/namespaces/sender-domain.d.ts +4 -1
- package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
- package/sdk/dist/namespaces/sender-domain.js +1 -1
- package/sdk/dist/namespaces/sender-domain.js.map +1 -1
- package/sdk/dist/namespaces/service.d.ts +11 -31
- package/sdk/dist/namespaces/service.d.ts.map +1 -1
- package/sdk/dist/namespaces/service.js.map +1 -1
- package/sdk/dist/namespaces/subdomains.d.ts +12 -2
- package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
- package/sdk/dist/namespaces/subdomains.js +23 -18
- package/sdk/dist/namespaces/subdomains.js.map +1 -1
- package/sdk/dist/namespaces/tier.d.ts +9 -1
- package/sdk/dist/namespaces/tier.d.ts.map +1 -1
- package/sdk/dist/namespaces/tier.js.map +1 -1
- package/sdk/dist/node/index.d.ts +2 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +1 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/retry.d.ts +50 -0
- package/sdk/dist/retry.d.ts.map +1 -0
- package/sdk/dist/retry.js +61 -0
- package/sdk/dist/retry.js.map +1 -0
- package/sdk/dist/scoped.d.ts +13 -13
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js.map +1 -1
package/core-dist/keystore.js
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, rmdirSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { getKeystorePath } from "./config.js";
|
|
5
|
+
function withFileLock(path, fn, { retries = 200, delayMs = 20 } = {}) {
|
|
6
|
+
const lockDir = path + ".lock";
|
|
7
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
8
|
+
for (let i = 0; i < retries; i++) {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(lockDir, { mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
const code = e.code;
|
|
14
|
+
if (code !== "EEXIST")
|
|
15
|
+
throw e;
|
|
16
|
+
const until = Date.now() + delayMs;
|
|
17
|
+
while (Date.now() < until) { /* spin */ }
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return fn();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
try {
|
|
25
|
+
rmdirSync(lockDir);
|
|
26
|
+
}
|
|
27
|
+
catch { /* best-effort */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Could not acquire keystore lock after ${retries} retries: ${lockDir}`);
|
|
31
|
+
}
|
|
5
32
|
/**
|
|
6
33
|
* Load the keystore from disk.
|
|
7
34
|
* Auto-migrates legacy formats:
|
|
@@ -13,7 +40,6 @@ export function loadKeyStore(path) {
|
|
|
13
40
|
try {
|
|
14
41
|
const data = readFileSync(p, "utf-8");
|
|
15
42
|
const parsed = JSON.parse(data);
|
|
16
|
-
// Auto-migrate array format (CLI legacy) to object format
|
|
17
43
|
if (Array.isArray(parsed)) {
|
|
18
44
|
const projects = {};
|
|
19
45
|
for (const item of parsed) {
|
|
@@ -29,7 +55,6 @@ export function loadKeyStore(path) {
|
|
|
29
55
|
return { projects };
|
|
30
56
|
}
|
|
31
57
|
if (parsed && typeof parsed === "object" && parsed.projects) {
|
|
32
|
-
// Strip legacy fields (tier, lease_expires_at, expires_at) from projects
|
|
33
58
|
for (const proj of Object.values(parsed.projects)) {
|
|
34
59
|
const rec = proj;
|
|
35
60
|
delete rec.tier;
|
|
@@ -38,6 +63,7 @@ export function loadKeyStore(path) {
|
|
|
38
63
|
}
|
|
39
64
|
return {
|
|
40
65
|
...(parsed.active_project_id && { active_project_id: parsed.active_project_id }),
|
|
66
|
+
...(parsed.previous_active_project_id && { previous_active_project_id: parsed.previous_active_project_id }),
|
|
41
67
|
projects: parsed.projects,
|
|
42
68
|
};
|
|
43
69
|
}
|
|
@@ -62,27 +88,40 @@ export function getProject(projectId, path) {
|
|
|
62
88
|
}
|
|
63
89
|
export function saveProject(projectId, project, path) {
|
|
64
90
|
const p = path ?? getKeystorePath();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
withFileLock(p, () => {
|
|
92
|
+
const store = loadKeyStore(p);
|
|
93
|
+
store.projects[projectId] = project;
|
|
94
|
+
saveKeyStore(store, p);
|
|
95
|
+
});
|
|
68
96
|
}
|
|
69
97
|
export function updateProject(projectId, update, path) {
|
|
70
98
|
const p = path ?? getKeystorePath();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
withFileLock(p, () => {
|
|
100
|
+
const store = loadKeyStore(p);
|
|
101
|
+
const existing = store.projects[projectId];
|
|
102
|
+
if (existing) {
|
|
103
|
+
store.projects[projectId] = { ...existing, ...update };
|
|
104
|
+
saveKeyStore(store, p);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
77
107
|
}
|
|
78
108
|
export function removeProject(projectId, path) {
|
|
79
109
|
const p = path ?? getKeystorePath();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
withFileLock(p, () => {
|
|
111
|
+
const store = loadKeyStore(p);
|
|
112
|
+
delete store.projects[projectId];
|
|
113
|
+
if (store.active_project_id === projectId) {
|
|
114
|
+
const fallback = store.previous_active_project_id;
|
|
115
|
+
if (fallback && fallback !== projectId && store.projects[fallback]) {
|
|
116
|
+
store.active_project_id = fallback;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
delete store.active_project_id;
|
|
120
|
+
}
|
|
121
|
+
delete store.previous_active_project_id;
|
|
122
|
+
}
|
|
123
|
+
saveKeyStore(store, p);
|
|
124
|
+
});
|
|
86
125
|
}
|
|
87
126
|
export function getActiveProjectId(path) {
|
|
88
127
|
const store = loadKeyStore(path);
|
|
@@ -90,8 +129,13 @@ export function getActiveProjectId(path) {
|
|
|
90
129
|
}
|
|
91
130
|
export function setActiveProjectId(projectId, path) {
|
|
92
131
|
const p = path ?? getKeystorePath();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
withFileLock(p, () => {
|
|
133
|
+
const store = loadKeyStore(p);
|
|
134
|
+
if (store.active_project_id && store.active_project_id !== projectId) {
|
|
135
|
+
store.previous_active_project_id = store.active_project_id;
|
|
136
|
+
}
|
|
137
|
+
store.active_project_id = projectId;
|
|
138
|
+
saveKeyStore(store, p);
|
|
139
|
+
});
|
|
96
140
|
}
|
|
97
141
|
//# sourceMappingURL=keystore.js.map
|
package/lib/apps.mjs
CHANGED
|
@@ -72,7 +72,7 @@ Arguments:
|
|
|
72
72
|
Options:
|
|
73
73
|
--description <d> Human-readable description of the app
|
|
74
74
|
--tags <t1,t2> Comma-separated list of tags
|
|
75
|
-
--visibility <v> Visibility: 'public' or 'private'
|
|
75
|
+
--visibility <v> Visibility: 'public', 'unlisted', or 'private'
|
|
76
76
|
--fork-allowed Allow other users to fork this app
|
|
77
77
|
|
|
78
78
|
Examples:
|
package/lib/domains.mjs
CHANGED
|
@@ -11,14 +11,14 @@ Subcommands:
|
|
|
11
11
|
add <domain> <subdomain_name> [--project <id>] Register a custom domain
|
|
12
12
|
list [<id>] List custom domains for a project
|
|
13
13
|
status <domain> [--project <id>] Check domain DNS/SSL status
|
|
14
|
-
delete <domain> [--project <id>]
|
|
14
|
+
delete <domain> --confirm [--project <id>] Release a custom domain. Requires --confirm.
|
|
15
15
|
|
|
16
16
|
Examples:
|
|
17
17
|
run402 domains add example.com myapp
|
|
18
18
|
run402 domains add example.com myapp --project prj_123
|
|
19
19
|
run402 domains list
|
|
20
20
|
run402 domains status example.com
|
|
21
|
-
run402 domains delete example.com
|
|
21
|
+
run402 domains delete example.com --confirm
|
|
22
22
|
|
|
23
23
|
Notes:
|
|
24
24
|
- After adding a domain, configure DNS as shown in the response
|
|
@@ -75,8 +75,17 @@ async function status(args) {
|
|
|
75
75
|
|
|
76
76
|
async function deleteDomain(args) {
|
|
77
77
|
const { project, rest } = parseProjectFlag(args);
|
|
78
|
-
const domain = rest
|
|
79
|
-
if (!domain) { console.error("Usage: run402 domains delete <domain> [--project <id>]"); process.exit(1); }
|
|
78
|
+
const domain = rest.find((a) => !a.startsWith("--"));
|
|
79
|
+
if (!domain) { console.error("Usage: run402 domains delete <domain> --confirm [--project <id>]"); process.exit(1); }
|
|
80
|
+
if (!Array.isArray(args) || !args.includes("--confirm")) {
|
|
81
|
+
console.error(JSON.stringify({
|
|
82
|
+
status: "error",
|
|
83
|
+
code: "CONFIRMATION_REQUIRED",
|
|
84
|
+
message: `Destructive: releasing custom domain '${domain}' detaches it from this project and clears its DNS/SSL configuration. This is irreversible. Re-run with --confirm to proceed.`,
|
|
85
|
+
details: { domain },
|
|
86
|
+
}));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
80
89
|
const projectId = resolveProjectId(project);
|
|
81
90
|
try {
|
|
82
91
|
await getSdk().domains.remove(domain, { projectId });
|
package/lib/email.mjs
CHANGED
|
@@ -167,12 +167,7 @@ async function create(args) {
|
|
|
167
167
|
|
|
168
168
|
try {
|
|
169
169
|
const data = await getSdk().email.createMailbox(projectId, slug);
|
|
170
|
-
|
|
171
|
-
if (data.slug) {
|
|
172
|
-
console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, slug: data.slug }));
|
|
173
|
-
} else {
|
|
174
|
-
console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, already_existed: true }));
|
|
175
|
-
}
|
|
170
|
+
console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, slug: data.slug }));
|
|
176
171
|
} catch (err) {
|
|
177
172
|
reportSdkError(err);
|
|
178
173
|
}
|
|
@@ -203,7 +198,7 @@ async function send(args) {
|
|
|
203
198
|
text: text ?? undefined,
|
|
204
199
|
from_name: fromName ?? undefined,
|
|
205
200
|
});
|
|
206
|
-
console.log(JSON.stringify({ status: "ok", message_id: data.
|
|
201
|
+
console.log(JSON.stringify({ status: "ok", message_id: data.message_id, to: data.to, template: data.template, subject: data.subject }));
|
|
207
202
|
} catch (err) {
|
|
208
203
|
reportSdkError(err);
|
|
209
204
|
}
|
|
@@ -325,7 +320,7 @@ async function reply(args) {
|
|
|
325
320
|
from_name: fromName ?? undefined,
|
|
326
321
|
in_reply_to: messageId,
|
|
327
322
|
});
|
|
328
|
-
console.log(JSON.stringify({ status: "ok", message_id: data.
|
|
323
|
+
console.log(JSON.stringify({ status: "ok", message_id: data.message_id, to: data.to, subject: replySubject, in_reply_to: messageId }));
|
|
329
324
|
} catch (err) {
|
|
330
325
|
reportSdkError(err);
|
|
331
326
|
}
|
|
@@ -352,8 +347,8 @@ async function deleteMailbox(args) {
|
|
|
352
347
|
}
|
|
353
348
|
|
|
354
349
|
try {
|
|
355
|
-
await getSdk().email.deleteMailbox(projectId, positional ?? undefined);
|
|
356
|
-
console.log(JSON.stringify({ status: "ok", mailbox_id:
|
|
350
|
+
const data = await getSdk().email.deleteMailbox(projectId, positional ?? undefined);
|
|
351
|
+
console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, deleted: true }));
|
|
357
352
|
} catch (err) {
|
|
358
353
|
reportSdkError(err);
|
|
359
354
|
}
|
package/lib/init.mjs
CHANGED
|
@@ -10,12 +10,23 @@ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
|
|
|
10
10
|
const HELP = `run402 init — Set up allowance, funding, and check tier status
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
|
-
run402 init
|
|
14
|
-
run402 init mpp
|
|
15
|
-
run402 init --
|
|
16
|
-
|
|
13
|
+
run402 init Set up with x402 (Base Sepolia) — default
|
|
14
|
+
run402 init mpp Set up with MPP (Tempo Moderato)
|
|
15
|
+
run402 init <rail> --switch-rail
|
|
16
|
+
Switch the persisted payment rail to <rail>.
|
|
17
|
+
Required when an allowance already exists on
|
|
18
|
+
the other rail; protects scripted re-runs from
|
|
19
|
+
silently flipping billing networks.
|
|
20
|
+
run402 init --json Same as init, but emit a JSON summary on stdout
|
|
21
|
+
(human lines go to stderr — for agent automation)
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
Options:
|
|
24
|
+
--switch-rail Confirm switching the persisted payment rail. Re-running
|
|
25
|
+
init with the SAME rail as the existing allowance is always
|
|
26
|
+
idempotent and does not need this flag.
|
|
27
|
+
--json Emit a structured JSON summary on stdout.
|
|
28
|
+
|
|
29
|
+
Steps (idempotent when re-run with the same rail; pass --switch-rail to change rails):
|
|
19
30
|
1. Creates config directory (~/.config/run402)
|
|
20
31
|
2. Creates agent allowance if none exists
|
|
21
32
|
3. Checks on-chain balance; requests faucet if zero
|
|
@@ -32,6 +43,19 @@ export async function run(args = []) {
|
|
|
32
43
|
if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
|
|
33
44
|
const jsonMode = args.includes("--json");
|
|
34
45
|
const isMpp = args[0] === "mpp";
|
|
46
|
+
const requestedRail = isMpp ? "mpp" : "x402";
|
|
47
|
+
const switchRailConfirmed = args.includes("--switch-rail");
|
|
48
|
+
|
|
49
|
+
const existingAllowance = readAllowance();
|
|
50
|
+
if (existingAllowance?.rail && existingAllowance.rail !== requestedRail && !switchRailConfirmed) {
|
|
51
|
+
console.error(JSON.stringify({
|
|
52
|
+
status: "error",
|
|
53
|
+
code: "RAIL_SWITCH_REQUIRES_CONFIRM",
|
|
54
|
+
message: `Already on rail '${existingAllowance.rail}'. Pass --switch-rail to switch to '${requestedRail}'.`,
|
|
55
|
+
details: { current_rail: existingAllowance.rail, requested_rail: requestedRail },
|
|
56
|
+
}));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
35
59
|
|
|
36
60
|
// In --json mode, human-readable lines go to stderr so stdout stays clean for
|
|
37
61
|
// agents. We also collect structured data for the final JSON emit.
|
|
@@ -55,7 +79,7 @@ export async function run(args = []) {
|
|
|
55
79
|
line("Config", CONFIG_DIR);
|
|
56
80
|
|
|
57
81
|
// 2. Allowance
|
|
58
|
-
let allowance =
|
|
82
|
+
let allowance = existingAllowance;
|
|
59
83
|
const previousRail = allowance?.rail;
|
|
60
84
|
if (!allowance) {
|
|
61
85
|
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
package/lib/projects.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
|
|
2
|
+
import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
|
|
3
3
|
import { getSdk } from "./sdk.mjs";
|
|
4
4
|
import { reportSdkError } from "./sdk-errors.mjs";
|
|
5
5
|
|
|
@@ -22,7 +22,7 @@ Subcommands:
|
|
|
22
22
|
apply-expose [id] <manifest_json> Apply a declarative authorization manifest
|
|
23
23
|
apply-expose [id] --file <path> Apply a manifest from a JSON file
|
|
24
24
|
get-expose [id] Get the current authorization manifest
|
|
25
|
-
delete [id]
|
|
25
|
+
delete [id] --confirm Immediately and irreversibly delete a project (cascade purge) and remove from local state. Requires --confirm.
|
|
26
26
|
pin [id] Pin a project (admin only; project owners get 403 admin_required)
|
|
27
27
|
promote-user [id] <email> Promote a user to project_admin role
|
|
28
28
|
demote-user [id] <email> Demote a user from project_admin role
|
|
@@ -43,7 +43,7 @@ Examples:
|
|
|
43
43
|
run402 projects apply-expose abc123 --file manifest.json
|
|
44
44
|
run402 projects get-expose abc123
|
|
45
45
|
run402 projects keys abc123
|
|
46
|
-
run402 projects delete abc123
|
|
46
|
+
run402 projects delete abc123 --confirm
|
|
47
47
|
|
|
48
48
|
Notes:
|
|
49
49
|
- <id> is the project_id shown in 'run402 projects list' (prefix: 'prj_')
|
|
@@ -128,9 +128,16 @@ async function provision(args) {
|
|
|
128
128
|
// gives the user a more specific prompt than the SDK's 401/402 path.
|
|
129
129
|
allowanceAuthHeaders("/projects/v1");
|
|
130
130
|
|
|
131
|
+
const activeBefore = getActiveProjectId();
|
|
131
132
|
try {
|
|
132
133
|
const data = await getSdk().projects.provision({ tier: opts.tier, name: opts.name });
|
|
133
|
-
|
|
134
|
+
const activeAfter = getActiveProjectId();
|
|
135
|
+
const out = { ...data };
|
|
136
|
+
if (activeBefore && activeAfter && activeBefore !== activeAfter) {
|
|
137
|
+
out.note = `active project changed: ${activeBefore} -> ${activeAfter}`;
|
|
138
|
+
out.previous_active_project_id = activeBefore;
|
|
139
|
+
}
|
|
140
|
+
console.log(JSON.stringify(out, null, 2));
|
|
134
141
|
} catch (err) {
|
|
135
142
|
reportSdkError(err);
|
|
136
143
|
}
|
|
@@ -302,7 +309,17 @@ async function demoteUser(projectId, email) {
|
|
|
302
309
|
console.log(JSON.stringify(data, null, 2));
|
|
303
310
|
}
|
|
304
311
|
|
|
305
|
-
async function deleteProject(projectId) {
|
|
312
|
+
async function deleteProject(projectId, args = []) {
|
|
313
|
+
const confirmed = Array.isArray(args) && args.includes("--confirm");
|
|
314
|
+
if (!confirmed) {
|
|
315
|
+
console.error(JSON.stringify({
|
|
316
|
+
status: "error",
|
|
317
|
+
code: "CONFIRMATION_REQUIRED",
|
|
318
|
+
message: `Destructive: deleting project ${projectId} drops all DB schemas, functions, subdomains, mailbox, blobs, and secrets. This is irreversible. Re-run with --confirm to proceed.`,
|
|
319
|
+
details: { project_id: projectId, destroys: ["schemas", "functions", "subdomains", "mailbox", "blobs", "secrets"] },
|
|
320
|
+
}));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
306
323
|
try {
|
|
307
324
|
await getSdk().projects.delete(projectId);
|
|
308
325
|
console.log(JSON.stringify({ status: "ok", message: `Project ${projectId} deleted.` }));
|
|
@@ -346,7 +363,7 @@ export async function run(sub, args) {
|
|
|
346
363
|
case "schema": { const { projectId } = resolvePositionalProject(args); await schema(projectId); break; }
|
|
347
364
|
case "apply-expose": { const { projectId, rest } = resolvePositionalProject(args); await applyExpose(projectId, rest); break; }
|
|
348
365
|
case "get-expose": { const { projectId } = resolvePositionalProject(args); await getExpose(projectId); break; }
|
|
349
|
-
case "delete": { const { projectId } = resolvePositionalProject(args); await deleteProject(projectId); break; }
|
|
366
|
+
case "delete": { const { projectId, rest } = resolvePositionalProject(args); await deleteProject(projectId, rest); break; }
|
|
350
367
|
case "pin": { const { projectId } = resolvePositionalProject(args); await pin(projectId); break; }
|
|
351
368
|
case "promote-user": { const { projectId, rest } = resolvePositionalProject(args); await promoteUser(projectId, rest[0]); break; }
|
|
352
369
|
case "demote-user": { const { projectId, rest } = resolvePositionalProject(args); await demoteUser(projectId, rest[0]); break; }
|
package/lib/sites.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { fileSetFromDir } from "#sdk/node";
|
|
4
5
|
import { allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
|
|
5
6
|
import { resolveFilePathsInManifest } from "./manifest.mjs";
|
|
6
7
|
import { getSdk } from "./sdk.mjs";
|
|
7
8
|
import { reportSdkError } from "./sdk-errors.mjs";
|
|
8
9
|
|
|
10
|
+
const SMALL_DIR_THRESHOLD = 5;
|
|
11
|
+
|
|
9
12
|
const HELP = `run402 sites - Deploy and manage static sites
|
|
10
13
|
|
|
11
14
|
Usage:
|
|
@@ -28,6 +31,9 @@ Options (deploy-dir):
|
|
|
28
31
|
--project <id> Project ID (defaults to active project)
|
|
29
32
|
--target <target> Deployment target (e.g. 'production')
|
|
30
33
|
--quiet Suppress progress events on stderr
|
|
34
|
+
--dry-run Plan-only: print the diff envelope and exit
|
|
35
|
+
--confirm-prune Required when <path> has fewer files than the
|
|
36
|
+
small-dir guardrail threshold
|
|
31
37
|
|
|
32
38
|
Manifest format (JSON):
|
|
33
39
|
{
|
|
@@ -97,6 +103,7 @@ Examples:
|
|
|
97
103
|
|
|
98
104
|
Usage:
|
|
99
105
|
run402 sites deploy-dir <path> [--project <id>] [--target <target>] [--quiet]
|
|
106
|
+
[--dry-run] [--confirm-prune]
|
|
100
107
|
|
|
101
108
|
Arguments:
|
|
102
109
|
<path> Local directory to deploy (positional, required)
|
|
@@ -106,11 +113,23 @@ Options:
|
|
|
106
113
|
--target <target> Deployment target (e.g. 'production')
|
|
107
114
|
--quiet Suppress progress events on stderr (events are on by
|
|
108
115
|
default — see Progress events below)
|
|
116
|
+
--dry-run Plan the deploy and print a JSON envelope describing
|
|
117
|
+
what would happen, then exit. Does NOT upload bytes
|
|
118
|
+
or commit a release.
|
|
119
|
+
--confirm-prune Acknowledge that deploying <path> may remove files
|
|
120
|
+
that exist in the current site but are absent from
|
|
121
|
+
<path>. Required when <path> contains fewer than
|
|
122
|
+
${SMALL_DIR_THRESHOLD} files (the small-dir guardrail).
|
|
109
123
|
|
|
110
124
|
Behavior:
|
|
111
125
|
- Walks <path> recursively, skips .git / node_modules / .DS_Store
|
|
112
126
|
- Computes per-file SHA-256 and uploads only bytes the gateway doesn't
|
|
113
127
|
already have (CAS-backed unified deploy primitive)
|
|
128
|
+
- A static-site deploy REPLACES the live release: any path in the current
|
|
129
|
+
site that is absent from <path> is removed from the new release. To
|
|
130
|
+
avoid accidentally wiping a multi-page site by deploying a single-file
|
|
131
|
+
directory, a small <path> (fewer than ${SMALL_DIR_THRESHOLD} files) requires
|
|
132
|
+
--confirm-prune.
|
|
114
133
|
- Symlinks are rejected (no following)
|
|
115
134
|
- Paths in the manifest are POSIX-style relative to <path>
|
|
116
135
|
|
|
@@ -131,6 +150,8 @@ Examples:
|
|
|
131
150
|
run402 sites deploy-dir ./dist --project prj_abc
|
|
132
151
|
run402 sites deploy-dir ./my-site --project prj_abc --target production
|
|
133
152
|
run402 sites deploy-dir ./dist --project prj_abc --quiet
|
|
153
|
+
run402 sites deploy-dir ./tiny-site --project prj_abc --confirm-prune
|
|
154
|
+
run402 sites deploy-dir ./dist --project prj_abc --dry-run
|
|
134
155
|
`,
|
|
135
156
|
};
|
|
136
157
|
|
|
@@ -207,12 +228,21 @@ async function deploy(args) {
|
|
|
207
228
|
}
|
|
208
229
|
|
|
209
230
|
async function deployDir(args) {
|
|
210
|
-
const opts = {
|
|
231
|
+
const opts = {
|
|
232
|
+
dir: null,
|
|
233
|
+
project: undefined,
|
|
234
|
+
target: undefined,
|
|
235
|
+
quiet: false,
|
|
236
|
+
dryRun: false,
|
|
237
|
+
confirmPrune: false,
|
|
238
|
+
};
|
|
211
239
|
for (let i = 0; i < args.length; i++) {
|
|
212
240
|
if (args[i] === "--help" || args[i] === "-h") { console.log(SUB_HELP["deploy-dir"]); process.exit(0); }
|
|
213
241
|
if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
|
|
214
242
|
if (args[i] === "--target" && args[i + 1]) { opts.target = args[++i]; continue; }
|
|
215
243
|
if (args[i] === "--quiet") { opts.quiet = true; continue; }
|
|
244
|
+
if (args[i] === "--dry-run") { opts.dryRun = true; continue; }
|
|
245
|
+
if (args[i] === "--confirm-prune") { opts.confirmPrune = true; continue; }
|
|
216
246
|
if (args[i] === "--inherit") {
|
|
217
247
|
console.error(JSON.stringify({
|
|
218
248
|
status: "error",
|
|
@@ -231,6 +261,59 @@ async function deployDir(args) {
|
|
|
231
261
|
// Preserve the aggressive early exit when no allowance is configured.
|
|
232
262
|
allowanceAuthHeaders("/deploy/v2/plans");
|
|
233
263
|
|
|
264
|
+
let fileSet;
|
|
265
|
+
try {
|
|
266
|
+
fileSet = await fileSetFromDir(opts.dir);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
reportSdkError(err);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const fileCount = Object.keys(fileSet).length;
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
fileCount < SMALL_DIR_THRESHOLD &&
|
|
275
|
+
!opts.confirmPrune &&
|
|
276
|
+
!opts.dryRun
|
|
277
|
+
) {
|
|
278
|
+
console.error(JSON.stringify({
|
|
279
|
+
status: "error",
|
|
280
|
+
code: "PRUNE_CONFIRMATION_REQUIRED",
|
|
281
|
+
message:
|
|
282
|
+
`sites deploy-dir would replace the entire site with ${fileCount} ` +
|
|
283
|
+
`file(s) from ${opts.dir}. Any files in the current release that ` +
|
|
284
|
+
`are absent from this directory will be removed. Pass ` +
|
|
285
|
+
`--confirm-prune to proceed, or --dry-run to preview the diff.`,
|
|
286
|
+
details: {
|
|
287
|
+
local_file_count: fileCount,
|
|
288
|
+
threshold: SMALL_DIR_THRESHOLD,
|
|
289
|
+
dir: opts.dir,
|
|
290
|
+
},
|
|
291
|
+
}));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (opts.dryRun) {
|
|
296
|
+
try {
|
|
297
|
+
const { plan } = await getSdk().deploy.plan({
|
|
298
|
+
project: projectId,
|
|
299
|
+
site: { replace: fileSet },
|
|
300
|
+
});
|
|
301
|
+
console.log(JSON.stringify({
|
|
302
|
+
status: "ok",
|
|
303
|
+
dry_run: true,
|
|
304
|
+
local_file_count: fileCount,
|
|
305
|
+
plan_id: plan.plan_id,
|
|
306
|
+
operation_id: plan.operation_id,
|
|
307
|
+
manifest_digest: plan.manifest_digest,
|
|
308
|
+
diff: plan.diff,
|
|
309
|
+
missing_content_count: plan.missing_content.filter((p) => !p.present).length,
|
|
310
|
+
}, null, 2));
|
|
311
|
+
} catch (err) {
|
|
312
|
+
reportSdkError(err);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
234
317
|
try {
|
|
235
318
|
const data = await getSdk().sites.deployDir({
|
|
236
319
|
project: projectId,
|
package/lib/subdomains.mjs
CHANGED
|
@@ -9,7 +9,7 @@ Usage:
|
|
|
9
9
|
|
|
10
10
|
Subcommands:
|
|
11
11
|
claim <name> [--project <id>] [--deployment <id>] Claim a subdomain
|
|
12
|
-
delete <name> [--project <id>]
|
|
12
|
+
delete <name> --confirm [--project <id>] Release a subdomain. Requires --confirm.
|
|
13
13
|
list [<id>] List subdomains for a project
|
|
14
14
|
|
|
15
15
|
Options default to the active project and its last deployment when omitted.
|
|
@@ -18,7 +18,7 @@ Legacy syntax 'claim <deployment_id> <name>' is still supported.
|
|
|
18
18
|
Examples:
|
|
19
19
|
run402 subdomains claim myapp
|
|
20
20
|
run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
|
|
21
|
-
run402 subdomains delete myapp
|
|
21
|
+
run402 subdomains delete myapp --confirm
|
|
22
22
|
run402 subdomains list
|
|
23
23
|
|
|
24
24
|
Notes:
|
|
@@ -77,10 +77,26 @@ async function claim(positionalArgs, flagArgs) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
async function deleteSubdomain(
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
async function deleteSubdomain(allArgs) {
|
|
81
|
+
const argList = Array.isArray(allArgs) ? allArgs : [];
|
|
82
|
+
let opts = { project: null };
|
|
83
|
+
let name = null;
|
|
84
|
+
for (let i = 0; i < argList.length; i++) {
|
|
85
|
+
if (argList[i] === "--project" && argList[i + 1]) { opts.project = argList[++i]; }
|
|
86
|
+
else if (!argList[i].startsWith("--") && !name) { name = argList[i]; }
|
|
87
|
+
}
|
|
88
|
+
if (!name) {
|
|
89
|
+
console.error(JSON.stringify({ status: "error", message: "Usage: run402 subdomains delete <name> --confirm [--project <id>]" }));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (!argList.includes("--confirm")) {
|
|
93
|
+
console.error(JSON.stringify({
|
|
94
|
+
status: "error",
|
|
95
|
+
code: "CONFIRMATION_REQUIRED",
|
|
96
|
+
message: `Destructive: releasing subdomain '${name}' makes it available for any other project to claim. This is irreversible. Re-run with --confirm to proceed.`,
|
|
97
|
+
details: { name },
|
|
98
|
+
}));
|
|
99
|
+
process.exit(1);
|
|
84
100
|
}
|
|
85
101
|
const projectId = resolveProjectId(opts.project);
|
|
86
102
|
try {
|
|
@@ -116,7 +132,7 @@ export async function run(sub, args) {
|
|
|
116
132
|
await claim(positional, flags);
|
|
117
133
|
break;
|
|
118
134
|
}
|
|
119
|
-
case "delete": await deleteSubdomain(args
|
|
135
|
+
case "delete": await deleteSubdomain(args); break;
|
|
120
136
|
case "list": await list(args[0]); break;
|
|
121
137
|
default:
|
|
122
138
|
console.error(`Unknown subcommand: ${sub}\n`);
|
package/package.json
CHANGED