shiply-cli 0.14.0 → 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/index.js +50 -2
- package/dist/status-cmd.js +117 -0
- package/package.json +1 -1
- package/skill/SKILL.md +2 -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) {
|
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
|
-
|
|
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
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
|
|
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/
|
|
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: [] }),
|