shiply-cli 0.12.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.js +29 -5
- package/dist/functions.js +342 -0
- package/dist/index.js +118 -7
- 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/dist/status-cmd.js +117 -0
- package/package.json +1 -1
- package/skill/SKILL.md +188 -2
package/dist/db.js
CHANGED
|
@@ -55,6 +55,11 @@ export async function dbCreate(ctx, name, opts) {
|
|
|
55
55
|
console.log(` recorded in .shiply.json — next publish will attach to ${existing.slug}`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function fmtSize(bytes) {
|
|
59
|
+
return bytes < 1024 * 1024
|
|
60
|
+
? `${(bytes / 1024).toFixed(1)} KB`
|
|
61
|
+
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
62
|
+
}
|
|
58
63
|
export async function dbLs(ctx) {
|
|
59
64
|
const base = resolveBase(ctx.base);
|
|
60
65
|
const r = await api(`${base}/api/v1/databases`, {
|
|
@@ -64,11 +69,30 @@ export async function dbLs(ctx) {
|
|
|
64
69
|
console.log('No databases yet. Create one: shiply db create <name>');
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
// 4-column table: Name | Provider | Size | Attached Site. Pad to the widest
|
|
73
|
+
// value in each column so the eye can scan straight down — cheap and faster
|
|
74
|
+
// than spinning a table library for ≤plan-cap rows.
|
|
75
|
+
const cols = r.databases.map((d) => ({
|
|
76
|
+
name: d.name,
|
|
77
|
+
provider: `[${d.provider}]`,
|
|
78
|
+
size: fmtSize(d.sizeBytes),
|
|
79
|
+
site: d.attachedSite
|
|
80
|
+
? d.attachedSite.slug
|
|
81
|
+
: d.siteId
|
|
82
|
+
? '(attached)'
|
|
83
|
+
: '(unbound)',
|
|
84
|
+
}));
|
|
85
|
+
const widths = {
|
|
86
|
+
name: Math.max('NAME'.length, ...cols.map((c) => c.name.length)),
|
|
87
|
+
provider: Math.max('PROVIDER'.length, ...cols.map((c) => c.provider.length)),
|
|
88
|
+
size: Math.max('SIZE'.length, ...cols.map((c) => c.size.length)),
|
|
89
|
+
};
|
|
90
|
+
// Header row keeps the output friendly for humans without breaking grep —
|
|
91
|
+
// single space + '#' prefix would have been cute but inconsistent with `db
|
|
92
|
+
// branches` and `drive ls`. We just lean on padding.
|
|
93
|
+
console.log(`${'NAME'.padEnd(widths.name)} ${'PROVIDER'.padEnd(widths.provider)} ${'SIZE'.padEnd(widths.size)} ATTACHED SITE`);
|
|
94
|
+
for (const c of cols) {
|
|
95
|
+
console.log(`${c.name.padEnd(widths.name)} ${c.provider.padEnd(widths.provider)} ${c.size.padEnd(widths.size)} ${c.site}`);
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
export async function dbSql(ctx, idOrName, sql, paramsJson) {
|
|
@@ -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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
2
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { parseArgs } from 'node:util';
|
|
4
7
|
import { confetti } from './confetti.js';
|
|
5
8
|
import { runDetect } from './detect.js';
|
|
@@ -8,12 +11,34 @@ import { runClaimVerify } from './claim.js';
|
|
|
8
11
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
9
12
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
10
13
|
import { domainAdd, domainConnect, domainLs, domainSubAdd, domainSync } from './domain.js';
|
|
14
|
+
import { runProject } from './projects.js';
|
|
15
|
+
import { runListing } from './listings.js';
|
|
16
|
+
import { runSendingDomain } from './sending-domains.js';
|
|
17
|
+
import { runCron, runFunction, runSecret } from './functions.js';
|
|
11
18
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
12
19
|
import { loginViaDeviceFlow } from './login.js';
|
|
13
20
|
import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
14
21
|
import { installSkill } from './skill.js';
|
|
15
22
|
import { readState, writeState } from './state.js';
|
|
16
23
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
24
|
+
import { runStatus } from './status-cmd.js';
|
|
25
|
+
// Resolve the package.json beside the compiled dist/ so `shiply --version`
|
|
26
|
+
// can print the running CLI version (works in dev tsc output + npm install).
|
|
27
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
function readPkgVersion() {
|
|
29
|
+
for (const candidate of [join(HERE, '..', 'package.json'), join(HERE, 'package.json')]) {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
32
|
+
if (pkg.version)
|
|
33
|
+
return pkg.version;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* keep looking */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return '0.0.0';
|
|
40
|
+
}
|
|
41
|
+
const VERSION = readPkgVersion();
|
|
17
42
|
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
18
43
|
|
|
19
44
|
Usage:
|
|
@@ -21,6 +46,7 @@ Usage:
|
|
|
21
46
|
Re-running UPDATES the same site (state in .shiply.json)
|
|
22
47
|
shiply update <dir> Same as publish when .shiply.json exists
|
|
23
48
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
49
|
+
(no arg → account/plan status, see below)
|
|
24
50
|
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
25
51
|
shiply detect [dir] Diagnose: print framework + dir that would upload
|
|
26
52
|
(no upload — use --framework=<name> to test an override)
|
|
@@ -38,6 +64,25 @@ Usage:
|
|
|
38
64
|
shiply domain connect <domain> One-click OAuth DNS setup (Cloudflare/Domain Connect)
|
|
39
65
|
shiply domain sub add <host> --site <slug> Map a subdomain (e.g. app.example.com) to a slug
|
|
40
66
|
shiply domain sync <domain> Re-sync DNS records for a connected domain
|
|
67
|
+
shiply project <ls|create|get|archive|restore>
|
|
68
|
+
Customer-intake projects + AI brief generator
|
|
69
|
+
shiply project create <label> [--customer-email <e>] [--customer-name <n>]
|
|
70
|
+
Returns the intake URL to share with the customer
|
|
71
|
+
shiply listing <ls|create|rm> Marketplace listings (sell built sites)
|
|
72
|
+
shiply listing create <site-slug> --price <cents> --jurisdiction "<region>"
|
|
73
|
+
Requires Stripe Connect onboarding (see MCP get_connect_status)
|
|
74
|
+
shiply sending-domain <ls|add|verify|rm> Outbound email on a verified domain (Resend-backed)
|
|
75
|
+
shiply sending-domain add <domain> Returns DNS records to add at your registrar
|
|
76
|
+
shiply sending-domain verify <id> Re-check DNS after adding records
|
|
77
|
+
shiply function <deploy|get|rm> <slug> Deploy a Cloudflare Worker for your site (Workers Lite).
|
|
78
|
+
Reads worker.js from CWD; pass --ts for worker.ts.
|
|
79
|
+
Deploys + binds routes so https://<slug>.shiply.now/*
|
|
80
|
+
hits your code BEFORE static fallback. Developer plan.
|
|
81
|
+
shiply secret <set|ls|rm> <slug> Manage worker secrets (Stripe keys, API tokens, etc.)
|
|
82
|
+
shiply secret set <slug> NAME VALUE — becomes env.NAME
|
|
83
|
+
in your worker on next deploy/restart.
|
|
84
|
+
shiply cron <ls|set|rm> <slug> Manage cron triggers on the deployed worker
|
|
85
|
+
shiply cron set <slug> /api/cron/foo "0 9 * * *"
|
|
41
86
|
shiply data init [dir] Scaffold a starter .shiply/data.json in <dir> (default cwd)
|
|
42
87
|
shiply data list <slug> List collections on a site with counts
|
|
43
88
|
shiply data query <slug> <coll> [--limit N] [--cursor C] [--where '<json>']
|
|
@@ -50,14 +95,18 @@ Usage:
|
|
|
50
95
|
Delete every record in a collection
|
|
51
96
|
shiply claim verify <code> Confirm a pairing code from /claim/<slug>?pair=1
|
|
52
97
|
(uses .shiply.json from CWD)
|
|
53
|
-
shiply
|
|
98
|
+
shiply status [--json] With no arg: print account plan + capabilities +
|
|
99
|
+
upgrade URL (pass --json for raw API output)
|
|
100
|
+
shiply skill [--project] [--force] Install the shiply skill for your AI agent
|
|
54
101
|
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
102
|
+
(--force overwrites an existing skill — recommended weekly)
|
|
55
103
|
shiply login [--name <label>] Open a browser, click Allow → key saved
|
|
56
104
|
(--email <addr> falls back to legacy 6-digit code)
|
|
57
105
|
shiply help
|
|
58
106
|
|
|
59
107
|
Options:
|
|
60
108
|
--spa Single-page app mode (unknown paths fall back to index.html)
|
|
109
|
+
--ts (function deploy) read worker.ts instead of worker.js
|
|
61
110
|
--framework <name> Force the detector (e.g. hugo, nuxt, vite) for non-standard layouts
|
|
62
111
|
--claim-token <tok> Update a specific anonymous site (overrides .shiply.json)
|
|
63
112
|
--new-site Ignore .shiply.json and create a fresh site
|
|
@@ -106,7 +155,19 @@ async function reportReadiness(host, celebrate) {
|
|
|
106
155
|
return r.ready;
|
|
107
156
|
}
|
|
108
157
|
async function main() {
|
|
158
|
+
// `shiply status --json` is a flag (raw account/plan status to stdout).
|
|
159
|
+
// But the global `--json` parseArgs option is a string (data-insert body).
|
|
160
|
+
// Detect the bare flag in raw argv BEFORE parseArgs sees it; strip the
|
|
161
|
+
// token so the parser doesn't complain about a missing string value.
|
|
162
|
+
const rawArgv = process.argv.slice(2);
|
|
163
|
+
const statusJsonFlag = rawArgv[0] === 'status' && rawArgv.includes('--json') &&
|
|
164
|
+
!rawArgv.some((a, i) => a === '--json' && typeof rawArgv[i + 1] === 'string' && !rawArgv[i + 1].startsWith('-'));
|
|
165
|
+
if (statusJsonFlag) {
|
|
166
|
+
const idx = rawArgv.indexOf('--json');
|
|
167
|
+
rawArgv.splice(idx, 1);
|
|
168
|
+
}
|
|
109
169
|
const { values, positionals } = parseArgs({
|
|
170
|
+
args: rawArgv,
|
|
110
171
|
allowPositionals: true,
|
|
111
172
|
options: {
|
|
112
173
|
spa: { type: 'boolean' },
|
|
@@ -120,12 +181,14 @@ async function main() {
|
|
|
120
181
|
wait: { type: 'boolean' },
|
|
121
182
|
'new-site': { type: 'boolean' },
|
|
122
183
|
project: { type: 'boolean' },
|
|
184
|
+
force: { type: 'boolean' },
|
|
123
185
|
timeout: { type: 'string' },
|
|
124
186
|
out: { type: 'string', short: 'o' },
|
|
125
187
|
site: { type: 'string' },
|
|
126
188
|
binding: { type: 'string' },
|
|
127
189
|
params: { type: 'string' },
|
|
128
190
|
postgres: { type: 'boolean' },
|
|
191
|
+
ts: { type: 'boolean' },
|
|
129
192
|
'preview-branch': { type: 'string' },
|
|
130
193
|
yes: { type: 'boolean' },
|
|
131
194
|
'no-confetti': { type: 'boolean' },
|
|
@@ -134,9 +197,14 @@ async function main() {
|
|
|
134
197
|
cursor: { type: 'string' },
|
|
135
198
|
where: { type: 'string' },
|
|
136
199
|
help: { type: 'boolean', short: 'h' },
|
|
200
|
+
version: { type: 'boolean', short: 'v' },
|
|
137
201
|
},
|
|
138
202
|
});
|
|
139
203
|
const [cmd, dir] = positionals;
|
|
204
|
+
if (values.version || cmd === 'version' || cmd === '--version') {
|
|
205
|
+
console.log(VERSION);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
140
208
|
if (values.help || !cmd || cmd === 'help') {
|
|
141
209
|
process.stdout.write(HELP);
|
|
142
210
|
return;
|
|
@@ -221,8 +289,15 @@ async function main() {
|
|
|
221
289
|
return;
|
|
222
290
|
}
|
|
223
291
|
case 'status': {
|
|
224
|
-
if (!dir)
|
|
225
|
-
|
|
292
|
+
if (!dir) {
|
|
293
|
+
// No positional → account/plan status. Reads the saved API key.
|
|
294
|
+
await runStatus({
|
|
295
|
+
base: values.base,
|
|
296
|
+
key: values.key,
|
|
297
|
+
json: statusJsonFlag || (typeof values.json === 'string' && values.json.length > 0),
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
226
301
|
const host = targetToHostname(dir);
|
|
227
302
|
const celebrate = !values['no-confetti'];
|
|
228
303
|
console.log(`checking https://${host}/ …`);
|
|
@@ -480,6 +555,43 @@ async function main() {
|
|
|
480
555
|
throw new Error('usage: shiply db <create|ls|sql|migrate|delete|attach|branch|branches|delete-branch|merge>');
|
|
481
556
|
}
|
|
482
557
|
}
|
|
558
|
+
case 'project': {
|
|
559
|
+
await runProject(positionals.slice(1));
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
case 'listing': {
|
|
563
|
+
await runListing(positionals.slice(1));
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'sending-domain': {
|
|
567
|
+
await runSendingDomain(positionals.slice(1));
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'function': {
|
|
571
|
+
await runFunction(positionals.slice(1), {
|
|
572
|
+
base: values.base,
|
|
573
|
+
key: values.key,
|
|
574
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
575
|
+
ts: Boolean(values.ts),
|
|
576
|
+
});
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case 'secret': {
|
|
580
|
+
await runSecret(positionals.slice(1), {
|
|
581
|
+
base: values.base,
|
|
582
|
+
key: values.key,
|
|
583
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
584
|
+
});
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'cron': {
|
|
588
|
+
await runCron(positionals.slice(1), {
|
|
589
|
+
base: values.base,
|
|
590
|
+
key: values.key,
|
|
591
|
+
json: typeof values.json === 'string' && values.json.length > 0,
|
|
592
|
+
});
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
483
595
|
case 'claim': {
|
|
484
596
|
if (dir !== 'verify') {
|
|
485
597
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
@@ -495,12 +607,11 @@ async function main() {
|
|
|
495
607
|
process.exit(r.ok ? 0 : 1);
|
|
496
608
|
}
|
|
497
609
|
case 'skill': {
|
|
498
|
-
const written = await installSkill(Boolean(values.project));
|
|
610
|
+
const written = await installSkill(Boolean(values.project), Boolean(values.force));
|
|
499
611
|
for (const w of written)
|
|
500
612
|
console.log(`✔ skill installed — ${w}`);
|
|
501
|
-
console.log('
|
|
502
|
-
|
|
503
|
-
return;
|
|
613
|
+
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.');
|
|
614
|
+
break;
|
|
504
615
|
}
|
|
505
616
|
case 'login': {
|
|
506
617
|
const base = resolveBase(values.base);
|