shiply-cli 0.14.0 → 0.14.2

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) {
package/dist/domain.js CHANGED
@@ -24,11 +24,46 @@ export async function domainConnect(ctx, domain) {
24
24
  const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/connect`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
25
25
  console.log(`Open this link in your browser to connect ${domain}:\n ${r.url}`);
26
26
  }
27
- export async function domainSubAdd(ctx, hostname, slug) {
28
- const parts = hostname.split('.');
29
- // NOTE: naive registrable-domain split (last two labels) wrong for multi-label TLDs like co.uk; the dashboard avoids this by passing the parent domain explicitly.
27
+ /** Common 2-label public suffixes. Not exhaustive — but covers the registrar
28
+ * ccTLDs we've seen in the wild (au, uk, nz, za, br, in, jp, kr, mx, co...).
29
+ * For anything else we fall back to the last-two-labels heuristic, which is
30
+ * correct for the vast majority of TLDs. A future improvement is to ship the
31
+ * Public Suffix List as data; this keeps the CLI bundle tiny. */
32
+ const TWO_LABEL_SUFFIXES = new Set([
33
+ 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'id.au',
34
+ 'co.uk', 'org.uk', 'me.uk', 'gov.uk', 'ac.uk', 'net.uk',
35
+ 'co.nz', 'net.nz', 'org.nz', 'govt.nz',
36
+ 'co.za', 'org.za', 'web.za',
37
+ 'com.br', 'net.br', 'org.br', 'gov.br',
38
+ 'co.in', 'net.in', 'org.in', 'gov.in',
39
+ 'co.jp', 'or.jp', 'ne.jp', 'go.jp', 'ac.jp',
40
+ 'co.kr', 'or.kr', 'ne.kr', 'go.kr',
41
+ 'com.mx', 'org.mx', 'gob.mx',
42
+ 'co.id', 'or.id', 'ac.id', 'go.id',
43
+ 'com.sg', 'org.sg', 'edu.sg', 'gov.sg',
44
+ 'com.hk', 'org.hk', 'edu.hk', 'gov.hk',
45
+ 'com.tw', 'org.tw', 'gov.tw',
46
+ ]);
47
+ /** Split hostname into (parent, subdomain) using a small Public Suffix List.
48
+ * parts.slice(-2).join('.') is wrong for `paidsooner.com.au` (treats `com.au`
49
+ * as the parent — that bug shipped phantom state to prod). */
50
+ export function splitRegistrable(hostname) {
51
+ const parts = hostname.toLowerCase().split('.');
52
+ // Try a 2-label public suffix first (e.g. com.au, co.uk).
53
+ if (parts.length >= 3) {
54
+ const twoLabel = parts.slice(-2).join('.');
55
+ if (TWO_LABEL_SUFFIXES.has(twoLabel)) {
56
+ const domain = parts.slice(-3).join('.');
57
+ const subdomain = parts.slice(0, -3).join('.') || '@';
58
+ return { domain, subdomain };
59
+ }
60
+ }
30
61
  const domain = parts.slice(-2).join('.');
31
62
  const subdomain = parts.slice(0, -2).join('.') || '@';
63
+ return { domain, subdomain };
64
+ }
65
+ export async function domainSubAdd(ctx, hostname, slug) {
66
+ const { domain, subdomain } = splitRegistrable(hostname);
32
67
  const base = resolveBase(ctx.base);
33
68
  const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/subdomains`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify({ subdomain, slug }) });
