shiply-cli 0.5.0 → 0.7.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/drive.js +77 -0
- package/dist/index.js +53 -0
- package/dist/manifest.js +6 -1
- package/package.json +1 -1
- package/skill/SKILL.md +27 -0
package/dist/drive.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { api, resolveBase } from './publish.js';
|
|
5
|
+
const sha256 = (b) => createHash('sha256').update(b).digest('hex');
|
|
6
|
+
const headers = (apiKey) => ({ 'content-type': 'application/json', authorization: `Bearer ${apiKey}` });
|
|
7
|
+
async function defaultDrive(ctx) {
|
|
8
|
+
const base = resolveBase(ctx.base);
|
|
9
|
+
const d = await api(`${base}/api/v1/drives/default`, { headers: headers(ctx.apiKey) });
|
|
10
|
+
return d.publicId;
|
|
11
|
+
}
|
|
12
|
+
export async function driveLs(ctx, prefix = '') {
|
|
13
|
+
const base = resolveBase(ctx.base);
|
|
14
|
+
const id = await defaultDrive(ctx);
|
|
15
|
+
const out = await api(`${base}/api/v1/drives/${id}/files?prefix=${encodeURIComponent(prefix)}&limit=200`, { headers: headers(ctx.apiKey) });
|
|
16
|
+
if (out.files.length === 0) {
|
|
17
|
+
console.log(' (empty)');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const f of out.files)
|
|
21
|
+
console.log(` ${f.path} ${Math.ceil(f.size / 1024)}KB`);
|
|
22
|
+
}
|
|
23
|
+
export async function drivePut(ctx, file, asPath) {
|
|
24
|
+
const base = resolveBase(ctx.base);
|
|
25
|
+
const id = await defaultDrive(ctx);
|
|
26
|
+
const body = await readFile(file);
|
|
27
|
+
const path = asPath ?? basename(file);
|
|
28
|
+
const hash = sha256(body);
|
|
29
|
+
const meta = { path, size: body.length, contentType: 'application/octet-stream', hash };
|
|
30
|
+
const stage = await api(`${base}/api/v1/drives/${id}/files/uploads`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify({ files: [meta] }) });
|
|
31
|
+
for (const u of stage.uploads) {
|
|
32
|
+
const res = await fetch(u.url, { method: 'PUT', body: new Uint8Array(body) });
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error(`upload failed for ${u.path}`);
|
|
35
|
+
}
|
|
36
|
+
await api(`${base}/api/v1/drives/${id}/files/finalize`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: headers(ctx.apiKey),
|
|
39
|
+
body: JSON.stringify({ files: [meta] }),
|
|
40
|
+
});
|
|
41
|
+
console.log(`✔ put ${path}`);
|
|
42
|
+
}
|
|
43
|
+
export async function driveGet(ctx, path, out) {
|
|
44
|
+
const base = resolveBase(ctx.base);
|
|
45
|
+
const id = await defaultDrive(ctx);
|
|
46
|
+
const { url } = await api(`${base}/api/v1/drives/${id}/files/${path.split('/').map(encodeURIComponent).join('/')}`, { headers: headers(ctx.apiKey) });
|
|
47
|
+
const res = await fetch(url);
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`download failed (${res.status})`);
|
|
50
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
51
|
+
if (out) {
|
|
52
|
+
await writeFile(out, buf);
|
|
53
|
+
console.log(`✔ saved ${out}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
process.stdout.write(buf);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function driveRm(ctx, path) {
|
|
60
|
+
const base = resolveBase(ctx.base);
|
|
61
|
+
const id = await defaultDrive(ctx);
|
|
62
|
+
await api(`${base}/api/v1/drives/${id}/files/${path.split('/').map(encodeURIComponent).join('/')}`, {
|
|
63
|
+
method: 'DELETE',
|
|
64
|
+
headers: headers(ctx.apiKey),
|
|
65
|
+
});
|
|
66
|
+
console.log(`✔ removed ${path}`);
|
|
67
|
+
}
|
|
68
|
+
export async function drivePublish(ctx, prefix) {
|
|
69
|
+
const base = resolveBase(ctx.base);
|
|
70
|
+
const id = await defaultDrive(ctx);
|
|
71
|
+
const out = await api(`${base}/api/v1/publish/from-drive`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: headers(ctx.apiKey),
|
|
74
|
+
body: JSON.stringify({ driveId: id, ...(prefix ? { prefix } : {}) }),
|
|
75
|
+
});
|
|
76
|
+
console.log(`✔ published drive (${out.filesCount} files)\n\n ${out.siteUrl}\n`);
|
|
77
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { confetti } from './confetti.js';
|
|
6
6
|
import { detectFramework, findBuildOutput } from './framework.js';
|
|
7
|
+
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
7
8
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
8
9
|
import { api, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
9
10
|
import { installSkill } from './skill.js';
|
|
@@ -16,6 +17,8 @@ Usage:
|
|
|
16
17
|
Re-running UPDATES the same site (state in .shiply.json)
|
|
17
18
|
shiply update <dir> Same as publish when .shiply.json exists
|
|
18
19
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
20
|
+
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
21
|
+
shiply drive <ls|put|get|rm|publish> Private cloud storage (drive publish → live site)
|
|
19
22
|
shiply skill [--project] Install the shiply skill for your AI agent
|
|
20
23
|
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
21
24
|
shiply login [--email <address>] Email a 6-digit code, mint + save an API key
|
|
@@ -192,6 +195,56 @@ async function main() {
|
|
|
192
195
|
await new Promise((r) => setTimeout(r, 5000));
|
|
193
196
|
}
|
|
194
197
|
}
|
|
198
|
+
case 'duplicate': {
|
|
199
|
+
if (!dir)
|
|
200
|
+
throw new Error('usage: shiply duplicate <slug>');
|
|
201
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
202
|
+
if (!apiKey)
|
|
203
|
+
throw new Error('duplicate needs an API key — run `shiply login` first');
|
|
204
|
+
const base = resolveBase(values.base);
|
|
205
|
+
const out = await api(`${base}/api/v1/publishes/${dir}/duplicate`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
208
|
+
body: JSON.stringify({}),
|
|
209
|
+
});
|
|
210
|
+
console.log(`✔ duplicated ${dir} → ${out.slug} (${out.filesCount} files)`);
|
|
211
|
+
console.log(`\n ${out.siteUrl}\n`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case 'drive': {
|
|
215
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
216
|
+
if (!apiKey)
|
|
217
|
+
throw new Error('drive commands need an API key — run `shiply login` first');
|
|
218
|
+
const dctx = { base: values.base, apiKey };
|
|
219
|
+
const sub = dir; // positionals[1]
|
|
220
|
+
const arg = positionals[2];
|
|
221
|
+
const arg2 = positionals[3];
|
|
222
|
+
switch (sub) {
|
|
223
|
+
case 'ls':
|
|
224
|
+
await driveLs(dctx, arg);
|
|
225
|
+
return;
|
|
226
|
+
case 'put':
|
|
227
|
+
if (!arg)
|
|
228
|
+
throw new Error('usage: shiply drive put <file> [as-path]');
|
|
229
|
+
await drivePut(dctx, arg, arg2);
|
|
230
|
+
return;
|
|
231
|
+
case 'get':
|
|
232
|
+
if (!arg)
|
|
233
|
+
throw new Error('usage: shiply drive get <path> [-o out]');
|
|
234
|
+
await driveGet(dctx, arg, values.timeout ? undefined : arg2);
|
|
235
|
+
return;
|
|
236
|
+
case 'rm':
|
|
237
|
+
if (!arg)
|
|
238
|
+
throw new Error('usage: shiply drive rm <path>');
|
|
239
|
+
await driveRm(dctx, arg);
|
|
240
|
+
return;
|
|
241
|
+
case 'publish':
|
|
242
|
+
await drivePublish(dctx, arg);
|
|
243
|
+
return;
|
|
244
|
+
default:
|
|
245
|
+
throw new Error('usage: shiply drive <ls|put|get|rm|publish>');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
195
248
|
case 'skill': {
|
|
196
249
|
const written = await installSkill(Boolean(values.project));
|
|
197
250
|
for (const w of written)
|
package/dist/manifest.js
CHANGED
|
@@ -36,7 +36,8 @@ export const contentTypeFor = (path) => MIME[extname(path).toLowerCase()] ?? 'ap
|
|
|
36
36
|
const SKIP_DIRS = new Set(['node_modules']);
|
|
37
37
|
/** Walk a directory into the publish manifest: posix-relative paths, byte
|
|
38
38
|
* sizes, sha256 hex hashes (server hash-skips unchanged files on update).
|
|
39
|
-
* Dot entries and node_modules are never published.
|
|
39
|
+
* Dot entries and node_modules are never published — except the .shiply/
|
|
40
|
+
* config directory (proxy.json, data.json), which the server consumes. */
|
|
40
41
|
export async function buildManifest(dir) {
|
|
41
42
|
const out = [];
|
|
42
43
|
await walk(dir, '', out);
|
|
@@ -45,6 +46,10 @@ export async function buildManifest(dir) {
|
|
|
45
46
|
async function walk(abs, rel, out) {
|
|
46
47
|
const entries = await readdir(abs, { withFileTypes: true });
|
|
47
48
|
for (const e of entries) {
|
|
49
|
+
if (e.name === '.shiply' && e.isDirectory() && rel === '') {
|
|
50
|
+
await walk(join(abs, e.name), '.shiply', out);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
48
53
|
if (e.name.startsWith('.') || SKIP_DIRS.has(e.name))
|
|
49
54
|
continue;
|
|
50
55
|
const childAbs = join(abs, e.name);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -69,6 +69,33 @@ cheap: unchanged files are skipped server-side.
|
|
|
69
69
|
finalized version live instantly.
|
|
70
70
|
- **SPA**: pass `"spaMode":true` so deep links serve index.html.
|
|
71
71
|
|
|
72
|
+
## Proxy routes — AI/API calls WITHOUT exposing keys
|
|
73
|
+
NEVER embed API keys in published HTML/JS. Instead: store the key as a
|
|
74
|
+
Variable (PUT /api/v1/variables), then include `.shiply/proxy.json` in the
|
|
75
|
+
published files declaring routes, e.g. {"proxies":{"/api/chat":{"upstream":
|
|
76
|
+
"https://openrouter.ai/api/v1/chat/completions","method":"POST","headers":
|
|
77
|
+
{"Authorization":"Bearer ${OPENROUTER_API_KEY}"}}}}. The page then calls the
|
|
78
|
+
relative path (fetch('/api/chat')) and shiply injects the secret server-side.
|
|
79
|
+
Prefix routes "/api/db/*" forward subpaths (great for Supabase REST).
|
|
80
|
+
Owned sites only; https public upstreams only. Docs: /docs/proxy-routes
|
|
81
|
+
|
|
82
|
+
## Site Data — forms/waitlists/guestbooks with ZERO backend
|
|
83
|
+
When the user wants to collect anything from visitors (waitlist, contact
|
|
84
|
+
form, RSVP, guestbook): include `.shiply/data.json` in the published files,
|
|
85
|
+
e.g. {"collections":{"signups":{"fields":{"email":{"type":"email",
|
|
86
|
+
"required":true}},"access":{"read":"owner","insert":"public"}}}}, then have
|
|
87
|
+
the page POST JSON to the relative path /.shiply/data/signups. Records are
|
|
88
|
+
validated server-side; the owner reads them in the dashboard (Data section,
|
|
89
|
+
CSV export) or GET /api/v1/publishes/<slug>/data/<collection> with a Bearer
|
|
90
|
+
key. Owned sites only. Docs: /docs/site-data
|
|
91
|
+
|
|
92
|
+
## Make a site private (paid plans)
|
|
93
|
+
To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
|
|
94
|
+
with {"mode":"password","password":"..."} or {"mode":"restricted",
|
|
95
|
+
"allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
|
|
96
|
+
MCP tool set_site_access does the same. Enforced before any content is served;
|
|
97
|
+
changing settings signs current visitors out. Docs: /docs/access-control
|
|
98
|
+
|
|
72
99
|
## Full React / Vue / Svelte / Astro apps — YES, they work
|
|
73
100
|
shiply hosts complete framework apps. Publish the BUILD OUTPUT, never the
|
|
74
101
|
source: run the build, then publish the output dir with SPA mode for
|