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 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
- for (const d of r.databases) {
68
- const size = d.sizeBytes < 1024 * 1024
69
- ? `${(d.sizeBytes / 1024).toFixed(1)} KB`
70
- : `${(d.sizeBytes / 1024 / 1024).toFixed(1)} MB`;
71
- console.log(`${d.name} [${d.provider}] ${size}${d.siteId ? '' : ' (unbound)'}`);
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 skill [--project] Install the shiply skill for your AI agent
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
- throw new Error('usage: shiply status <slug-or-domain> [--wait]');
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(' Your agent now knows how to publish, update (same URL!), check SSL, and more.');
502
- console.log(' Restart the agent session to pick it up. Re-run after CLI upgrades.');
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);