34
69
  if (r.autoConnected) {
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';
@@ -18,6 +21,24 @@ import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js'
18
21
  import { installSkill } from './skill.js';
19
22
  import { readState, writeState } from './state.js';
20
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();
21
42
  const HELP = `shiply — instant static hosting for agents (https://shiply.now)
22
43
 
23
44
  Usage:
@@ -25,6 +46,7 @@ Usage:
25
46
  Re-running UPDATES the same site (state in .shiply.json)
26
47
  shiply update <dir> Same as publish when .shiply.json exists
27
48
  shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
49
+ (no arg → account/plan status, see below)
28
50
  shiply duplicate <slug> Server-side copy of an owned site → new URL
29
51
  shiply detect [dir] Diagnose: print framework + dir that would upload
30
52
  (no upload — use --framework=<name> to test an override)
@@ -73,6 +95,8 @@ Usage:
73
95
  Delete every record in a collection
74
96
  shiply claim verify <code> Confirm a pairing code from /claim/<slug>?pair=1
75
97
  (uses .shiply.json from CWD)
98
+ shiply status [--json] With no arg: print account plan + capabilities +
99
+ upgrade URL (pass --json for raw API output)
76
100
  shiply skill [--project] [--force] Install the shiply skill for your AI agent
77
101
  (global ~/.claude/skills, or ./.claude/skills with --project)
78
102
  (--force overwrites an existing skill — recommended weekly)
@@ -131,7 +155,19 @@ async function reportReadiness(host, celebrate) {
131
155
  return r.ready;
132
156
  }
133
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
+ }
134
169
  const { values, positionals } = parseArgs({
170
+ args: rawArgv,
135
171
  allowPositionals: true,
136
172
  options: {
137
173
  spa: { type: 'boolean' },
@@ -161,9 +197,14 @@ async function main() {
161
197
  cursor: { type: 'string' },
162
198
  where: { type: 'string' },
163
199
  help: { type: 'boolean', short: 'h' },
200
+ version: { type: 'boolean', short: 'v' },
164
201
  },
165
202
  });
166
203
  const [cmd, dir] = positionals;
204
+ if (values.version || cmd === 'version' || cmd === '--version') {
205
+ console.log(VERSION);
206
+ return;
207
+ }
167
208
  if (values.help || !cmd || cmd === 'help') {
168
209
  process.stdout.write(HELP);
169
210
  return;
@@ -248,8 +289,15 @@ async function main() {
248
289
  return;
249
290
  }
250
291
  case 'status': {
251
- if (!dir)
252
- 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
+ }
253
301
  const host = targetToHostname(dir);
254
302
  const celebrate = !values['no-confetti'];
255
303
  console.log(`checking https://${host}/ …`);
@@ -0,0 +1,117 @@
1
+ import { loadApiKey } from './config.js';
2
+ import { api, resolveBase } from './publish.js';
3
+ // Minimal ANSI helpers — match the rest of the CLI's "spare colors, never
4
+ // rainbow" tone (see drive/db output).
5
+ const isTty = process.stdout.isTTY;
6
+ const ansi = {
7
+ green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
8
+ red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
9
+ dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
10
+ bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
11
+ };
12
+ function fmtLimit(n) {
13
+ if (n === null || n === undefined)
14
+ return 'unlimited';
15
+ return String(n);
16
+ }
17
+ /** Turn `databases_neon_postgres` → `Neon Postgres`-ish: replace _ with space,
18
+ * capitalise words, leave acronyms (D1, MCP, URL) upper. Keep it simple. */
19
+ function humanise(key) {
20
+ const acronyms = new Set(['d1', 'mcp', 'url', 'api']);
21
+ return key
22
+ .split('_')
23
+ .map((w) => (acronyms.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
24
+ .join(' ');
25
+ }
26
+ export async function runStatus(opts) {
27
+ const apiKey = opts.key ?? (await loadApiKey());
28
+ if (!apiKey) {
29
+ console.error('✖ shiply status needs an API key — run `shiply login` first');
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const base = resolveBase(opts.base);
34
+ let raw;
35
+ try {
36
+ raw = await api(`${base}/api/v1/status`, {
37
+ headers: { authorization: `Bearer ${apiKey}` },
38
+ });
39
+ }
40
+ catch (e) {
41
+ // Surface 404 cleanly while the sibling /status endpoint rolls out — old
42
+ // server versions will respond with "HTTP 404" and the user just sees a
43
+ // friendly hint instead of a stack-style error.
44
+ const msg = e instanceof Error ? e.message : String(e);
45
+ if (/404|not.found/i.test(msg)) {
46
+ console.error('✖ this shiply server does not expose /api/v1/status yet');
47
+ console.error(' upgrade your CLI or check https://shiply.now');
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ throw e;
52
+ }
53
+ if (opts.json) {
54
+ process.stdout.write(`${JSON.stringify(raw, null, 2)}\n`);
55
+ return;
56
+ }
57
+ console.log(ansi.bold('shiply status'));
58
+ console.log('');
59
+ // Plan + identity
60
+ if (raw.plan) {
61
+ const sub = raw.plan.subscription_status ? ` (${raw.plan.subscription_status})` : '';
62
+ console.log(`Plan: ${raw.plan.name}${sub}`);
63
+ }
64
+ if (raw.user?.email)
65
+ console.log(`Email: ${raw.user.email}`);
66
+ if (raw.user)
67
+ console.log(`Handle: ${raw.user.handle ?? '(none)'}`);
68
+ console.log('');
69
+ // Capabilities — iterate in server order; tolerate unknown keys.
70
+ if (raw.capabilities) {
71
+ console.log(ansi.bold('Capabilities:'));
72
+ const entries = Object.entries(raw.capabilities);
73
+ const labels = entries.map(([k]) => humanise(k));
74
+ const labelWidth = Math.max(...labels.map((l) => l.length));
75
+ for (let i = 0; i < entries.length; i++) {
76
+ const [, cap] = entries[i];
77
+ const mark = cap.available ? ansi.green('✓') : ansi.red('✗');
78
+ const label = labels[i].padEnd(labelWidth, ' ');
79
+ const hint = [];
80
+ if (cap.plan_required)
81
+ hint.push(`${cap.plan_required}+`);
82
+ if (cap.limit !== undefined && cap.limit !== null)
83
+ hint.push(`limit: ${cap.limit}`);
84
+ if (cap.note)
85
+ hint.push(cap.note);
86
+ const tail = hint.length > 0 ? ansi.dim(` — ${hint.join(' · ')}`) : '';
87
+ console.log(` ${mark} ${label}${tail}`);
88
+ }
89
+ console.log('');
90
+ }
91
+ // Limits
92
+ if (raw.limits) {
93
+ console.log(ansi.bold('Limits:'));
94
+ for (const [k, v] of Object.entries(raw.limits)) {
95
+ console.log(` ${humanise(k)}: ${fmtLimit(v)}`);
96
+ }
97
+ console.log('');
98
+ }
99
+ // Blocked-features summary + upgrade hint
100
+ const blocked = raw.blocked_features ?? [];
101
+ if (blocked.length > 0) {
102
+ console.log(`${blocked.length} feature${blocked.length === 1 ? '' : 's'} need${blocked.length === 1 ? 's' : ''} an upgrade:`);
103
+ const labels = blocked.map(humanise);
104
+ const labelWidth = Math.max(...labels.map((l) => l.length));
105
+ for (let i = 0; i < blocked.length; i++) {
106
+ const key = blocked[i];
107
+ const req = raw.capabilities?.[key]?.plan_required ?? 'higher plan';
108
+ console.log(` ${ansi.red('✗')} ${labels[i].padEnd(labelWidth, ' ')} ${ansi.dim(`— requires ${req}`)}`);
109
+ }
110
+ console.log('');
111
+ if (raw.plan?.upgrade_url)
112
+ console.log(`Upgrade at: ${raw.plan.upgrade_url}`);
113
+ }
114
+ else {
115
+ console.log('Nothing blocked. You have full access.');
116
+ }
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -263,7 +263,7 @@ rest and surfaced to the site's Worker as `env.DATABASE_URL`.
263
263
 
264
264
  ### D1 (default)
265
265
  ```bash
266
- shiply db create app # provision a D1 (binding APP_DB)
266
+ shiply db create app # provision a D1 (binding SITE_DB)
267
267
  shiply db ls # list (name, provider, size, attached site)
268
268
  shiply db sql app "SELECT 1" # one-shot query against your account
269
269
  shiply db sql app "SELECT * FROM t WHERE id=?1" --params '[42]'
@@ -277,7 +277,7 @@ Run `shiply db create` inside a publish directory: it records the new
277
277
  it. Site code then calls the shim:
278
278
 
279
279
  ```js
280
- fetch('/_shiply/db/APP_DB/query', {
280
+ fetch('/_shiply/db/SITE_DB/query', {
281
281
  method: 'POST',
282
282
  headers: { 'content-type': 'application/json' },
283
283
  body: JSON.stringify({ sql: 'SELECT * FROM posts', params: [] }),