shiply-cli 0.28.0 → 0.29.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/dist/data.js +1 -0
- package/dist/db.js +2 -0
- package/dist/domain.js +30 -0
- package/dist/drive.js +15 -4
- package/dist/index.js +94 -5
- package/dist/login.js +9 -3
- package/dist/publish.js +6 -1
- package/dist/sending-domains.js +1 -0
- package/dist/sites.js +1 -0
- package/dist/variable.js +80 -0
- package/package.json +2 -2
- package/skill/SKILL.md +2 -0
- package/skill/references/client-work.md +93 -12
- package/skill/references/custom-domains.md +2 -0
- package/skill/references/databases.md +5 -0
- package/skill/references/drives.md +70 -0
- package/skill/references/email.md +24 -0
- package/skill/references/publishing.md +3 -1
- package/skill/references/site-features.md +20 -7
package/dist/data.js
CHANGED
|
@@ -105,6 +105,7 @@ export async function dataExport(ctx, slug, collection, opts) {
|
|
|
105
105
|
export async function dataWipeCollection(ctx, slug, collection, opts) {
|
|
106
106
|
if (!opts.yes) {
|
|
107
107
|
console.error(` refusing — wipe-collection deletes EVERY record in "${collection}" on ${slug}. Re-run with --yes to confirm.`);
|
|
108
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
const base = resolveBase(ctx.base);
|
package/dist/db.js
CHANGED
|
@@ -144,6 +144,7 @@ export async function dbMigrate(ctx, idOrName, dir) {
|
|
|
144
144
|
export async function dbDelete(ctx, idOrName, opts) {
|
|
145
145
|
if (!opts.yes) {
|
|
146
146
|
console.log(` refusing — \`db delete\` drops the database "${idOrName}" and every row in it. Re-run with --yes to confirm.`);
|
|
147
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
const id = await resolveId(ctx, idOrName);
|
|
@@ -195,6 +196,7 @@ export async function dbBranches(ctx, parentNameOrId) {
|
|
|
195
196
|
export async function dbDeleteBranch(ctx, branchIdOrName, opts) {
|
|
196
197
|
if (!opts.yes) {
|
|
197
198
|
console.log(` refusing — \`db delete-branch\` drops the branch "${branchIdOrName}" and its Neon endpoint. Re-run with --yes to confirm.`);
|
|
199
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
const base = resolveBase(ctx.base);
|
package/dist/domain.js
CHANGED
|
@@ -143,6 +143,36 @@ export async function domainPrimary(ctx, domain, hostname) {
|
|
|
143
143
|
console.log(` ${s.hostname}`);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
export async function domainRm(ctx, domain, opts = {}) {
|
|
147
|
+
if (!opts.yes) {
|
|
148
|
+
throw new Error(`refusing to remove ${domain} without --yes (subdomains mapped to it stop resolving)`);
|
|
149
|
+
}
|
|
150
|
+
const base = resolveBase(ctx.base);
|
|
151
|
+
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
headers: headers(ctx.apiKey),
|
|
154
|
+
});
|
|
155
|
+
console.log(`✔ removed ${r.removed}`);
|
|
156
|
+
}
|
|
157
|
+
/** Re-poll CF cert status + live TLS/HTTPS probe for all subdomains of a
|
|
158
|
+
* registered custom domain. Run this after `domain connect` / `domain sub add`
|
|
159
|
+
* until every subdomain reports ready=true. */
|
|
160
|
+
export async function domainCheck(ctx, domain) {
|
|
161
|
+
const base = resolveBase(ctx.base);
|
|
162
|
+
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/check`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: headers(ctx.apiKey),
|
|
165
|
+
body: '{}',
|
|
166
|
+
});
|
|
167
|
+
if (!r.subdomains?.length) {
|
|
168
|
+
console.log(`No subdomains mapped under ${domain} yet. shiply domain sub add <host> --site <slug>`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const s of r.subdomains) {
|
|
172
|
+
console.log(` ${s.hostname} ${s.ready ? '✔ ready' : '✖ not ready'}`);
|
|
173
|
+
console.log(`DOMAIN_CHECK hostname=${s.hostname} ready=${s.ready}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
146
176
|
export async function domainSync(ctx, domain) {
|
|
147
177
|
const base = resolveBase(ctx.base);
|
|
148
178
|
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/sync-dns`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
|
package/dist/drive.js
CHANGED
|
@@ -30,8 +30,12 @@ export async function drivePut(ctx, file, asPath) {
|
|
|
30
30
|
const stage = await api(`${base}/api/v1/drives/${id}/files/uploads`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify({ files: [meta] }) });
|
|
31
31
|
for (const u of stage.uploads) {
|
|
32
32
|
const res = await fetch(u.url, { method: 'PUT', body: new Uint8Array(body) });
|
|
33
|
-
if (!res.ok)
|
|
34
|
-
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
if (res.status === 401 || res.status === 403) {
|
|
35
|
+
throw new Error(`upload failed for ${u.path}: HTTP ${res.status} — the upload URL is not authorized. Re-run \`shiply drive put\` to mint a fresh one.`);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`upload failed for ${u.path}: HTTP ${res.status} — the object store rejected the file. Re-run \`shiply drive put\`; if it keeps failing, check the file size/name.`);
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
await api(`${base}/api/v1/drives/${id}/files/finalize`, {
|
|
37
41
|
method: 'POST',
|
|
@@ -45,8 +49,15 @@ export async function driveGet(ctx, path, out) {
|
|
|
45
49
|
const id = await defaultDrive(ctx);
|
|
46
50
|
const { url } = await api(`${base}/api/v1/drives/${id}/files/${path.split('/').map(encodeURIComponent).join('/')}`, { headers: headers(ctx.apiKey) });
|
|
47
51
|
const res = await fetch(url);
|
|
48
|
-
if (!res.ok)
|
|
49
|
-
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
if (res.status === 404) {
|
|
54
|
+
throw new Error(`download failed: HTTP 404 — no file at "${path}" in this drive. Check the path with \`shiply drive ls\`.`);
|
|
55
|
+
}
|
|
56
|
+
if (res.status === 401 || res.status === 403) {
|
|
57
|
+
throw new Error(`download failed: HTTP ${res.status} — the download URL is not authorized. Re-run \`shiply drive get\` to mint a fresh one.`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`download failed: HTTP ${res.status} for "${path}" — re-run \`shiply drive get\`; if it keeps failing, the object may be corrupted.`);
|
|
60
|
+
}
|
|
50
61
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
51
62
|
if (out) {
|
|
52
63
|
await writeFile(out, buf);
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { detectIsr } from './isr.js';
|
|
|
15
15
|
import { runClaimVerify } from './claim.js';
|
|
16
16
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
17
17
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
18
|
-
import { domainAdd, domainConnect, domainLs, domainPrimary, domainSubAdd, domainSync } from './domain.js';
|
|
18
|
+
import { domainAdd, domainCheck, domainConnect, domainLs, domainPrimary, domainRm, domainSubAdd, domainSync } from './domain.js';
|
|
19
19
|
import { runProject } from './projects.js';
|
|
20
20
|
import { runClient, parseClientArg } from './clients.js';
|
|
21
21
|
import { runListing } from './listings.js';
|
|
@@ -36,6 +36,7 @@ import { runStatus } from './status-cmd.js';
|
|
|
36
36
|
import { accessSet, accessShow } from './access.js';
|
|
37
37
|
import { sitesLs, sitesPromote, sitesRm, sitesRollback } from './sites.js';
|
|
38
38
|
import { verifySite } from './verify.js';
|
|
39
|
+
import { variableAttach, variableDetach, variableLs, variableRm, variableSet } from './variable.js';
|
|
39
40
|
// Resolve the package.json beside the compiled dist/ so `shiply --version`
|
|
40
41
|
// can print the running CLI version (works in dev tsc output + npm install).
|
|
41
42
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +64,10 @@ Usage:
|
|
|
63
64
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
64
65
|
(no arg → account/plan status, see below)
|
|
65
66
|
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
67
|
+
shiply ls List sites in your account
|
|
68
|
+
shiply rm <slug> --yes Permanently delete an owned site
|
|
69
|
+
shiply rollback <slug> [versionId] Re-point a site to a prior finalized deploy
|
|
70
|
+
(omit versionId to list deploys and pick one)
|
|
66
71
|
shiply promote <preview-slug> --to <dest-slug>
|
|
67
72
|
Copy the exact previewed bytes into a production site (no rebuild)
|
|
68
73
|
shiply detect [dir] Diagnose: print framework + dir that would upload
|
|
@@ -88,6 +93,14 @@ Usage:
|
|
|
88
93
|
shiply domain sub add <host> --site <slug> Map a subdomain (e.g. app.example.com) to a slug
|
|
89
94
|
shiply domain sync <domain> Re-sync DNS records for a connected domain
|
|
90
95
|
shiply domain primary <domain> <hostname> Mark hostname as primary; siblings 301-redirect (SEO)
|
|
96
|
+
shiply domain check <domain> Re-poll cert/DNS/HTTPS readiness for every subdomain
|
|
97
|
+
shiply domain rm <domain> --yes Remove a custom domain (its subdomains stop resolving)
|
|
98
|
+
shiply variable <ls|set|rm|attach|detach> Account-level encrypted variables (e.g. API keys for sites)
|
|
99
|
+
shiply variable set <NAME> <value> Save/update a variable (or: --stdin to read the value)
|
|
100
|
+
shiply variable ls [--reveal] List variables (masked by default)
|
|
101
|
+
shiply variable rm <NAME> --yes Delete a variable
|
|
102
|
+
shiply variable attach <NAME> --site <slug> Expose a saved variable to one site's Worker env
|
|
103
|
+
shiply variable detach <NAME> --site <slug> Stop exposing a variable to a site's Worker env
|
|
91
104
|
shiply project <ls|create|get|archive|restore>
|
|
92
105
|
Customer-intake projects + AI brief generator
|
|
93
106
|
shiply project create <label> [--customer-email <e>] [--customer-name <n>]
|
|
@@ -220,7 +233,7 @@ async function main() {
|
|
|
220
233
|
// parseArgs sees it. (A prior lookahead that cancelled the flag when a
|
|
221
234
|
// positional followed made `shiply publish --json ./dist` swallow the dir as
|
|
222
235
|
// the --json value, leaving no <dir>.)
|
|
223
|
-
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs']);
|
|
236
|
+
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs', 'variable']);
|
|
224
237
|
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json');
|
|
225
238
|
if (bareJsonFlag) {
|
|
226
239
|
const idx = rawArgv.indexOf('--json');
|
|
@@ -263,6 +276,8 @@ async function main() {
|
|
|
263
276
|
ts: { type: 'boolean' },
|
|
264
277
|
'preview-branch': { type: 'string' },
|
|
265
278
|
yes: { type: 'boolean' },
|
|
279
|
+
reveal: { type: 'boolean' },
|
|
280
|
+
stdin: { type: 'boolean' },
|
|
266
281
|
'no-confetti': { type: 'boolean' },
|
|
267
282
|
json: { type: 'string' },
|
|
268
283
|
limit: { type: 'string' },
|
|
@@ -832,8 +847,18 @@ async function main() {
|
|
|
832
847
|
throw new Error('usage: shiply domain primary <domain> <hostname>');
|
|
833
848
|
await domainPrimary(dctx, arg, arg2);
|
|
834
849
|
return;
|
|
850
|
+
case 'rm':
|
|
851
|
+
if (!arg)
|
|
852
|
+
throw new Error('usage: shiply domain rm <domain> --yes');
|
|
853
|
+
await domainRm(dctx, arg, { yes: Boolean(values.yes) });
|
|
854
|
+
return;
|
|
855
|
+
case 'check':
|
|
856
|
+
if (!arg)
|
|
857
|
+
throw new Error('usage: shiply domain check <domain>');
|
|
858
|
+
await domainCheck(dctx, arg);
|
|
859
|
+
return;
|
|
835
860
|
default:
|
|
836
|
-
throw new Error('usage: shiply domain <ls|add|connect|sub|sync|primary>');
|
|
861
|
+
throw new Error('usage: shiply domain <ls|add|connect|sub|sync|primary|rm|check>');
|
|
837
862
|
}
|
|
838
863
|
}
|
|
839
864
|
case 'db': {
|
|
@@ -904,6 +929,55 @@ async function main() {
|
|
|
904
929
|
throw new Error('usage: shiply db <create|ls|sql|migrate|delete|attach|branch|branches|delete-branch|merge>');
|
|
905
930
|
}
|
|
906
931
|
}
|
|
932
|
+
case 'variable': {
|
|
933
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
934
|
+
if (!apiKey)
|
|
935
|
+
throw new Error('variable commands need an API key — run `shiply login` first');
|
|
936
|
+
const vctx = { base: values.base, apiKey };
|
|
937
|
+
const jsonOut = bareJsonFlag;
|
|
938
|
+
const sub = dir; // positionals[1]
|
|
939
|
+
const arg = positionals[2]; // name
|
|
940
|
+
const arg2 = positionals[3]; // value (set) — unused for others
|
|
941
|
+
switch (sub) {
|
|
942
|
+
case 'ls':
|
|
943
|
+
await variableLs(vctx, { reveal: Boolean(values.reveal), json: jsonOut });
|
|
944
|
+
return;
|
|
945
|
+
case 'set': {
|
|
946
|
+
if (!arg)
|
|
947
|
+
throw new Error("usage: shiply variable set <NAME> <value> (or: shiply variable set <NAME> --stdin)");
|
|
948
|
+
let value = arg2;
|
|
949
|
+
if (values.stdin) {
|
|
950
|
+
const { readFileSync } = await import('node:fs');
|
|
951
|
+
value = readFileSync(0, 'utf8').replace(/\n$/, '');
|
|
952
|
+
}
|
|
953
|
+
if (value === undefined)
|
|
954
|
+
throw new Error("usage: shiply variable set <NAME> <value> (or: shiply variable set <NAME> --stdin)");
|
|
955
|
+
await variableSet(vctx, arg, value, { json: jsonOut });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
case 'rm':
|
|
959
|
+
if (!arg)
|
|
960
|
+
throw new Error('usage: shiply variable rm <NAME> --yes');
|
|
961
|
+
await variableRm(vctx, arg, { yes: Boolean(values.yes), json: jsonOut });
|
|
962
|
+
return;
|
|
963
|
+
case 'attach':
|
|
964
|
+
if (!arg)
|
|
965
|
+
throw new Error('usage: shiply variable attach <NAME> --site <slug>');
|
|
966
|
+
if (!values.site)
|
|
967
|
+
throw new Error('--site <slug> is required for `shiply variable attach`');
|
|
968
|
+
await variableAttach(vctx, arg, values.site, { json: jsonOut });
|
|
969
|
+
return;
|
|
970
|
+
case 'detach':
|
|
971
|
+
if (!arg)
|
|
972
|
+
throw new Error('usage: shiply variable detach <NAME> --site <slug>');
|
|
973
|
+
if (!values.site)
|
|
974
|
+
throw new Error('--site <slug> is required for `shiply variable detach`');
|
|
975
|
+
await variableDetach(vctx, arg, values.site, { json: jsonOut });
|
|
976
|
+
return;
|
|
977
|
+
default:
|
|
978
|
+
throw new Error('usage: shiply variable <ls|set|rm|attach|detach>');
|
|
979
|
+
}
|
|
980
|
+
}
|
|
907
981
|
case 'project': {
|
|
908
982
|
await runProject(positionals.slice(1));
|
|
909
983
|
break;
|
|
@@ -1047,14 +1121,17 @@ async function main() {
|
|
|
1047
1121
|
break;
|
|
1048
1122
|
}
|
|
1049
1123
|
case 'claim': {
|
|
1124
|
+
// Normalized to exit 1 (was 2) — every other usage error in this CLI
|
|
1125
|
+
// exits 1, and automation that checks `exit !== 0` shouldn't need a
|
|
1126
|
+
// special case for this one command.
|
|
1050
1127
|
if (dir !== 'verify') {
|
|
1051
1128
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
1052
|
-
process.exit(
|
|
1129
|
+
process.exit(1);
|
|
1053
1130
|
}
|
|
1054
1131
|
const code = positionals[2];
|
|
1055
1132
|
if (!code) {
|
|
1056
1133
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
1057
|
-
process.exit(
|
|
1134
|
+
process.exit(1);
|
|
1058
1135
|
}
|
|
1059
1136
|
const r = await runClaimVerify({ code });
|
|
1060
1137
|
console.log(r.message);
|
|
@@ -1185,6 +1262,18 @@ main().catch((e) => {
|
|
|
1185
1262
|
console.error(` upgrade: https://shiply.now/dashboard/plan`);
|
|
1186
1263
|
process.exit(1);
|
|
1187
1264
|
}
|
|
1265
|
+
// Auth refusals (server returns 401/403): a bare "sign in" tells an agent
|
|
1266
|
+
// nothing about HOW, and a revoked key vs a plan/ownership limit need
|
|
1267
|
+
// different next actions — branch on the code instead of printing raw text.
|
|
1268
|
+
if (e instanceof ApiError && e.code === 'unauthorized') {
|
|
1269
|
+
console.error(`✖ API key missing, invalid, or revoked — run \`shiply login\` for a fresh key (or check ~/.shiply/credentials)`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
if (e instanceof ApiError && e.code === 'forbidden') {
|
|
1273
|
+
console.error(`✖ ${e.message}`);
|
|
1274
|
+
console.error(` if this is a plan limit: https://shiply.now/dashboard/plan`);
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1188
1277
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1189
1278
|
console.error(`✖ ${msg}`);
|
|
1190
1279
|
process.exit(1);
|
package/dist/login.js
CHANGED
|
@@ -36,9 +36,15 @@ export async function loginViaDeviceFlow(base, agentNameOverride) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
|
38
38
|
// Transient network blip — don't kill the whole flow, the user has time.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// Covers both server-side 5xx (ApiError) AND transport failures (DNS
|
|
40
|
+
// hiccup, connection reset, timeout) that never make it to an ApiError —
|
|
41
|
+
// those used to abort the whole device-flow wait on one dropped packet.
|
|
42
|
+
if (e instanceof ApiError) {
|
|
43
|
+
if (e.status >= 500)
|
|
44
|
+
continue;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
42
48
|
}
|
|
43
49
|
if (poll.status === 'approved' && poll.api_key) {
|
|
44
50
|
return { ok: true, apiKey: poll.api_key, slug: poll.slug_claimed ?? null };
|
package/dist/publish.js
CHANGED
|
@@ -251,7 +251,12 @@ async function uploadAll(dir, targets) {
|
|
|
251
251
|
`(they're short-lived). Re-run \`shiply publish\` to mint fresh ones — ` +
|
|
252
252
|
`unchanged files are hash-skipped, so the retry is cheap.`);
|
|
253
253
|
}
|
|
254
|
-
|
|
254
|
+
if (res.status === 401 || res.status === 403) {
|
|
255
|
+
throw new Error(`upload failed for ${t.path}: HTTP ${res.status} — the upload URL is not authorized. ` +
|
|
256
|
+
`Re-run \`shiply publish\` to mint fresh presigned URLs (unchanged files are hash-skipped, so the retry is cheap).`);
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`upload failed for ${t.path}: HTTP ${res.status} — the object store rejected the file. ` +
|
|
259
|
+
`Re-run \`shiply publish\`; if it keeps failing on this file, check its size/name for anything unusual.`);
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
};
|
package/dist/sending-domains.js
CHANGED
|
@@ -104,6 +104,7 @@ async function verifyAction(ctx, id, flags) {
|
|
|
104
104
|
async function removeAction(ctx, id, flags) {
|
|
105
105
|
if (!flags.yes) {
|
|
106
106
|
console.log(` refusing — \`sending-domain rm\` deletes the domain and forces existing demand tests to fall back to the shared shiply sender. Re-run with --yes to confirm.`);
|
|
107
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
107
108
|
return;
|
|
108
109
|
}
|
|
109
110
|
const base = resolveBase(ctx.base);
|
package/dist/sites.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function sitesLs(ctx) {
|
|
|
20
20
|
export async function sitesRm(ctx, slug, opts) {
|
|
21
21
|
if (!opts.yes) {
|
|
22
22
|
console.error(` refusing — \`shiply rm\` PERMANENTLY deletes ${slug} and its files. Re-run with --yes to confirm.`);
|
|
23
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
const base = resolveBase(ctx.base);
|
package/dist/variable.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
const headers = (apiKey) => ({
|
|
3
|
+
'content-type': 'application/json',
|
|
4
|
+
authorization: `Bearer ${apiKey}`,
|
|
5
|
+
});
|
|
6
|
+
export async function variableLs(ctx, opts = {}) {
|
|
7
|
+
const base = resolveBase(ctx.base);
|
|
8
|
+
const reveal = Boolean(opts.reveal);
|
|
9
|
+
const r = await api(`${base}/api/v1/variables${reveal ? '?reveal=1' : ''}`, { headers: headers(ctx.apiKey) });
|
|
10
|
+
if (opts.json) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(r.variables)}\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (r.variables.length === 0) {
|
|
15
|
+
console.log('No variables saved. shiply variable set <NAME> <value>');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const v of r.variables) {
|
|
19
|
+
console.log(` ${v.name} ${reveal ? (v.value ?? '') : (v.masked ?? '')}`);
|
|
20
|
+
console.log(`VARIABLE name=${v.name} value=${reveal ? (v.value ?? '') : (v.masked ?? '')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function variableSet(ctx, name, value, opts = {}) {
|
|
24
|
+
const base = resolveBase(ctx.base);
|
|
25
|
+
await api(`${base}/api/v1/variables`, {
|
|
26
|
+
method: 'PUT',
|
|
27
|
+
headers: headers(ctx.apiKey),
|
|
28
|
+
body: JSON.stringify({ name, value }),
|
|
29
|
+
});
|
|
30
|
+
const upper = name.trim().toUpperCase();
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
process.stdout.write(`${JSON.stringify({ name: upper, set: true })}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(`✔ ${upper} saved`);
|
|
36
|
+
console.log(' not exposed to any site yet — attach it: shiply variable attach ' + upper + ' --site <slug>');
|
|
37
|
+
}
|
|
38
|
+
export async function variableRm(ctx, name, opts = {}) {
|
|
39
|
+
if (!opts.yes) {
|
|
40
|
+
throw new Error(`refusing to delete ${name} without --yes (this cannot be undone)`);
|
|
41
|
+
}
|
|
42
|
+
const base = resolveBase(ctx.base);
|
|
43
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}`, {
|
|
44
|
+
method: 'DELETE',
|
|
45
|
+
headers: headers(ctx.apiKey),
|
|
46
|
+
});
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
process.stdout.write(`${JSON.stringify({ name: name.trim().toUpperCase(), removed: true })}\n`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(`✔ ${name.trim().toUpperCase()} removed`);
|
|
52
|
+
}
|
|
53
|
+
export async function variableAttach(ctx, name, slug, opts = {}) {
|
|
54
|
+
const base = resolveBase(ctx.base);
|
|
55
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}/attach`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: headers(ctx.apiKey),
|
|
58
|
+
body: JSON.stringify({ slug }),
|
|
59
|
+
});
|
|
60
|
+
const upper = name.trim().toUpperCase();
|
|
61
|
+
if (opts.json) {
|
|
62
|
+
process.stdout.write(`${JSON.stringify({ name: upper, slug, attached: true })}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(`✔ ${upper} attached to ${slug} — takes effect on the site's next function deploy`);
|
|
66
|
+
}
|
|
67
|
+
export async function variableDetach(ctx, name, slug, opts = {}) {
|
|
68
|
+
const base = resolveBase(ctx.base);
|
|
69
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}/detach`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: headers(ctx.apiKey),
|
|
72
|
+
body: JSON.stringify({ slug }),
|
|
73
|
+
});
|
|
74
|
+
const upper = name.trim().toUpperCase();
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
process.stdout.write(`${JSON.stringify({ name: upper, slug, detached: true })}\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`✔ ${upper} detached from ${slug} — takes effect on the site's next function deploy`);
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiply-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Publish static sites to shiply.now from the command line
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/skill/SKILL.md
CHANGED
|
@@ -243,6 +243,8 @@ covers its CLI commands, MCP tools, AND REST endpoints.
|
|
|
243
243
|
* [Client work](references/client-work.md) — freelancer delivery: customer
|
|
244
244
|
intake projects + AI briefs, group work by client, e-sign contracts,
|
|
245
245
|
sell sites on the marketplace.
|
|
246
|
+
* [Drives](references/drives.md) — private cloud storage: staged uploads,
|
|
247
|
+
sharing links, client assignment, publish-from-drive, REST + CLI + MCP.
|
|
246
248
|
* [Authentication for the user's app](references/site-features.md) — shiply
|
|
247
249
|
does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
|
|
248
250
|
(pattern in Site features → "Bring your own auth").
|
|
@@ -31,16 +31,49 @@ list_project_files — files uploaded by the customer
|
|
|
31
31
|
resend_intake_invite — re-email the customer their intake link (requires customerEmail on project)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
REST:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
REST:
|
|
35
|
+
|
|
36
|
+
| Endpoint | Action |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `POST/GET /api/v1/projects` | create / list |
|
|
39
|
+
| `GET/PATCH /api/v1/projects/{id}` | read / update brief or metadata |
|
|
40
|
+
| `POST /api/v1/projects/{id}/regenerate-brief` | re-run AI from current intake responses |
|
|
41
|
+
| `POST /api/v1/projects/{id}/archive` `{reason?}` | dedicated archive route — mirrors `archive_project` (`PATCH .../{id}` `{"status":"archived"}` still works too, back-compat) |
|
|
42
|
+
| `GET /api/v1/projects/{id}/files/zip` | download every customer-uploaded file as one .zip |
|
|
43
|
+
| `GET /api/v1/projects/{id}/files/{fileId}/download` | 302-redirect to a short-lived signed URL for one file |
|
|
44
|
+
| `POST /api/v1/projects/{id}/upload` | dev sends the customer a file (multipart `file` field, ≤25 MB) |
|
|
45
|
+
| `PATCH /api/v1/projects/{id}/status-note` `{note}` | short customer-facing status line shown in the portal header (`note:null` clears it) |
|
|
46
|
+
| `POST /api/v1/projects/{id}/resend-portal-link` | dev re-sends the customer's **portal** link — different from `resend_intake_invite`/`resend-invite` below, which re-sends the original **intake** invite |
|
|
38
47
|
|
|
39
48
|
CLI: `shiply project ls · shiply project create <label> [--customer-email <e>]
|
|
40
49
|
[--customer-name <n>] · shiply project get <id> · shiply project archive <id>`
|
|
41
50
|
|
|
42
51
|
Docs: https://shiply.now/docs/projects
|
|
43
52
|
|
|
53
|
+
### Intake — the customer-facing side (token auth, not Bearer)
|
|
54
|
+
|
|
55
|
+
The intake URL a customer opens is token-scoped, not account-scoped — no
|
|
56
|
+
Bearer key. These routes power the wizard, portal chat, and dev-side nudges:
|
|
57
|
+
|
|
58
|
+
| Endpoint | Action |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `GET /api/v1/intake/{token}/state` | current status + saved wizard responses + project label |
|
|
61
|
+
| `PATCH /api/v1/intake/{token}` `{stepKey, data}` | save one wizard step (auto-save as the customer types) |
|
|
62
|
+
| `POST /api/v1/intake/{token}/submit` | run AI brief generation from the saved responses |
|
|
63
|
+
| `GET /api/v1/intake/{token}/contract` | the project's contract, if one has been sent to this token |
|
|
64
|
+
| `GET/POST /api/v1/intake/{token}/files` | list / upload customer attachments |
|
|
65
|
+
| `DELETE /api/v1/intake/{token}/files/{fileId}` | remove an uploaded attachment |
|
|
66
|
+
| `GET/POST /api/v1/intake/{token}/messages` `{body}` (POST) | portal chat thread with the dev, oldest-first, `?before=` cursor |
|
|
67
|
+
| `POST /api/v1/intake/{token}/nudge` | dev-triggered reminder ping to the customer |
|
|
68
|
+
| `POST /api/v1/intake/{token}/email-link` | re-email the customer their intake link |
|
|
69
|
+
| `POST /api/v1/intake/{token}/cookie` | set the portal session cookie for this token |
|
|
70
|
+
| `GET /api/v1/intake/{token}/activity` | activity/audit log for this intake |
|
|
71
|
+
| `POST /api/v1/intake/{token}/suggest` | AI suggestion helper for a wizard field |
|
|
72
|
+
|
|
73
|
+
Dev-side equivalent for re-sending the invite email is `resend_intake_invite`
|
|
74
|
+
(MCP) / `POST /api/v1/projects/{id}/resend-invite` — different from the
|
|
75
|
+
customer-token `email-link` route above.
|
|
76
|
+
|
|
44
77
|
## Group work by client (optional, for freelancers)
|
|
45
78
|
|
|
46
79
|
Building sites for clients? Pass an optional `client` (a name or email) on any
|
|
@@ -56,7 +89,28 @@ drive/intake/contract all land under the same client. List calls take an
|
|
|
56
89
|
optional `clientId` filter (`list_sites`/`list_drives`/`list_projects`).
|
|
57
90
|
Omit `client` for personal projects; shiply works identically.
|
|
58
91
|
CLI: `--client "<name-or-email>"` on publish/drive/project, plus
|
|
59
|
-
`shiply client ls|show`.
|
|
92
|
+
`shiply client ls|show`. Re-assign (or clear) a client on an already-published
|
|
93
|
+
site directly: `PATCH /api/v1/publish/{slug}/client` `{clientId}`
|
|
94
|
+
(`clientId: null` clears it) — no MCP tool for this yet, REST only.
|
|
95
|
+
|
|
96
|
+
REST (Bearer; Studio plan — `assertStudioEntitled` gates `POST`):
|
|
97
|
+
|
|
98
|
+
| Endpoint | Action |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `GET /api/v1/clients` | list your clients → `{clients}` |
|
|
101
|
+
| `POST /api/v1/clients` `{name, email?, phone?, company?, notes?}` | create a client explicitly (usually unnecessary — clients self-assemble from `client`/`customerEmail`) |
|
|
102
|
+
| `GET /api/v1/clients/{id}` | rollup view: sites, drives, projects, contracts, invoices under this client |
|
|
103
|
+
| `GET /api/v1/clients/{id}/invoices` | list invoices for this client |
|
|
104
|
+
| `POST /api/v1/clients/{id}/invoices` `{description, amountCents, currency?, dueDate?, projectId?}` | create an invoice |
|
|
105
|
+
| `POST /api/v1/invoices/{id}/send` | email the invoice to the client |
|
|
106
|
+
| `POST /api/v1/invoices/{id}/void` | void an unpaid invoice |
|
|
107
|
+
| `POST /api/v1/clients/{id}/portal` | enable the client's public portal (idempotent) → `{portalUrl}` |
|
|
108
|
+
| `DELETE /api/v1/clients/{id}/portal` | disable the portal — the link dies immediately |
|
|
109
|
+
| `POST /api/v1/clients/{id}/portal/rotate` | mint a fresh portal token (old link dies instantly) |
|
|
110
|
+
|
|
111
|
+
No `list_clients`/`get_client`/portal-management MCP tools exist yet — this
|
|
112
|
+
is REST + CLI (`shiply client ls|show`) only; an MCP agent reads client data
|
|
113
|
+
indirectly via `list_sites`/`list_drives`/`list_projects` with `clientId`.
|
|
60
114
|
|
|
61
115
|
## Contracts — draft, sign, amend (extends Projects)
|
|
62
116
|
|
|
@@ -91,7 +145,7 @@ Example agent flow (draft → tweak fee → send → poll → amend):
|
|
|
91
145
|
6. `contract_send({contractId: amendment.id})` (no PATCH needed if the amend fields are right)
|
|
92
146
|
|
|
93
147
|
REST (Bearer for dev, portal cookie for customer):
|
|
94
|
-
`POST /api/v1/projects/{id}/
|
|
148
|
+
`POST /api/v1/projects/{id}/contract` (draft — singular; the plural `/contracts` is a GET-only read bundle) ·
|
|
95
149
|
`GET/PATCH /api/v1/contracts/{id}` (read either-party / dev edit) ·
|
|
96
150
|
`POST /api/v1/contracts/{id}/send` (dev) ·
|
|
97
151
|
`POST /api/v1/contracts/{id}/view` (customer ping) ·
|
|
@@ -141,12 +195,13 @@ refund_order — issue Stripe refund (within refund window; goes back to buy
|
|
|
141
195
|
get_connect_status — Stripe Connect onboarding state + actionable URL
|
|
142
196
|
```
|
|
143
197
|
|
|
144
|
-
REST: `POST /api/v1/listings · PATCH /api/v1/listings/{id} ·
|
|
145
|
-
POST /api/v1/listings/{id}/checkout ·
|
|
146
|
-
GET /api/v1/connect/status`
|
|
147
|
-
(
|
|
148
|
-
|
|
149
|
-
|
|
198
|
+
REST: `GET/POST /api/v1/listings · PATCH /api/v1/listings/{id} ·
|
|
199
|
+
POST /api/v1/listings/{id}/checkout · GET /api/v1/orders ·
|
|
200
|
+
POST /api/v1/orders/{id}/refund · GET /api/v1/connect/status`
|
|
201
|
+
(`GET /api/v1/listings` and `GET /api/v1/orders` list your own listings/
|
|
202
|
+
orders — `?limit=` supported; MCP mirrors are `list_listings` /
|
|
203
|
+
`list_my_orders`. Sales specifically: `GET /api/v1/listings/sales` /
|
|
204
|
+
`list_my_sales`.)
|
|
150
205
|
|
|
151
206
|
CLI: `shiply listing ls · shiply listing create <site-slug> --price <cents>
|
|
152
207
|
--jurisdiction "<region>" · shiply listing rm <slug> --id <listing-id>`
|
|
@@ -157,3 +212,29 @@ isn't `'ready'` it returns an `onboardingUrl` you should hand to the
|
|
|
157
212
|
user as a clickable link.
|
|
158
213
|
|
|
159
214
|
Docs: https://shiply.now/docs/marketplace
|
|
215
|
+
|
|
216
|
+
## Inbox threads — reply, forward, archive, summarize, AI draft
|
|
217
|
+
|
|
218
|
+
The account-wide inbox (replies, unsubscribes, complaints, bounces across
|
|
219
|
+
every site) beyond the read-only MCP tools in [email.md](email.md):
|
|
220
|
+
|
|
221
|
+
MCP tools: `list_inbox` (filtered list + counts in one call — filters
|
|
222
|
+
`all|unread|replies|unsubscribes|complaints|bounces|archived`), `read_thread`
|
|
223
|
+
(marks read implicitly), `mark_thread_read`, `summarize_thread`,
|
|
224
|
+
`reply_to_thread`, `forward_thread`, `archive_thread`, `unarchive_thread`.
|
|
225
|
+
|
|
226
|
+
REST (Bearer):
|
|
227
|
+
|
|
228
|
+
| Endpoint | Action |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `GET /api/v1/inbox/threads?filter=&limit=&offset=` | atomic snapshot: `{threads, counts}` |
|
|
231
|
+
| `GET /api/v1/inbox/threads/{id}` | one thread (also marks read) |
|
|
232
|
+
| `PATCH /api/v1/inbox/threads/{id}` `{action:"mark_read"\|"archive"\|"unarchive"}` | thread lifecycle |
|
|
233
|
+
| `POST /api/v1/inbox/threads/{id}/reply` `{body, subject?}` | reply from the original recipient alias, threaded via In-Reply-To |
|
|
234
|
+
| `POST /api/v1/inbox/threads/{id}/forward` `{to, messageId?, subject?, note?}` | forward one message to a new address |
|
|
235
|
+
| `POST /api/v1/inbox/threads/{id}/summarize` | AI summary of the thread |
|
|
236
|
+
| `POST /api/v1/inbox/threads/{id}/ai-reply` | AI-drafted reply body (review before sending — no matching MCP tool yet) |
|
|
237
|
+
|
|
238
|
+
**Gap (intentional, tracked):** the CLI has no `shiply inbox` thread-action
|
|
239
|
+
subcommands (reply/forward/archive/summarize) — use MCP or REST for those;
|
|
240
|
+
`shiply email inbox` only reads.
|
|
@@ -40,6 +40,8 @@ shiply domain sub add www.example.com --site my-site # map a subdomain → sit
|
|
|
40
40
|
shiply domain ls # list all domains + subdomains + status
|
|
41
41
|
shiply domain sync example.com # re-sync DNS records with a connected provider
|
|
42
42
|
shiply domain primary <hostname> # mark hostname canonical (siblings 301)
|
|
43
|
+
shiply domain check example.com # re-poll cert/DNS/HTTPS readiness per subdomain
|
|
44
|
+
shiply domain rm example.com --yes # remove a domain (its subdomains stop resolving)
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
For one-click connect (`shiply domain connect`), the CLI prints an
|
|
@@ -95,6 +95,11 @@ for D1 and Neon), `POST .../attach` to bind, `DELETE` to drop. Neon-only:
|
|
|
95
95
|
`POST/GET/DELETE /api/v1/databases/{id}/branches[/{branchId}]` (400 on
|
|
96
96
|
D1 rows).
|
|
97
97
|
|
|
98
|
+
Main MCP server (account-wide lifecycle — start here to discover a
|
|
99
|
+
databaseId): `list_databases`, `create_database`, `delete_database`,
|
|
100
|
+
`attach_database`. Then, to query/introspect one database, connect the
|
|
101
|
+
per-DB MCP server below with that id.
|
|
102
|
+
|
|
98
103
|
Per-DB MCP server (so an agent can be scoped to one DB):
|
|
99
104
|
`https://shiply.now/api/mcp/db/<id>/sse` (or `/mcp` for streamable-http),
|
|
100
105
|
Bearer `shp_…` — tools: `db_query`, `db_list_tables`, `db_schema`. The same
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: reference
|
|
3
|
+
title: Drives (private storage — REST, CLI, MCP)
|
|
4
|
+
description: Full reference for Drives — private cloud storage for agent memory, notes, and assets. Staged uploads, sharing links, client assignment, and publish-from-drive.
|
|
5
|
+
timestamp: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Drives — private cloud storage
|
|
9
|
+
|
|
10
|
+
Part of the shiply skill (see SKILL.md for publish basics). A Drive is
|
|
11
|
+
private storage — files, notes, context — that is **not** served publicly
|
|
12
|
+
like a published site. Every account gets a `default` drive on first use;
|
|
13
|
+
you can also create named drives (e.g. one per client).
|
|
14
|
+
|
|
15
|
+
## REST (Bearer `shp_...`)
|
|
16
|
+
|
|
17
|
+
| Endpoint | Action |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `GET /api/v1/drives?clientId=` | list your drives (optional client filter) → `{drives}` |
|
|
20
|
+
| `POST /api/v1/drives` `{name, client?}` | create a named drive. `client` is the same optional `{clientId?, clientEmail?, clientName?, clientCompany?}` shape used on publish/create_project |
|
|
21
|
+
| `GET /api/v1/drives/default` | the account's auto-provisioned default drive (creates it on first call) |
|
|
22
|
+
| `GET /api/v1/drives/{id}` | drive detail |
|
|
23
|
+
| `DELETE /api/v1/drives/{id}` | delete a drive and its files |
|
|
24
|
+
| `GET /api/v1/drives/{id}/files?prefix=&cursor=` | list files (optional path-prefix filter, pagination cursor) |
|
|
25
|
+
| `POST /api/v1/drives/{id}/files/uploads` `{files:[{path,size,contentType?,hash}]}` | stage up to 1000 files → presigned PUT URLs (same 3-step flow as site publish) |
|
|
26
|
+
| `POST /api/v1/drives/{id}/files/finalize` `{files:[{path,size,contentType?,hash}]}` | confirm uploads landed — same `files` array as the uploads call |
|
|
27
|
+
| `GET /api/v1/drives/{id}/files/{...path}` | `{url}` — a presigned, time-limited download URL for one file |
|
|
28
|
+
| `DELETE /api/v1/drives/{id}/files/{...path}` | delete one file (optional `If-Match` header for optimistic concurrency) |
|
|
29
|
+
| `POST /api/v1/drives/{id}/assign-client` `{clientId}` | file this drive under a client (`clientId: null` clears it) |
|
|
30
|
+
| `POST /api/v1/drives/{id}/share` `{instructions?}` | enable a public share link for the drive |
|
|
31
|
+
| `DELETE /api/v1/drives/{id}/share` | disable the share link |
|
|
32
|
+
| `POST /api/v1/drives/{id}/share/rotate` | mint a fresh share token — the old link dies instantly |
|
|
33
|
+
| `POST /api/v1/drives/{id}/share/invite` `{emails:[...max 50], message?}` | email the share link to a list of addresses |
|
|
34
|
+
|
|
35
|
+
`{id}` is the drive's public id (`drv_...`), or the literal string
|
|
36
|
+
`default` where the endpoint supports it (only `GET /api/v1/drives/default`
|
|
37
|
+
resolves the alias itself — everywhere else, resolve `default` to a real
|
|
38
|
+
`drv_...` id first via that call).
|
|
39
|
+
|
|
40
|
+
## CLI
|
|
41
|
+
|
|
42
|
+
The CLI only operates on the account's **default** drive — for named/
|
|
43
|
+
per-client drives, use REST or MCP.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
shiply drive ls [prefix] # list files (optionally under a prefix)
|
|
47
|
+
shiply drive put <file> [as-path] # stage + finalize a file into the default drive
|
|
48
|
+
shiply drive get <path> [out-file] # download; omit out-file to print to stdout
|
|
49
|
+
shiply drive rm <path> # delete a file
|
|
50
|
+
shiply drive publish [prefix] # snapshot the default drive (or a prefix) as a new live site
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## MCP tools
|
|
54
|
+
|
|
55
|
+
| Tool | Purpose |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `list_drives` | your drives (optional `clientId` filter) |
|
|
58
|
+
| `create_drive` | create a named drive (`name`, optional `client`) |
|
|
59
|
+
| `drive_list_files` | list files in a drive (`driveId` = `drv_...` or `"default"`, optional `prefix`) |
|
|
60
|
+
| `drive_put_file` | write a file inline (utf8 or base64, ≤2 MB — use the REST staged-upload flow above for bigger files) |
|
|
61
|
+
| `drive_delete_file` | delete a file from a drive |
|
|
62
|
+
| `publish_from_drive` | snapshot a drive (or a path prefix within it) into a new live site |
|
|
63
|
+
|
|
64
|
+
**Gap (intentional, tracked):** file download, drive delete, and the
|
|
65
|
+
share/assign-client family (`share`, `share/rotate`, `share/invite`,
|
|
66
|
+
`assign-client`) are REST-only today — there is no `drive_download`,
|
|
67
|
+
`delete_drive`, or share-management MCP tool yet. Use the REST endpoints
|
|
68
|
+
above from an MCP-only agent until parity lands.
|
|
69
|
+
|
|
70
|
+
Docs: https://shiply.now/docs/drives
|
|
@@ -127,6 +127,22 @@ the confirmed audience (unsubscribe link auto-added). `list_tests()` — all
|
|
|
127
127
|
tests for this key. `resend_confirmation({ testId, email })` — re-send the
|
|
128
128
|
opt-in to one address.
|
|
129
129
|
|
|
130
|
+
REST (Bearer, mirrors the MCP tools above):
|
|
131
|
+
|
|
132
|
+
| Endpoint | Action |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `GET /api/v1/tests` | list your demand tests → `{tests}` |
|
|
135
|
+
| `POST /api/v1/tests` `{idea, copy:{headline, sub?, cta?}, price?, captureFields?, sendingDomainId?}` | create a test — deploys the capture page + provisions the confirmed-subscriber segment |
|
|
136
|
+
| `GET /api/v1/tests/{id}` | merged status object — same shape as `get_test_status` |
|
|
137
|
+
| `PATCH /api/v1/tests/{id}/branding` `{brandName?, logoUrl?, brandColor?, accentColor?, footerText?, replyTo?}` | customize the capture page's branding |
|
|
138
|
+
| `POST /api/v1/tests/{id}/broadcasts` `{subject, html, text?}` | email the confirmed audience — same as `send_broadcast` |
|
|
139
|
+
| `POST /api/v1/tests/suggest-copy` `{idea}` | AI-drafted `{headline, sub, cta}` from a one-line idea (MiniMax; 503 `service_unavailable` if no AI provider is configured) |
|
|
140
|
+
|
|
141
|
+
`list_tests` maps to `GET /api/v1/tests` above. `resend_confirmation` (re-send
|
|
142
|
+
the opt-in email to one address) is **MCP-only today** — no REST route exists
|
|
143
|
+
yet; use the MCP tool from a REST-only agent by proxying through an MCP
|
|
144
|
+
client, or ask for the confirmation link to be resent from the dashboard.
|
|
145
|
+
|
|
130
146
|
## Sending domains — outbound email on your domain
|
|
131
147
|
|
|
132
148
|
For agents that want shiply to send emails (demand-test broadcasts, project
|
|
@@ -140,6 +156,14 @@ to add), `verify_sending_domain`, `remove_sending_domain`.
|
|
|
140
156
|
REST: `GET/POST /api/v1/sending-domains ·
|
|
141
157
|
POST /api/v1/sending-domains/{id}/verify · DELETE /api/v1/sending-domains/{id}`
|
|
142
158
|
|
|
159
|
+
Auto-DNS shortcuts (skip the "copy these records to your registrar" step —
|
|
160
|
+
user supplies a scoped, never-stored Cloudflare API token for the domain's
|
|
161
|
+
zone): `POST /api/v1/sending-domains/{id}/auto-dns` `{provider:"cloudflare",
|
|
162
|
+
token}` adds the SPF/DKIM records automatically; `POST
|
|
163
|
+
/api/v1/sending-domains/{id}/email-routing` `{provider:"cloudflare", token}`
|
|
164
|
+
additionally turns on inbound Email Routing and binds the catch-all to
|
|
165
|
+
shiply's inbound worker. No MCP tools for either yet — REST only.
|
|
166
|
+
|
|
143
167
|
CLI: `shiply sending-domain ls · shiply sending-domain add <domain> ·
|
|
144
168
|
shiply sending-domain verify <id> · shiply sending-domain rm <id> --yes`
|
|
145
169
|
|
|
@@ -70,7 +70,9 @@ shiply publish <dir> --json # one machine-readable JSON line, no
|
|
|
70
70
|
- **`shiply verify <slug>`** prints a human report PLUS a stable machine marker
|
|
71
71
|
line `VERIFY status=LIVE http=200 ssl=valid thumb=…` (or `status=PENDING`).
|
|
72
72
|
Headless agents should parse that `VERIFY status=…` line — it's the
|
|
73
|
-
contract, like `SITE_READY` in `status`.
|
|
73
|
+
contract, like `SITE_READY` in `status`. REST: `GET /api/v1/sites/{slug}/verify`
|
|
74
|
+
(public, no auth — mirrors `site_status`) → `{status, ssl, http, thumbnailUrl,
|
|
75
|
+
consoleErrorsDeepLink}`. MCP: `verify_site`.
|
|
74
76
|
- **`shiply publish <dir> --as <name>`** gives a stable preview URL that the
|
|
75
77
|
next publish with the same `--as <name>` reuses — no more changing URLs or
|
|
76
78
|
orphaned scratch sites across iterations, independent of the source dir.
|
|
@@ -52,18 +52,28 @@ lists), or `--public` to open it up.
|
|
|
52
52
|
REST: PATCH /api/v1/publishes/<slug>/access
|
|
53
53
|
with {"mode":"password","password":"..."} or {"mode":"restricted",
|
|
54
54
|
"allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
|
|
55
|
-
GET returns the policy (never the hash). MCP
|
|
55
|
+
GET returns the policy (never the hash). MCP tools: `get_site_access` (read),
|
|
56
|
+
`set_site_access` (write). Enforced
|
|
56
57
|
at the edge before any content/proxy/data is served; visitors get a 7-day
|
|
57
58
|
signed cookie; changing any setting signs current visitors out.
|
|
58
59
|
Docs: /docs/access-control
|
|
59
60
|
|
|
60
61
|
## Variables — encrypted per-user KV
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
CLI (preferred): `shiply variable ls [--reveal]`, `shiply variable set <NAME>
|
|
64
|
+
<value>` (or `--stdin`), `shiply variable rm <NAME> --yes`.
|
|
65
|
+
REST: GET/PUT /api/v1/variables {"name","value"}, DELETE
|
|
66
|
+
/api/v1/variables/{NAME}; GET ?reveal=1 returns plaintext values.
|
|
67
|
+
MCP: `list_variables`, `set_variable`, `delete_variable`. Referenced by proxy
|
|
68
|
+
routes (`${VAR}`) and injected into Workers as `env.<NAME>`. Supabase can be
|
|
69
|
+
connected from the dashboard and lands here as SUPABASE_URL + SUPABASE_ANON_KEY.
|
|
70
|
+
|
|
71
|
+
Expose (or stop exposing) a saved account variable to one specific site's
|
|
72
|
+
Worker env, independent of proxy routes: CLI `shiply variable attach <NAME>
|
|
73
|
+
--site <slug>` / `shiply variable detach <NAME> --site <slug>` (both
|
|
74
|
+
idempotent). REST: `POST /api/v1/variables/{name}/attach` `{slug}` / `POST
|
|
75
|
+
/api/v1/variables/{name}/detach` `{slug}`. MCP: `attach_variable`,
|
|
76
|
+
`detach_variable`, `list_site_variables`.
|
|
67
77
|
|
|
68
78
|
## Drives — private cloud storage (agent memory + assets)
|
|
69
79
|
|
|
@@ -71,6 +81,8 @@ Private storage for files/notes/context you don't want on a public site.
|
|
|
71
81
|
MCP tools: `list_drives`, `create_drive`, `drive_put_file` (driveId
|
|
72
82
|
"default", utf8/base64, ≤2 MB), `drive_list_files`, `drive_delete_file`,
|
|
73
83
|
`publish_from_drive` (snapshot a drive into a live site).
|
|
84
|
+
Full reference (REST staged uploads, sharing, client assignment, CLI):
|
|
85
|
+
[drives.md](drives.md).
|
|
74
86
|
|
|
75
87
|
## Path-mounting + public profile
|
|
76
88
|
|
|
@@ -78,7 +90,8 @@ MCP tools: `list_drives`, `create_drive`, `drive_put_file` (driveId
|
|
|
78
90
|
(host/docs → target).
|
|
79
91
|
- `set_profile` / `feature_site` (MCP) — stand up the user's public portfolio
|
|
80
92
|
at <handle>.shiply.now and feature a public site on shiply.now/explore.
|
|
81
|
-
- `export_account` (MCP)
|
|
93
|
+
- `export_account` (MCP) / `GET /api/v1/account/export` (REST) — JSON bundle
|
|
94
|
+
of the user's data (no secrets).
|
|
82
95
|
- Site metadata: `PATCH /api/v1/publish/<slug>/metadata`
|
|
83
96
|
{"title?","spaMode?","addedToProfile?"}.
|
|
84
97
|
|