shiply-cli 0.12.0 → 0.14.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/functions.js +342 -0
- package/dist/index.js +68 -5
- package/dist/listings.js +169 -0
- package/dist/projects.js +171 -0
- package/dist/sending-domains.js +159 -0
- package/dist/skill.js +5 -1
- package/package.json +1 -1
- package/skill/SKILL.md +199 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/** CLI for shiply Workers Lite — function deploy, secrets, crons.
|
|
2
|
+
*
|
|
3
|
+
* Reads --base / --key / --json / --ts from the argv tail the way
|
|
4
|
+
* projects.ts and listings.ts do (ad-hoc parser) so the top-level
|
|
5
|
+
* `parseArgs` in index.ts doesn't have to know about each one. */
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { loadApiKey } from './config.js';
|
|
9
|
+
import { api, resolveBase } from './publish.js';
|
|
10
|
+
const headers = (apiKey) => ({
|
|
11
|
+
'content-type': 'application/json',
|
|
12
|
+
authorization: `Bearer ${apiKey}`,
|
|
13
|
+
});
|
|
14
|
+
/** Strip --flag / --flag=value / --flag value out of argv, returning the
|
|
15
|
+
* remaining positionals and a typed flag bag. Mirrors the helper in
|
|
16
|
+
* projects.ts so the dispatch pattern stays consistent across subcommands.
|
|
17
|
+
* Top-level flags (consumed by index.ts parseArgs before we got here)
|
|
18
|
+
* are merged in via the `inherited` arg. */
|
|
19
|
+
function readFlags(argv, inherited = {}) {
|
|
20
|
+
const raw = {};
|
|
21
|
+
const rest = [];
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (a.startsWith('--')) {
|
|
25
|
+
const eq = a.indexOf('=');
|
|
26
|
+
if (eq >= 0) {
|
|
27
|
+
raw[a.slice(2, eq)] = a.slice(eq + 1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const next = argv[i + 1];
|
|
31
|
+
if (next && !next.startsWith('--')) {
|
|
32
|
+
raw[a.slice(2)] = next;
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
raw[a.slice(2)] = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
rest.push(a);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// `--ts` and `--json` are flags; --base/--key carry values.
|
|
45
|
+
// If the user passes `--ts something`, our naive parser captures
|
|
46
|
+
// `something` as the value — re-classify so it stays a positional.
|
|
47
|
+
const flags = {
|
|
48
|
+
base: (typeof raw.base === 'string' ? raw.base : undefined) ?? inherited.base,
|
|
49
|
+
key: (typeof raw.key === 'string' ? raw.key : undefined) ?? inherited.key,
|
|
50
|
+
json: Boolean(raw.json) || Boolean(inherited.json),
|
|
51
|
+
ts: Boolean(raw.ts) || Boolean(inherited.ts),
|
|
52
|
+
};
|
|
53
|
+
if (typeof raw.ts === 'string')
|
|
54
|
+
rest.unshift(raw.ts);
|
|
55
|
+
if (typeof raw.json === 'string')
|
|
56
|
+
rest.unshift(raw.json);
|
|
57
|
+
return { rest, flags };
|
|
58
|
+
}
|
|
59
|
+
async function getApiKey(flags, what) {
|
|
60
|
+
const k = flags.key ?? (await loadApiKey());
|
|
61
|
+
if (!k)
|
|
62
|
+
throw new Error(`${what} commands need an API key — run \`shiply login\` first`);
|
|
63
|
+
return k;
|
|
64
|
+
}
|
|
65
|
+
// --- shiply function --------------------------------------------------------
|
|
66
|
+
const FUNCTION_USAGE = [
|
|
67
|
+
'Usage: shiply function <deploy|get|rm> <slug> [--ts] [--json]',
|
|
68
|
+
' shiply function deploy <slug> # uploads worker.js from CWD',
|
|
69
|
+
' shiply function deploy <slug> --ts # uploads worker.ts from CWD',
|
|
70
|
+
' shiply function get <slug>',
|
|
71
|
+
' shiply function rm <slug>',
|
|
72
|
+
].join('\n');
|
|
73
|
+
export async function runFunction(argv, inherited = {}) {
|
|
74
|
+
const action = argv[0];
|
|
75
|
+
const rest = argv.slice(1);
|
|
76
|
+
switch (action) {
|
|
77
|
+
case 'deploy':
|
|
78
|
+
return deployCmd(rest, inherited);
|
|
79
|
+
case 'get':
|
|
80
|
+
return getFnCmd(rest, inherited);
|
|
81
|
+
case 'rm':
|
|
82
|
+
case 'remove':
|
|
83
|
+
case 'delete':
|
|
84
|
+
return removeFnCmd(rest, inherited);
|
|
85
|
+
default:
|
|
86
|
+
console.error(FUNCTION_USAGE);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function deployCmd(argv, inherited) {
|
|
91
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
92
|
+
const slug = rest[0];
|
|
93
|
+
if (!slug) {
|
|
94
|
+
console.error(FUNCTION_USAGE);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const apiKey = await getApiKey(flags, 'function');
|
|
98
|
+
const base = resolveBase(flags.base);
|
|
99
|
+
const lang = flags.ts ? 'ts' : 'js';
|
|
100
|
+
const filename = lang === 'ts' ? 'worker.ts' : 'worker.js';
|
|
101
|
+
let source;
|
|
102
|
+
try {
|
|
103
|
+
source = await readFile(join(process.cwd(), filename), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
throw new Error(`couldn't read ${filename} from ${process.cwd()} — ${e.message}\n hint: cd into the directory containing your ${filename}, or pass --ts if the file is worker.ts`);
|
|
107
|
+
}
|
|
108
|
+
const row = await api(`${base}/api/v1/sites/${slug}/function`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: headers(apiKey),
|
|
111
|
+
body: JSON.stringify({ source, lang }),
|
|
112
|
+
});
|
|
113
|
+
if (flags.json) {
|
|
114
|
+
console.log(JSON.stringify(row, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`✔ deployed worker for ${slug}`);
|
|
118
|
+
console.log(` script: ${row.cfScriptName}`);
|
|
119
|
+
console.log(` version: ${row.version.slice(0, 12)}`);
|
|
120
|
+
console.log(` source: ${filename} (${lang})`);
|
|
121
|
+
console.log(` routes: requests to https://${slug}.shiply.now/* now hit your worker first`);
|
|
122
|
+
}
|
|
123
|
+
async function getFnCmd(argv, inherited) {
|
|
124
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
125
|
+
const slug = rest[0];
|
|
126
|
+
if (!slug) {
|
|
127
|
+
console.error(FUNCTION_USAGE);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const apiKey = await getApiKey(flags, 'function');
|
|
131
|
+
const base = resolveBase(flags.base);
|
|
132
|
+
const row = await api(`${base}/api/v1/sites/${slug}/function`, {
|
|
133
|
+
headers: headers(apiKey),
|
|
134
|
+
});
|
|
135
|
+
if (flags.json) {
|
|
136
|
+
console.log(JSON.stringify(row, null, 2));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(`${slug} [${row.sourceLang}] version ${row.version.slice(0, 12)}`);
|
|
140
|
+
console.log(` script: ${row.cfScriptName}`);
|
|
141
|
+
console.log(` deployed: ${row.deployedAt}`);
|
|
142
|
+
console.log(` updated: ${row.updatedAt}`);
|
|
143
|
+
console.log(` source: ${row.source.length} bytes`);
|
|
144
|
+
}
|
|
145
|
+
async function removeFnCmd(argv, inherited) {
|
|
146
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
147
|
+
const slug = rest[0];
|
|
148
|
+
if (!slug) {
|
|
149
|
+
console.error(FUNCTION_USAGE);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const apiKey = await getApiKey(flags, 'function');
|
|
153
|
+
const base = resolveBase(flags.base);
|
|
154
|
+
await api(`${base}/api/v1/sites/${slug}/function`, {
|
|
155
|
+
method: 'DELETE',
|
|
156
|
+
headers: headers(apiKey),
|
|
157
|
+
});
|
|
158
|
+
if (flags.json) {
|
|
159
|
+
console.log(JSON.stringify({ ok: true }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(`✔ removed worker for ${slug} — static-only routing restored`);
|
|
163
|
+
}
|
|
164
|
+
// --- shiply secret ----------------------------------------------------------
|
|
165
|
+
const SECRET_USAGE = [
|
|
166
|
+
'Usage: shiply secret <set|ls|rm> <slug> [args] [--json]',
|
|
167
|
+
' shiply secret set <slug> <NAME> <VALUE>',
|
|
168
|
+
' shiply secret ls <slug>',
|
|
169
|
+
' shiply secret rm <slug> <NAME>',
|
|
170
|
+
].join('\n');
|
|
171
|
+
export async function runSecret(argv, inherited = {}) {
|
|
172
|
+
const action = argv[0];
|
|
173
|
+
const rest = argv.slice(1);
|
|
174
|
+
switch (action) {
|
|
175
|
+
case 'set':
|
|
176
|
+
return setSecretCmd(rest, inherited);
|
|
177
|
+
case 'ls':
|
|
178
|
+
case 'list':
|
|
179
|
+
return lsSecretsCmd(rest, inherited);
|
|
180
|
+
case 'rm':
|
|
181
|
+
case 'remove':
|
|
182
|
+
case 'delete':
|
|
183
|
+
return rmSecretCmd(rest, inherited);
|
|
184
|
+
default:
|
|
185
|
+
console.error(SECRET_USAGE);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function setSecretCmd(argv, inherited) {
|
|
190
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
191
|
+
const [slug, name, value] = rest;
|
|
192
|
+
if (!slug || !name || !value) {
|
|
193
|
+
console.error(SECRET_USAGE);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const apiKey = await getApiKey(flags, 'secret');
|
|
197
|
+
const base = resolveBase(flags.base);
|
|
198
|
+
await api(`${base}/api/v1/sites/${slug}/secrets`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: headers(apiKey),
|
|
201
|
+
body: JSON.stringify({ name, value }),
|
|
202
|
+
});
|
|
203
|
+
if (flags.json) {
|
|
204
|
+
console.log(JSON.stringify({ ok: true, name }));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.log(`✔ set ${name} on ${slug}`);
|
|
208
|
+
console.log(` available as env.${name} in your worker on next deploy/restart`);
|
|
209
|
+
}
|
|
210
|
+
async function lsSecretsCmd(argv, inherited) {
|
|
211
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
212
|
+
const slug = rest[0];
|
|
213
|
+
if (!slug) {
|
|
214
|
+
console.error(SECRET_USAGE);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
const apiKey = await getApiKey(flags, 'secret');
|
|
218
|
+
const base = resolveBase(flags.base);
|
|
219
|
+
const r = await api(`${base}/api/v1/sites/${slug}/secrets`, { headers: headers(apiKey) });
|
|
220
|
+
if (flags.json) {
|
|
221
|
+
console.log(JSON.stringify(r, null, 2));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!r.secrets.length) {
|
|
225
|
+
console.log(`no secrets on ${slug} yet`);
|
|
226
|
+
console.log(` set one: shiply secret set ${slug} STRIPE_SECRET_KEY sk_test_…`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const s of r.secrets) {
|
|
230
|
+
console.log(`${s.name}${s.updatedAt ? ` (updated ${s.updatedAt})` : ''}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function rmSecretCmd(argv, inherited) {
|
|
234
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
235
|
+
const [slug, name] = rest;
|
|
236
|
+
if (!slug || !name) {
|
|
237
|
+
console.error(SECRET_USAGE);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const apiKey = await getApiKey(flags, 'secret');
|
|
241
|
+
const base = resolveBase(flags.base);
|
|
242
|
+
await api(`${base}/api/v1/sites/${slug}/secrets/${encodeURIComponent(name)}`, {
|
|
243
|
+
method: 'DELETE',
|
|
244
|
+
headers: headers(apiKey),
|
|
245
|
+
});
|
|
246
|
+
if (flags.json) {
|
|
247
|
+
console.log(JSON.stringify({ ok: true, name }));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log(`✔ removed secret ${name} from ${slug}`);
|
|
251
|
+
}
|
|
252
|
+
// --- shiply cron ------------------------------------------------------------
|
|
253
|
+
const CRON_USAGE = [
|
|
254
|
+
'Usage: shiply cron <ls|set|rm> <slug> [args] [--json]',
|
|
255
|
+
' shiply cron ls <slug>',
|
|
256
|
+
' shiply cron set <slug> <path> "<schedule>"',
|
|
257
|
+
' e.g. shiply cron set my-site /api/cron/digest "0 9 * * *"',
|
|
258
|
+
' shiply cron rm <slug> <path>',
|
|
259
|
+
].join('\n');
|
|
260
|
+
export async function runCron(argv, inherited = {}) {
|
|
261
|
+
const action = argv[0];
|
|
262
|
+
const rest = argv.slice(1);
|
|
263
|
+
switch (action) {
|
|
264
|
+
case 'ls':
|
|
265
|
+
case 'list':
|
|
266
|
+
return lsCronsCmd(rest, inherited);
|
|
267
|
+
case 'set':
|
|
268
|
+
case 'add':
|
|
269
|
+
return setCronCmd(rest, inherited);
|
|
270
|
+
case 'rm':
|
|
271
|
+
case 'remove':
|
|
272
|
+
case 'delete':
|
|
273
|
+
return rmCronCmd(rest, inherited);
|
|
274
|
+
default:
|
|
275
|
+
console.error(CRON_USAGE);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function lsCronsCmd(argv, inherited) {
|
|
280
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
281
|
+
const slug = rest[0];
|
|
282
|
+
if (!slug) {
|
|
283
|
+
console.error(CRON_USAGE);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
const apiKey = await getApiKey(flags, 'cron');
|
|
287
|
+
const base = resolveBase(flags.base);
|
|
288
|
+
const r = await api(`${base}/api/v1/sites/${slug}/crons`, { headers: headers(apiKey) });
|
|
289
|
+
if (flags.json) {
|
|
290
|
+
console.log(JSON.stringify(r, null, 2));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!r.crons.length) {
|
|
294
|
+
console.log(`no crons on ${slug} yet`);
|
|
295
|
+
console.log(` add one: shiply cron set ${slug} /api/cron/digest "0 9 * * *"`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
for (const c of r.crons) {
|
|
299
|
+
console.log(`${c.schedule} → ${c.path}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function setCronCmd(argv, inherited) {
|
|
303
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
304
|
+
const [slug, path, schedule] = rest;
|
|
305
|
+
if (!slug || !path || !schedule) {
|
|
306
|
+
console.error(CRON_USAGE);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
const apiKey = await getApiKey(flags, 'cron');
|
|
310
|
+
const base = resolveBase(flags.base);
|
|
311
|
+
await api(`${base}/api/v1/sites/${slug}/crons`, {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: headers(apiKey),
|
|
314
|
+
body: JSON.stringify({ path, schedule }),
|
|
315
|
+
});
|
|
316
|
+
if (flags.json) {
|
|
317
|
+
console.log(JSON.stringify({ ok: true, path, schedule }));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
console.log(`✔ scheduled "${schedule}" → ${path} on ${slug}`);
|
|
321
|
+
console.log(` Cloudflare will POST to your worker at this path on the schedule`);
|
|
322
|
+
}
|
|
323
|
+
async function rmCronCmd(argv, inherited) {
|
|
324
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
325
|
+
const [slug, path] = rest;
|
|
326
|
+
if (!slug || !path) {
|
|
327
|
+
console.error(CRON_USAGE);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
const apiKey = await getApiKey(flags, 'cron');
|
|
331
|
+
const base = resolveBase(flags.base);
|
|
332
|
+
await api(`${base}/api/v1/sites/${slug}/crons`, {
|
|
333
|
+
method: 'DELETE',
|
|
334
|
+
headers: headers(apiKey),
|
|
335
|
+
body: JSON.stringify({ path }),
|
|
336
|
+
});
|
|
337
|
+
if (flags.json) {
|
|
338
|
+
console.log(JSON.stringify({ ok: true, path }));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
console.log(`✔ removed cron ${path} from ${slug}`);
|
|
342
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ import { runClaimVerify } from './claim.js';
|
|
|
8
8
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
9
9
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
10
10
|
import { domainAdd, domainConnect, domainLs, domainSubAdd, domainSync } from './domain.js';
|
|
11
|
+
import { runProject } from './projects.js';
|
|
12
|
+
import { runListing } from './listings.js';
|
|
13
|
+
import { runSendingDomain } from './sending-domains.js';
|
|
14
|
+
import { runCron, runFunction, runSecret } from './functions.js';
|
|
11
15
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
12
16
|
import { loginViaDeviceFlow } from './login.js';
|
|
13
17
|
import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
@@ -38,6 +42,25 @@ Usage:
|
|
|
38
42
|
shiply domain connect <domain> One-click OAuth DNS setup (Cloudflare/Domain Connect)
|
|
39
43
|
shiply domain sub add <host> --site <slug> Map a subdomain (e.g. app.example.com) to a slug
|
|
40
44
|
shiply domain sync <domain> Re-sync DNS records for a connected domain
|
|
45
|
+
shiply project <ls|create|get|archive|restore>
|
|
46
|
+
Customer-intake projects + AI brief generator
|
|
47
|
+
shiply project create <label> [--customer-email <e>] [--customer-name <n>]
|
|
48
|
+
Returns the intake URL to share with the customer
|
|
49
|
+
shiply listing <ls|create|rm> Marketplace listings (sell built sites)
|
|
50
|
+
shiply listing create <site-slug> --price <cents> --jurisdiction "<region>"
|
|
51
|
+
Requires Stripe Connect onboarding (see MCP get_connect_status)
|
|
52
|
+
shiply sending-domain <ls|add|verify|rm> Outbound email on a verified domain (Resend-backed)
|
|
53
|
+
shiply sending-domain add <domain> Returns DNS records to add at your registrar
|
|
54
|
+
shiply sending-domain verify <id> Re-check DNS after adding records
|
|
55
|
+
shiply function <deploy|get|rm> <slug> Deploy a Cloudflare Worker for your site (Workers Lite).
|
|
56
|
+
Reads worker.js from CWD; pass --ts for worker.ts.
|
|
57
|
+
Deploys + binds routes so https://<slug>.shiply.now/*
|
|
58
|
+
hits your code BEFORE static fallback. Developer plan.
|
|
59
|
+
shiply secret <set|ls|rm> <slug> Manage worker secrets (Stripe keys, API tokens, etc.)
|
|
60
|
+
shiply secret set <slug> NAME VALUE — becomes env.NAME
|
|
61
|
+
in your worker on next deploy/restart.
|
|
62
|
+
shiply cron <ls|set|rm> <slug> Manage cron triggers on the deployed worker
|
|
63
|
+
shiply cron set <slug> /api/cron/foo "0 9 * * *"
|
|
41
64
|
shiply data init [dir] Scaffold a starter .shiply/data.json in <dir> (default cwd)
|
|
42
65
|
shiply data list <slug> List collections on a site with counts
|
|
43
66
|
shiply data query <slug> <coll> [--limit N] [--cursor C] [--where '<json>']
|
|
@@ -50,14 +73,16 @@ Usage:
|
|
|
50
73
|
Delete every record in a collection
|
|
51
74
|
shiply claim verify <code> Confirm a pairing code from /claim/<slug>?pair=1
|
|
52
75
|
(uses .shiply.json from CWD)
|
|
53
|
-
shiply skill [--project]
|
|
76
|
+
shiply skill [--project] [--force] Install the shiply skill for your AI agent
|
|
54
77
|
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
78
|
+
(--force overwrites an existing skill — recommended weekly)
|
|
55
79
|
shiply login [--name <label>] Open a browser, click Allow → key saved
|
|
56
80
|
(--email <addr> falls back to legacy 6-digit code)
|
|
57
81
|
shiply help
|
|
58
82
|
|
|
59
83
|
Options:
|
|
60
84
|
--spa Single-page app mode (unknown paths fall back to index.html)
|
|
85
|
+
--ts (function deploy) read worker.ts instead of worker.js
|
|
61
86
|
--framework <name> Force the detector (e.g. hugo, nuxt, vite) for non-standard layouts
|
|
62
87
|
--claim-token <tok> Update a specific anonymous site (overrides .shiply.json)
|
|
63
88
|
--new-site Ignore .shiply.json and create a fresh site
|
|
@@ -120,12 +145,14 @@ async function main() {
|
|
|
120
145
|
wait: { type: 'boolean' },
|
|
121
146
|
'new-site': { type: 'boolean' },
|
|
122
147
|
project: { type: 'boolean' },
|
|
148
|
+
force: { type: 'boolean' },
|
|
123
149
|
timeout: { type: 'string' },
|
|
124
150
|
out: { type: 'string', short: 'o' },
|
|
125
151
|
site: { type: 'string' },
|
|
126
152
|
binding: { type: 'string' },
|
|
127
153
|
params: { type: 'string' },
|
|
128
154
|
postgres: { type: 'boolean' },
|
|
155
|
+
ts: { type: 'boolean' },
|
|
129
156
|
'preview-branch': { type: 'string' },
|
|
130
157
|
yes: { type: 'boolean' },
|
|
131
158
|
'no-confetti': { type: 'boolean' },
|
|
@@ -480,6 +507,43 @@ async function main() {
|
|
|
480
507
|
throw new Error('usage: shiply db <create|ls|sql|migrate|delete|attach|branch|branches|delete-branch|merge>');
|
|
481
508
|
}
|
|
482
509
|
}
|
|
510
|
+
case 'project': {
|
|
511
|
+
await runProject(positionals.slice(1));
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case 'listing': {
|
|
515
|
+
await runListing(positionals.slice(1));
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
case 'sending-domain': {
|
|
519
|
+
await runSendingDomain(positionals.slice(1));
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case 'function': {
|
|
523
|
+
await runFunction(positionals.slice(1), {
|
|
524
|
+
base: values.base,
|
|
525
|
+
key: values.key,
|
|
526
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
527
|
+
ts: Boolean(values.ts),
|
|
528
|
+
});
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case 'secret': {
|
|
532
|
+
await runSecret(positionals.slice(1), {
|
|
533
|
+
base: values.base,
|
|
534
|
+
key: values.key,
|
|
535
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case 'cron': {
|
|
540
|
+
await runCron(positionals.slice(1), {
|
|
541
|
+
base: values.base,
|
|
542
|
+
key: values.key,
|
|
543
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
544
|
+
});
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
483
547
|
case 'claim': {
|
|
484
548
|
if (dir !== 'verify') {
|
|
485
549
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
@@ -495,12 +559,11 @@ async function main() {
|
|
|
495
559
|
process.exit(r.ok ? 0 : 1);
|
|
496
560
|
}
|
|
497
561
|
case 'skill': {
|
|
498
|
-
const written = await installSkill(Boolean(values.project));
|
|
562
|
+
const written = await installSkill(Boolean(values.project), Boolean(values.force));
|
|
499
563
|
for (const w of written)
|
|
500
564
|
console.log(`✔ skill installed — ${w}`);
|
|
501
|
-
console.log('
|
|
502
|
-
|
|
503
|
-
return;
|
|
565
|
+
console.log('\nThe skill instructs your agent how to publish, manage sites, query databases, run demand tests, drive customer-intake projects, list/sell sites in the marketplace, manage inbox + sending domains, and more.');
|
|
566
|
+
break;
|
|
504
567
|
}
|
|
505
568
|
case 'login': {
|
|
506
569
|
const base = resolveBase(values.base);
|
package/dist/listings.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('listing commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function money(cents, currency = 'usd') {
|
|
46
|
+
return `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
47
|
+
}
|
|
48
|
+
async function listAction(ctx, flags) {
|
|
49
|
+
const base = resolveBase(ctx.base);
|
|
50
|
+
// The public REST surface doesn't yet expose GET /api/v1/listings. To list
|
|
51
|
+
// the seller's own listings without inventing an endpoint, defer to the
|
|
52
|
+
// dashboard URL the seller can open. Surface the same hint as the dashboard
|
|
53
|
+
// surfaces; agents wanting machine-readable listings should call the MCP
|
|
54
|
+
// `list_listings` tool.
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
printJson({
|
|
57
|
+
note: 'GET /api/v1/listings is not exposed; use the MCP `list_listings` tool for machine-readable output.',
|
|
58
|
+
dashboardUrl: `${resolveBase(ctx.base)}/dashboard/sales`,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log('No REST listing index endpoint yet — use the MCP tool `list_listings` for machine output.');
|
|
63
|
+
console.log(` dashboard: ${base}/dashboard/sales`);
|
|
64
|
+
// suppress unused-var warning while keeping the auth check active.
|
|
65
|
+
void headers(ctx.apiKey);
|
|
66
|
+
}
|
|
67
|
+
async function createAction(ctx, slug, flags) {
|
|
68
|
+
const priceFlag = flags.price;
|
|
69
|
+
if (typeof priceFlag !== 'string') {
|
|
70
|
+
throw new Error('usage: shiply listing create <site-slug> --price <cents> --jurisdiction "<region>" [--pitch "<text>"] [--terms standard|custom] [--terms-custom "<text>"] [--status draft|live]');
|
|
71
|
+
}
|
|
72
|
+
const priceCents = Number.parseInt(priceFlag, 10);
|
|
73
|
+
if (!Number.isFinite(priceCents) || priceCents < 100 || priceCents > 999_900) {
|
|
74
|
+
throw new Error('--price must be cents between 100 and 999900 (i.e. $1.00 – $9999.00)');
|
|
75
|
+
}
|
|
76
|
+
const jurisdiction = typeof flags.jurisdiction === 'string' ? flags.jurisdiction : undefined;
|
|
77
|
+
if (!jurisdiction || jurisdiction.length < 2) {
|
|
78
|
+
throw new Error('--jurisdiction is required, e.g. --jurisdiction "California, USA"');
|
|
79
|
+
}
|
|
80
|
+
const termsMode = (typeof flags.terms === 'string' ? flags.terms : 'standard');
|
|
81
|
+
if (termsMode !== 'standard' && termsMode !== 'custom') {
|
|
82
|
+
throw new Error('--terms must be "standard" or "custom"');
|
|
83
|
+
}
|
|
84
|
+
const status = typeof flags.status === 'string' ? flags.status : undefined;
|
|
85
|
+
if (status && status !== 'draft' && status !== 'live') {
|
|
86
|
+
throw new Error('--status must be "draft" or "live" on create');
|
|
87
|
+
}
|
|
88
|
+
const body = {
|
|
89
|
+
slug,
|
|
90
|
+
priceCents,
|
|
91
|
+
jurisdiction,
|
|
92
|
+
termsMode,
|
|
93
|
+
};
|
|
94
|
+
if (typeof flags.pitch === 'string')
|
|
95
|
+
body.pitch = flags.pitch;
|
|
96
|
+
if (typeof flags['terms-custom'] === 'string')
|
|
97
|
+
body.termsCustom = flags['terms-custom'];
|
|
98
|
+
if (status)
|
|
99
|
+
body.status = status;
|
|
100
|
+
const base = resolveBase(ctx.base);
|
|
101
|
+
const r = await api(`${base}/api/v1/listings`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: headers(ctx.apiKey),
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
});
|
|
106
|
+
if (flags.json) {
|
|
107
|
+
printJson(r);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`✔ listed ${slug} for ${money(r.priceCents, r.currency)} [${r.status}]`);
|
|
111
|
+
console.log(` id: ${r.id}`);
|
|
112
|
+
console.log(` marketplace: ${base}/marketplace/${slug}`);
|
|
113
|
+
console.log(` edit later: shiply listing update ${slug} --price <cents>`);
|
|
114
|
+
}
|
|
115
|
+
async function removeAction(ctx, slug, flags) {
|
|
116
|
+
// The REST surface for unlisting is PATCH /api/v1/listings/{id} with
|
|
117
|
+
// status: "draft". We don't have a slug→id resolver in REST, so the CLI
|
|
118
|
+
// currently requires the listing id via --id. Agents that only have the
|
|
119
|
+
// slug should use the MCP `delete_listing` tool which resolves slug→id
|
|
120
|
+
// server-side.
|
|
121
|
+
const id = typeof flags.id === 'string' ? flags.id : undefined;
|
|
122
|
+
if (!id) {
|
|
123
|
+
throw new Error('usage: shiply listing rm <site-slug> --id <listing-id> (use the MCP `delete_listing` tool to unlist by slug without needing the id)');
|
|
124
|
+
}
|
|
125
|
+
const base = resolveBase(ctx.base);
|
|
126
|
+
const r = await api(`${base}/api/v1/listings/${encodeURIComponent(id)}`, {
|
|
127
|
+
method: 'PATCH',
|
|
128
|
+
headers: headers(ctx.apiKey),
|
|
129
|
+
body: JSON.stringify({ status: 'draft' }),
|
|
130
|
+
});
|
|
131
|
+
if (flags.json) {
|
|
132
|
+
printJson(r);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`✔ unlisted ${slug} [${r.status}]`);
|
|
136
|
+
}
|
|
137
|
+
export async function runListing(argv) {
|
|
138
|
+
const sub = argv[0];
|
|
139
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
140
|
+
if (!sub || sub === 'help') {
|
|
141
|
+
console.log('usage: shiply listing <ls|create|rm> [args] [--json]');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const ctx = await ctxFromEnv(flags);
|
|
145
|
+
switch (sub) {
|
|
146
|
+
case 'ls':
|
|
147
|
+
case 'list':
|
|
148
|
+
await listAction(ctx, flags);
|
|
149
|
+
return;
|
|
150
|
+
case 'create': {
|
|
151
|
+
const slug = rest[0];
|
|
152
|
+
if (!slug) {
|
|
153
|
+
throw new Error('usage: shiply listing create <site-slug> --price <cents> --jurisdiction "<region>"');
|
|
154
|
+
}
|
|
155
|
+
await createAction(ctx, slug, flags);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case 'rm':
|
|
159
|
+
case 'delete': {
|
|
160
|
+
const slug = rest[0];
|
|
161
|
+
if (!slug)
|
|
162
|
+
throw new Error('usage: shiply listing rm <site-slug> --id <listing-id>');
|
|
163
|
+
await removeAction(ctx, slug, flags);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`unknown listing subcommand: ${sub} (try ls|create|rm)`);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/projects.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('project commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function shortRow(p) {
|
|
46
|
+
const customer = p.customerEmail ?? p.customerName ?? 'no customer';
|
|
47
|
+
return `${p.id} [${p.status}] ${p.label} (${customer})`;
|
|
48
|
+
}
|
|
49
|
+
async function listAction(ctx, flags) {
|
|
50
|
+
const base = resolveBase(ctx.base);
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (typeof flags.status === 'string')
|
|
53
|
+
params.set('status', flags.status);
|
|
54
|
+
if (typeof flags.q === 'string')
|
|
55
|
+
params.set('q', flags.q);
|
|
56
|
+
const qs = params.toString();
|
|
57
|
+
const url = `${base}/api/v1/projects${qs ? `?${qs}` : ''}`;
|
|
58
|
+
const r = await api(url, { headers: headers(ctx.apiKey) });
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
printJson(r.projects);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (r.projects.length === 0) {
|
|
64
|
+
console.log('No projects yet. Create one: shiply project create <label>');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const p of r.projects)
|
|
68
|
+
console.log(shortRow(p));
|
|
69
|
+
}
|
|
70
|
+
async function createAction(ctx, label, flags) {
|
|
71
|
+
const base = resolveBase(ctx.base);
|
|
72
|
+
const body = { label };
|
|
73
|
+
if (typeof flags['customer-email'] === 'string')
|
|
74
|
+
body.customerEmail = flags['customer-email'];
|
|
75
|
+
if (typeof flags['customer-name'] === 'string')
|
|
76
|
+
body.customerName = flags['customer-name'];
|
|
77
|
+
const r = await api(`${base}/api/v1/projects`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify(body) });
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
printJson(r);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const appBase = base.replace(/\/+$/, '');
|
|
83
|
+
console.log(`✔ created project ${r.id} [${r.status}] ${r.label}`);
|
|
84
|
+
console.log(` intake URL (share with customer):`);
|
|
85
|
+
console.log(` ${appBase}/intake/${r.intakeToken}`);
|
|
86
|
+
if (r.customerEmail)
|
|
87
|
+
console.log(` intake invite emailed to ${r.customerEmail}`);
|
|
88
|
+
}
|
|
89
|
+
async function getAction(ctx, id, flags) {
|
|
90
|
+
const base = resolveBase(ctx.base);
|
|
91
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, { headers: headers(ctx.apiKey) });
|
|
92
|
+
if (flags.json) {
|
|
93
|
+
printJson(r);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(JSON.stringify(r, null, 2));
|
|
97
|
+
}
|
|
98
|
+
async function archiveAction(ctx, id, flags) {
|
|
99
|
+
const base = resolveBase(ctx.base);
|
|
100
|
+
// Server archives via PATCH /api/v1/projects/{id} with { status: "archived" }.
|
|
101
|
+
// There is no dedicated /archive route — PATCH is the documented surface.
|
|
102
|
+
const body = { status: 'archived' };
|
|
103
|
+
if (typeof flags.reason === 'string')
|
|
104
|
+
body.archivedReason = flags.reason;
|
|
105
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, { method: 'PATCH', headers: headers(ctx.apiKey), body: JSON.stringify(body) });
|
|
106
|
+
if (flags.json) {
|
|
107
|
+
printJson(r);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`✔ archived ${r.id} [${r.status}] ${r.label}`);
|
|
111
|
+
console.log(` restore later with: shiply project restore ${r.id}`);
|
|
112
|
+
}
|
|
113
|
+
async function restoreAction(ctx, id, flags) {
|
|
114
|
+
const base = resolveBase(ctx.base);
|
|
115
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, {
|
|
116
|
+
method: 'PATCH',
|
|
117
|
+
headers: headers(ctx.apiKey),
|
|
118
|
+
body: JSON.stringify({ status: 'draft' }),
|
|
119
|
+
});
|
|
120
|
+
if (flags.json) {
|
|
121
|
+
printJson(r);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log(`✔ restored ${r.id} [${r.status}] ${r.label}`);
|
|
125
|
+
}
|
|
126
|
+
export async function runProject(argv) {
|
|
127
|
+
const sub = argv[0];
|
|
128
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
129
|
+
if (!sub || sub === 'help') {
|
|
130
|
+
console.log('usage: shiply project <ls|create|get|archive|restore> [args] [--json]');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const ctx = await ctxFromEnv(flags);
|
|
134
|
+
switch (sub) {
|
|
135
|
+
case 'ls':
|
|
136
|
+
case 'list':
|
|
137
|
+
await listAction(ctx, flags);
|
|
138
|
+
return;
|
|
139
|
+
case 'create': {
|
|
140
|
+
const label = rest[0];
|
|
141
|
+
if (!label) {
|
|
142
|
+
throw new Error('usage: shiply project create <label> [--customer-email <email>] [--customer-name <name>]');
|
|
143
|
+
}
|
|
144
|
+
await createAction(ctx, label, flags);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'get': {
|
|
148
|
+
const id = rest[0];
|
|
149
|
+
if (!id)
|
|
150
|
+
throw new Error('usage: shiply project get <id>');
|
|
151
|
+
await getAction(ctx, id, flags);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case 'archive': {
|
|
155
|
+
const id = rest[0];
|
|
156
|
+
if (!id)
|
|
157
|
+
throw new Error('usage: shiply project archive <id> [--reason "<text>"]');
|
|
158
|
+
await archiveAction(ctx, id, flags);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
case 'restore': {
|
|
162
|
+
const id = rest[0];
|
|
163
|
+
if (!id)
|
|
164
|
+
throw new Error('usage: shiply project restore <id>');
|
|
165
|
+
await restoreAction(ctx, id, flags);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`unknown project subcommand: ${sub} (try ls|create|get|archive|restore)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('sending-domain commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function printDns(d) {
|
|
46
|
+
if (!d.dns || d.dns.length === 0) {
|
|
47
|
+
console.log(' (no DNS records returned — call `shiply sending-domain verify <id>` after Resend provisions records)');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(' Add these DNS records at your registrar:');
|
|
51
|
+
for (const r of d.dns) {
|
|
52
|
+
const status = r.status ? ` [${r.status}]` : '';
|
|
53
|
+
console.log(` ${r.type.padEnd(5)} ${r.name} → ${r.value}${status}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function listAction(ctx, flags) {
|
|
57
|
+
const base = resolveBase(ctx.base);
|
|
58
|
+
const r = await api(`${base}/api/v1/sending-domains`, { headers: headers(ctx.apiKey) });
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
printJson(r.domains);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!r.domains.length) {
|
|
64
|
+
console.log('No sending domains yet. Add one: shiply sending-domain add <domain>');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const d of r.domains) {
|
|
68
|
+
const from = d.fromAddress ? ` from=${d.fromAddress}` : '';
|
|
69
|
+
console.log(`${d.domain} [${d.status}] id=${d.id}${from}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function addAction(ctx, domain, flags) {
|
|
73
|
+
const base = resolveBase(ctx.base);
|
|
74
|
+
const r = await api(`${base}/api/v1/sending-domains`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: headers(ctx.apiKey),
|
|
77
|
+
body: JSON.stringify({ domain }),
|
|
78
|
+
});
|
|
79
|
+
if (flags.json) {
|
|
80
|
+
printJson(r);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`✔ ${r.domain} registered [${r.status}] id=${r.id}`);
|
|
84
|
+
printDns(r);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` After DNS propagates: shiply sending-domain verify ${r.id}`);
|
|
87
|
+
}
|
|
88
|
+
async function verifyAction(ctx, id, flags) {
|
|
89
|
+
const base = resolveBase(ctx.base);
|
|
90
|
+
const r = await api(`${base}/api/v1/sending-domains/${encodeURIComponent(id)}/verify`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
printJson(r);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`${r.domain} [${r.status}]`);
|
|
96
|
+
if (r.status === 'verified') {
|
|
97
|
+
console.log(' ✔ ready — shiply will send your demand-test emails from this domain');
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
printDns(r);
|
|
101
|
+
console.log(' Re-run `shiply sending-domain verify ' + r.id + '` after DNS updates propagate.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function removeAction(ctx, id, flags) {
|
|
105
|
+
if (!flags.yes) {
|
|
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
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const base = resolveBase(ctx.base);
|
|
110
|
+
await api(`${base}/api/v1/sending-domains/${encodeURIComponent(id)}`, {
|
|
111
|
+
method: 'DELETE',
|
|
112
|
+
headers: headers(ctx.apiKey),
|
|
113
|
+
});
|
|
114
|
+
if (flags.json) {
|
|
115
|
+
printJson({ removed: id });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(`✔ removed sending domain ${id}`);
|
|
119
|
+
}
|
|
120
|
+
export async function runSendingDomain(argv) {
|
|
121
|
+
const sub = argv[0];
|
|
122
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
123
|
+
if (!sub || sub === 'help') {
|
|
124
|
+
console.log('usage: shiply sending-domain <ls|add|verify|rm> [args] [--json]');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const ctx = await ctxFromEnv(flags);
|
|
128
|
+
switch (sub) {
|
|
129
|
+
case 'ls':
|
|
130
|
+
case 'list':
|
|
131
|
+
await listAction(ctx, flags);
|
|
132
|
+
return;
|
|
133
|
+
case 'add': {
|
|
134
|
+
const domain = rest[0];
|
|
135
|
+
if (!domain)
|
|
136
|
+
throw new Error('usage: shiply sending-domain add <domain>');
|
|
137
|
+
await addAction(ctx, domain, flags);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
case 'verify': {
|
|
141
|
+
const id = rest[0];
|
|
142
|
+
if (!id)
|
|
143
|
+
throw new Error('usage: shiply sending-domain verify <id>');
|
|
144
|
+
await verifyAction(ctx, id, flags);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'rm':
|
|
148
|
+
case 'remove':
|
|
149
|
+
case 'delete': {
|
|
150
|
+
const id = rest[0];
|
|
151
|
+
if (!id)
|
|
152
|
+
throw new Error('usage: shiply sending-domain rm <id> --yes');
|
|
153
|
+
await removeAction(ctx, id, flags);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`unknown sending-domain subcommand: ${sub} (try ls|add|verify|rm)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/skill.js
CHANGED
|
@@ -16,12 +16,16 @@ export function installTargets(project) {
|
|
|
16
16
|
}
|
|
17
17
|
return [{ agent: 'Claude Code (all projects)', dir: join(homedir(), '.claude', 'skills', 'shiply') }];
|
|
18
18
|
}
|
|
19
|
-
export async function installSkill(project) {
|
|
19
|
+
export async function installSkill(project, force = false) {
|
|
20
20
|
const content = await bundledSkill();
|
|
21
21
|
const written = [];
|
|
22
22
|
for (const t of installTargets(project)) {
|
|
23
23
|
await mkdir(t.dir, { recursive: true });
|
|
24
24
|
const file = join(t.dir, 'SKILL.md');
|
|
25
|
+
// writeFile always overwrites; --force is an explicit signal of intent
|
|
26
|
+
// (and matches what users type from the AGENT_PROMPT). Without --force we
|
|
27
|
+
// still overwrite — but a future version may add a sanity check.
|
|
28
|
+
void force;
|
|
25
29
|
await writeFile(file, content);
|
|
26
30
|
written.push(`${t.agent}: ${file}`);
|
|
27
31
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -3,6 +3,18 @@ name: shiply
|
|
|
3
3
|
description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
> **Skill freshness check.** Shiply ships new capabilities weekly. If this
|
|
9
|
+
> file's "Last updated" below is more than 7 days old, re-install it with
|
|
10
|
+
> `npx -y shiply-cli@latest skill --force` OR fetch `https://shiply.now/skill.md`
|
|
11
|
+
> fresh — your cached copy may be missing features like databases, projects,
|
|
12
|
+
> marketplace, or sending domains.
|
|
13
|
+
>
|
|
14
|
+
> **Last updated: 2026-06-20**
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
6
18
|
# shiply — instant web hosting for agents
|
|
7
19
|
|
|
8
20
|
shiply.now puts files on the web in seconds. No account needed to start:
|
|
@@ -126,11 +138,23 @@ lines for automation; exit code 0 = ready.
|
|
|
126
138
|
### 3. Raw HTTP (no installs)
|
|
127
139
|
```
|
|
128
140
|
1. POST https://shiply.now/api/v1/publish
|
|
129
|
-
{
|
|
141
|
+
{
|
|
142
|
+
"agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
|
|
143
|
+
"files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]
|
|
144
|
+
}
|
|
130
145
|
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
131
146
|
2. PUT each file's bytes to response upload.uploads[].url
|
|
132
147
|
3. POST upload.finalizeUrl with {"versionId":"..."}
|
|
133
148
|
```
|
|
149
|
+
|
|
150
|
+
**ALWAYS include `agentName` on anonymous publishes.** The response then
|
|
151
|
+
carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
|
|
152
|
+
expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
|
|
153
|
+
poll `poll_url` every `interval` seconds with the `device_code`, and on
|
|
154
|
+
`{status:"approved"}` you get a permanent `api_key` PLUS the linked site
|
|
155
|
+
is claimed in the same Allow click. Without `agentName` the user has to
|
|
156
|
+
do a separate manual claim step every time.
|
|
157
|
+
|
|
134
158
|
**Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
|
|
135
159
|
first publish — save it); owned → include `"slug":"..."`. Hashes make updates
|
|
136
160
|
cheap: unchanged files are skipped server-side.
|
|
@@ -318,6 +342,180 @@ providers; `db_schema` introspects `sqlite_master` (D1) or
|
|
|
318
342
|
`information_schema` (Neon). Full docs:
|
|
319
343
|
https://shiply.now/docs/databases
|
|
320
344
|
|
|
345
|
+
## Functions (Workers Lite) — webhooks, cron, secrets, full backend (Developer plan)
|
|
346
|
+
|
|
347
|
+
shiply Workers Lite lets a published site include server-side code that runs
|
|
348
|
+
on every request. Use when the user needs:
|
|
349
|
+
- A webhook receiver (Stripe, GitHub, etc.) with raw body + signature verification
|
|
350
|
+
- A cron job (daily reminders, periodic sync, retention emails)
|
|
351
|
+
- A privileged API call using secrets (Stripe key, OpenAI key) without exposing them to the browser
|
|
352
|
+
- An authenticated mutation on D1 / Neon (do the auth check in the function before writing)
|
|
353
|
+
|
|
354
|
+
### How it works
|
|
355
|
+
|
|
356
|
+
Author `worker.js` (or `worker.ts`) at the publish root. On `shiply publish`,
|
|
357
|
+
shiply compiles (TS) + deploys it as a per-site Cloudflare Worker bound to
|
|
358
|
+
the site's hostname. The worker handles every request; pass static asset
|
|
359
|
+
requests through `env.ASSETS.fetch(request)`.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { verifyStripeSig, json } from 'shiply-runtime'
|
|
363
|
+
|
|
364
|
+
export default {
|
|
365
|
+
async fetch(req: Request, env: Env): Promise<Response> {
|
|
366
|
+
const url = new URL(req.url)
|
|
367
|
+
if (url.pathname === '/api/webhooks/stripe' && req.method === 'POST') {
|
|
368
|
+
const body = await req.text()
|
|
369
|
+
if (!await verifyStripeSig(body, req.headers.get('stripe-signature'), env.STRIPE_WEBHOOK_SECRET)) {
|
|
370
|
+
return new Response('bad sig', { status: 400 })
|
|
371
|
+
}
|
|
372
|
+
// ... handle event, write to env.SITE_DB
|
|
373
|
+
return new Response('ok')
|
|
374
|
+
}
|
|
375
|
+
return env.ASSETS.fetch(req)
|
|
376
|
+
},
|
|
377
|
+
async scheduled(event: ScheduledEvent, env: Env) {
|
|
378
|
+
if (event.cron === '0 9 * * *') { /* daily job */ }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`.shiply/crons.json` declares cron schedules. `.shiply/secrets.json` declares
|
|
384
|
+
secret names (values set via CLI/MCP, never reach the publish payload).
|
|
385
|
+
|
|
386
|
+
### Bindings available in `env`
|
|
387
|
+
|
|
388
|
+
- `env.ASSETS` — fall through to static (`env.ASSETS.fetch(request)`)
|
|
389
|
+
- `env.SITE_DB` — attached D1 (if `shiply db attach` was run)
|
|
390
|
+
- `env.<NAME>` — each shiply Variable becomes a plain-text env var
|
|
391
|
+
- `env.<NAME>` — each secret set via `set_secret` is encrypted, accessible as env var
|
|
392
|
+
|
|
393
|
+
### MCP tools
|
|
394
|
+
|
|
395
|
+
| Tool | Purpose |
|
|
396
|
+
|---|---|
|
|
397
|
+
| `deploy_function` | Deploy a Worker function (alternative to publish auto-detection) |
|
|
398
|
+
| `get_function` | Read the deployed source + metadata |
|
|
399
|
+
| `remove_function` | Strip function + secrets + crons; fall back to static |
|
|
400
|
+
| `set_secret` / `list_secrets` / `remove_secret` | Manage CF Worker secrets |
|
|
401
|
+
| `set_cron` / `list_crons` / `remove_cron` | Manage cron triggers |
|
|
402
|
+
| `get_function_logs` | Deep-link to CF dashboard for live tail |
|
|
403
|
+
|
|
404
|
+
### CLI
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
shiply publish . # auto-detects worker.js + crons.json + secrets.json
|
|
408
|
+
shiply function deploy <slug> # alternative: upload worker.js without re-publishing
|
|
409
|
+
shiply function deploy <slug> --ts # uploads worker.ts (server-side compile)
|
|
410
|
+
shiply secret set <slug> STRIPE_KEY sk_xxx # set secret value
|
|
411
|
+
shiply cron set <slug> /api/daily "0 9 * * *"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### REST
|
|
415
|
+
|
|
416
|
+
`POST/GET/DELETE /api/v1/sites/{slug}/function` · `POST/GET /api/v1/sites/{slug}/secrets` · `DELETE /api/v1/sites/{slug}/secrets/{name}` · `GET/POST/DELETE /api/v1/sites/{slug}/crons`
|
|
417
|
+
|
|
418
|
+
### Limits + plan
|
|
419
|
+
|
|
420
|
+
- 1 MB compiled script size, 30s CPU per request, V8 isolate runtime (no Node APIs)
|
|
421
|
+
- 50 secrets, 20 cron triggers per site
|
|
422
|
+
- Plan-gated to Developer: Founder/Hobby see `402 payment_required`. Upgrade: shiply.now/dashboard/plan
|
|
423
|
+
|
|
424
|
+
Docs: https://shiply.now/docs/functions
|
|
425
|
+
|
|
426
|
+
## Projects — customer intake + AI brief
|
|
427
|
+
|
|
428
|
+
When a dev needs to capture a real client brief, they spin up a **project**:
|
|
429
|
+
the dashboard (or `shiply project create`) mints a one-URL intake form to
|
|
430
|
+
share with the customer. The customer fills a 10-step wizard, attaches
|
|
431
|
+
files, hits submit. An AI brief generator (MiniMax-M2 with Anthropic
|
|
432
|
+
fallback) turns the answers into a structured brief the dev reviews and
|
|
433
|
+
edits inline. All files flow into a per-project drive folder.
|
|
434
|
+
|
|
435
|
+
MCP tools:
|
|
436
|
+
```
|
|
437
|
+
list_projects — your projects (filter status/q)
|
|
438
|
+
create_project — start a new intake; pass customerName/customerEmail to email them the link
|
|
439
|
+
get_project — full project + brief
|
|
440
|
+
update_brief — patch brief jsonb (after AI generation)
|
|
441
|
+
regenerate_brief — re-run AI from current intake responses
|
|
442
|
+
archive_project / restore_project — lifecycle
|
|
443
|
+
list_project_files — files uploaded by the customer
|
|
444
|
+
resend_intake_invite — re-email the customer their intake link (requires customerEmail on project)
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
REST: `POST/GET /api/v1/projects · GET/PATCH /api/v1/projects/{id} · POST /api/v1/projects/{id}/regenerate-brief`
|
|
448
|
+
(archive: `PATCH /api/v1/projects/{id}` with `{"status":"archived"}` — no
|
|
449
|
+
dedicated `/archive` route).
|
|
450
|
+
|
|
451
|
+
CLI: `shiply project ls · shiply project create <label> [--customer-email <e>] [--customer-name <n>] · shiply project get <id> · shiply project archive <id>`
|
|
452
|
+
|
|
453
|
+
Docs: https://shiply.now/docs/projects
|
|
454
|
+
|
|
455
|
+
## Marketplace — sell built sites
|
|
456
|
+
|
|
457
|
+
Devs can list any owned site for sale: set a price + short pitch, choose
|
|
458
|
+
standard or custom terms, pick a jurisdiction. Stripe Connect Express
|
|
459
|
+
handles seller payouts — sellers complete a one-time onboarding before
|
|
460
|
+
listing (`get_connect_status` returns the onboarding URL when needed).
|
|
461
|
+
Buyers pay via Stripe Checkout; on `checkout.session.completed` the
|
|
462
|
+
webhook flips the order to `paid` and transfers site ownership
|
|
463
|
+
atomically. Refunds are available within the order's refund window
|
|
464
|
+
(`refundExpiresAt`); a refund reverts ownership to the seller.
|
|
465
|
+
|
|
466
|
+
Prices are passed as `priceCents` (whole-dollar cents between 100 and
|
|
467
|
+
999900). `termsMode='standard'` uses shiply's template; `'custom'`
|
|
468
|
+
requires `termsCustom` ≥50 chars.
|
|
469
|
+
|
|
470
|
+
MCP tools:
|
|
471
|
+
```
|
|
472
|
+
list_listings — my listings
|
|
473
|
+
create_listing — list a site (siteSlug, priceCents, pitch?, termsMode, jurisdiction, ...)
|
|
474
|
+
update_listing — change price/pitch/status (draft|live|paused)
|
|
475
|
+
delete_listing — pulls off marketplace (sets status=draft)
|
|
476
|
+
list_my_sales — orders where I'm the seller
|
|
477
|
+
list_my_orders — orders where I'm the buyer
|
|
478
|
+
refund_order — issue Stripe refund (within refund window; goes back to buyer)
|
|
479
|
+
get_connect_status — Stripe Connect onboarding state + actionable URL
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
REST: `POST /api/v1/listings · PATCH /api/v1/listings/{id} · POST /api/v1/listings/{id}/checkout · POST /api/v1/orders/{id}/refund · GET /api/v1/connect/status`
|
|
483
|
+
(no GET listing index over REST yet — use the MCP `list_listings` /
|
|
484
|
+
`list_my_sales` / `list_my_orders` tools for machine output, or the
|
|
485
|
+
dashboard `/dashboard/sales` page for humans.)
|
|
486
|
+
|
|
487
|
+
CLI: `shiply listing ls · shiply listing create <site-slug> --price <cents> --jurisdiction "<region>" · shiply listing rm <slug> --id <listing-id>`
|
|
488
|
+
|
|
489
|
+
**Stripe Connect note:** Sellers must complete one-time Stripe Connect
|
|
490
|
+
onboarding before listing. Call `get_connect_status` first — when status
|
|
491
|
+
isn't `'ready'` it returns an `onboardingUrl` you should hand to the
|
|
492
|
+
user as a clickable link.
|
|
493
|
+
|
|
494
|
+
Docs: https://shiply.now/docs/marketplace
|
|
495
|
+
|
|
496
|
+
## Sending domains — outbound email on your domain
|
|
497
|
+
|
|
498
|
+
For agents that want shiply to send emails (demand-test broadcasts,
|
|
499
|
+
project intake invites, transactional notifications) from a custom
|
|
500
|
+
verified domain instead of the shared shiply pool. Backed by Resend —
|
|
501
|
+
add the DNS records they return at your registrar, then call
|
|
502
|
+
`verify_sending_domain` to re-check.
|
|
503
|
+
|
|
504
|
+
MCP tools:
|
|
505
|
+
```
|
|
506
|
+
list_sending_domains — all your sending domains
|
|
507
|
+
add_sending_domain — start: returns DNS records to add at your registrar
|
|
508
|
+
verify_sending_domain — re-check DNS after adding records
|
|
509
|
+
remove_sending_domain — tear down
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
REST: `GET/POST /api/v1/sending-domains · POST /api/v1/sending-domains/{id}/verify · DELETE /api/v1/sending-domains/{id}`
|
|
513
|
+
|
|
514
|
+
CLI: `shiply sending-domain ls · shiply sending-domain add <domain> · shiply sending-domain verify <id> · shiply sending-domain rm <id> --yes`
|
|
515
|
+
|
|
516
|
+
Cannot be a `shiply.now` subdomain. Verified status flips once SPF +
|
|
517
|
+
DKIM (+ MX for inbound) all check out.
|
|
518
|
+
|
|
321
519
|
## Make a site private (paid plans)
|
|
322
520
|
To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
|
|
323
521
|
with {"mode":"password","password":"..."} or {"mode":"restricted",
|