shiply-cli 0.4.1 → 0.6.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.
@@ -0,0 +1,52 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const exists = async (p) => stat(p).then((s) => s.isFile() || s.isDirectory(), () => false);
4
+ /** Detect a framework *source* directory (package.json, no root index.html)
5
+ * so `shiply publish .` on a React/Vue/etc project can do the right thing:
6
+ * publish the build output with SPA mode instead of raw source files. */
7
+ export async function detectFramework(dir) {
8
+ if (await exists(join(dir, 'index.html'))) {
9
+ // Vite keeps index.html at the root of the *source* too — only treat it as
10
+ // a source dir when a vite config is present alongside it.
11
+ const viteConfig = (await Promise.all(['vite.config.ts', 'vite.config.js', 'vite.config.mjs'].map((f) => exists(join(dir, f))))).some(Boolean);
12
+ if (!viteConfig)
13
+ return null;
14
+ }
15
+ let pkgRaw;
16
+ try {
17
+ pkgRaw = await readFile(join(dir, 'package.json'), 'utf8');
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ let deps;
23
+ try {
24
+ const pkg = JSON.parse(pkgRaw);
25
+ deps = { ...pkg.dependencies, ...pkg.devDependencies };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ const has = (name) => name in deps;
31
+ if (has('next')) {
32
+ return { framework: 'Next.js', outputDir: 'out', spa: false, buildCommand: 'npm run build (needs `output: "export"` in next.config)' };
33
+ }
34
+ if (has('astro'))
35
+ return { framework: 'Astro', outputDir: 'dist', spa: false, buildCommand: 'npm run build' };
36
+ if (has('@sveltejs/kit')) {
37
+ return { framework: 'SvelteKit', outputDir: 'build', spa: true, buildCommand: 'npm run build (use adapter-static)' };
38
+ }
39
+ if (has('vite'))
40
+ return { framework: 'Vite', outputDir: 'dist', spa: true, buildCommand: 'npm run build' };
41
+ if (has('react-scripts'))
42
+ return { framework: 'Create React App', outputDir: 'build', spa: true, buildCommand: 'npm run build' };
43
+ if (has('react') || has('vue') || has('svelte')) {
44
+ return { framework: 'JavaScript app', outputDir: 'dist', spa: true, buildCommand: 'npm run build' };
45
+ }
46
+ return null;
47
+ }
48
+ /** If the detected output dir exists and has an index.html, return it. */
49
+ export async function findBuildOutput(dir, hint) {
50
+ const out = join(dir, hint.outputDir);
51
+ return (await exists(join(out, 'index.html'))) ? out : null;
52
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline/promises';
3
+ import { join } from 'node:path';
3
4
  import { parseArgs } from 'node:util';
4
5
  import { confetti } from './confetti.js';
6
+ import { detectFramework, findBuildOutput } from './framework.js';
5
7
  import { loadApiKey, saveApiKey } from './config.js';
6
8
  import { api, DEFAULT_BASE, publish, resolveBase } from './publish.js';
7
9
  import { installSkill } from './skill.js';
@@ -14,6 +16,7 @@ Usage:
14
16
  Re-running UPDATES the same site (state in .shiply.json)
15
17
  shiply update <dir> Same as publish when .shiply.json exists
16
18
  shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
19
+ shiply duplicate <slug> Server-side copy of an owned site → new URL
17
20
  shiply skill [--project] Install the shiply skill for your AI agent
18
21
  (global ~/.claude/skills, or ./.claude/skills with --project)
19
22
  shiply login [--email <address>] Email a 6-digit code, mint + save an API key
@@ -89,8 +92,29 @@ async function main() {
89
92
  if (!dir)
90
93
  throw new Error(`usage: shiply ${cmd} <dir>`);
91
94
  const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
95
+ // Full React/Vue/etc apps work great — but you publish the BUILD OUTPUT,
96
+ // not the source. Detect source dirs and do the right thing.
97
+ let publishDir = dir;
98
+ let spa = values.spa;
99
+ const hint = await detectFramework(dir);
100
+ if (hint) {
101
+ const out = await findBuildOutput(dir, hint);
102
+ if (out) {
103
+ publishDir = out;
104
+ if (hint.spa && spa === undefined)
105
+ spa = true;
106
+ console.log(`✔ ${hint.framework} project detected — publishing ${out}${hint.spa ? ' with SPA mode' : ''}`);
107
+ }
108
+ else {
109
+ throw new Error(`this looks like a ${hint.framework} project (source code, not a built site).\n` +
110
+ ` Build it first, then publish the output:\n` +
111
+ ` ${hint.buildCommand}\n` +
112
+ ` shiply ${cmd} ${join(dir, hint.outputDir)}${hint.spa ? ' --spa' : ''}\n` +
113
+ ` (or pass the built folder directly if it lives elsewhere)`);
114
+ }
115
+ }
92
116
  // same command, same URL: reuse this directory's site automatically
93
- const state = await readState(dir);
117
+ const state = await readState(publishDir);
94
118
  let claimToken = values['claim-token'] ?? state?.claimToken;
95
119
  let slug = state?.owned && apiKey ? state.slug : undefined;
96
120
  if (values['new-site']) {
@@ -101,14 +125,14 @@ async function main() {
101
125
  throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
102
126
  }
103
127
  const updating = Boolean(claimToken || slug);
104
- const res = await publish(dir, {
128
+ const res = await publish(publishDir, {
105
129
  apiKey,
106
130
  base: values.base,
107
- spaMode: values.spa,
131
+ spaMode: spa,
108
132
  claimToken,
109
133
  slug,
110
134
  });
111
- await writeState(dir, {
135
+ await writeState(publishDir, {
112
136
  slug: res.slug,
113
137
  siteUrl: res.siteUrl,
114
138
  ...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
@@ -169,6 +193,22 @@ async function main() {
169
193
  await new Promise((r) => setTimeout(r, 5000));
170
194
  }
171
195
  }
196
+ case 'duplicate': {
197
+ if (!dir)
198
+ throw new Error('usage: shiply duplicate <slug>');
199
+ const apiKey = values.key ?? (await loadApiKey());
200
+ if (!apiKey)
201
+ throw new Error('duplicate needs an API key — run `shiply login` first');
202
+ const base = resolveBase(values.base);
203
+ const out = await api(`${base}/api/v1/publishes/${dir}/duplicate`, {
204
+ method: 'POST',
205
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
206
+ body: JSON.stringify({}),
207
+ });
208
+ console.log(`✔ duplicated ${dir} → ${out.slug} (${out.filesCount} files)`);
209
+ console.log(`\n ${out.siteUrl}\n`);
210
+ return;
211
+ }
172
212
  case 'skill': {
173
213
  const written = await installSkill(Boolean(values.project));
174
214
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
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
@@ -1,75 +1,115 @@
1
- ---
2
- name: shiply
3
- description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
4
- ---
5
-
6
- # shiply — instant web hosting for agents
7
-
8
- shiply.now puts files on the web in seconds. No account needed to start:
9
- anonymous sites are live immediately, last 24 hours, and can be claimed into
10
- an account to keep them. The cardinal rule:
11
-
12
- **NEVER create a new site to update an existing one. Always re-publish to the
13
- same site** — otherwise you litter subdomains and lose the user's URL.
14
-
15
- ## Pick your interface (best first)
16
-
17
- ### 1. MCP (native tools)
18
- If the `shiply` MCP server is connected (https://shiply.now/mcp), use
19
- `publish_site`. Every result includes a `toUpdate` field telling you the exact
20
- call for updates — follow it. Other tools: `site_status`, `list_sites`,
21
- `set_handle`, `set_variable`, `add_domain`, `check_domain`, `get_analytics`,
22
- `delete_site`, `rollback` via dashboard.
23
-
24
- ### 2. CLI
25
- ```bash
26
- npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
27
- shiply publish ./dir # live URL + confetti
28
- shiply publish ./dir # run AGAIN after edits → updates the SAME site
29
- shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
30
- shiply login # email code → API key → sites become permanent
31
- ```
32
- The CLI stores each directory's site in `.shiply.json` (slug + update token),
33
- so repeat publishes reuse the URL automatically. `--new-site` opts out.
34
- Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
35
- lines for automation; exit code 0 = ready.
36
-
37
- ### 3. Raw HTTP (no installs)
38
- ```
39
- 1. POST https://shiply.now/api/v1/publish
40
- {"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
41
- (+ "Authorization: Bearer shp_…" for permanent owned sites)
42
- 2. PUT each file's bytes to response upload.uploads[].url
43
- 3. POST upload.finalizeUrl with {"versionId":"..."}
44
- ```
45
- **Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
46
- first publish — save it); owned → include `"slug":"..."`. Hashes make updates
47
- cheap: unchanged files are skipped server-side.
48
-
49
- ## The lifecycle to explain to users
50
- - Anonymous site: live instantly, expires in 24 h. Give the user the
51
- `claimUrl` — claiming keeps it forever on a free account.
52
- - API key (`shiply login` or POST /api/auth/agent/request-code →
53
- verify-code): publishes are permanent and manageable.
54
- - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
55
- storage, analytics: https://shiply.now/dashboard/plan
56
-
57
- ## Power features (Bearer key)
58
- - **Custom domains**: POST /api/v1/domains {"hostname","slug"} → tell the
59
- user to CNAME the hostname to `cname.shiply.now` → cert auto-issues; poll
60
- GET /api/v1/domains/{id}/check until `ready:true`.
61
- - **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
62
- check endpoint — confirms certificate + serving before telling the user
63
- it's done.
64
- - **Variables**: encrypted per-user KV for API keys the user's sites need
65
- (GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
66
- be connected from the dashboard and lands here as SUPABASE_URL +
67
- SUPABASE_ANON_KEY.
68
- - **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
69
- finalized version live instantly.
70
- - **SPA**: pass `"spaMode":true` so deep links serve index.html.
71
-
72
- ## Limits & references
73
- ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
74
- Machine guide: https://shiply.now/llms.txt · OpenAPI:
75
- https://shiply.now/openapi.json · Docs: https://shiply.now/docs
1
+ ---
2
+ name: shiply
3
+ description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
4
+ ---
5
+
6
+ # shiply — instant web hosting for agents
7
+
8
+ shiply.now puts files on the web in seconds. No account needed to start:
9
+ anonymous sites are live immediately, last 24 hours, and can be claimed into
10
+ an account to keep them. The cardinal rule:
11
+
12
+ **NEVER create a new site to update an existing one. Always re-publish to the
13
+ same site** — otherwise you litter subdomains and lose the user's URL.
14
+
15
+ ## Pick your interface (best first)
16
+
17
+ ### 1. MCP (native tools)
18
+ If the `shiply` MCP server is connected (https://shiply.now/mcp), use
19
+ `publish_site`. Every result includes a `toUpdate` field telling you the exact
20
+ call for updates — follow it. Other tools: `site_status`, `list_sites`,
21
+ `set_handle`, `set_variable`, `add_domain`, `check_domain`, `get_analytics`,
22
+ `delete_site`, `rollback` via dashboard.
23
+
24
+ ### 2. CLI
25
+ ```bash
26
+ npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
27
+ shiply publish ./dir # live URL + confetti
28
+ shiply publish ./dir # run AGAIN after edits → updates the SAME site
29
+ shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
30
+ shiply login # email code → API key → sites become permanent
31
+ ```
32
+ The CLI stores each directory's site in `.shiply.json` (slug + update token),
33
+ so repeat publishes reuse the URL automatically. `--new-site` opts out.
34
+ Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
35
+ lines for automation; exit code 0 = ready.
36
+
37
+ ### 3. Raw HTTP (no installs)
38
+ ```
39
+ 1. POST https://shiply.now/api/v1/publish
40
+ {"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
41
+ (+ "Authorization: Bearer shp_…" for permanent owned sites)
42
+ 2. PUT each file's bytes to response upload.uploads[].url
43
+ 3. POST upload.finalizeUrl with {"versionId":"..."}
44
+ ```
45
+ **Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
46
+ first publish — save it); owned → include `"slug":"..."`. Hashes make updates
47
+ cheap: unchanged files are skipped server-side.
48
+
49
+ ## The lifecycle to explain to users
50
+ - Anonymous site: live instantly, expires in 24 h. Give the user the
51
+ `claimUrl` — claiming keeps it forever on a free account.
52
+ - API key (`shiply login` or POST /api/auth/agent/request-code →
53
+ verify-code): publishes are permanent and manageable.
54
+ - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
55
+ storage, analytics: https://shiply.now/dashboard/plan
56
+
57
+ ## Power features (Bearer key)
58
+ - **Custom domains**: POST /api/v1/domains {"hostname","slug"} → tell the
59
+ user to CNAME the hostname to `cname.shiply.now` → cert auto-issues; poll
60
+ GET /api/v1/domains/{id}/check until `ready:true`.
61
+ - **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
62
+ check endpoint — confirms certificate + serving before telling the user
63
+ it's done.
64
+ - **Variables**: encrypted per-user KV for API keys the user's sites need
65
+ (GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
66
+ be connected from the dashboard and lands here as SUPABASE_URL +
67
+ SUPABASE_ANON_KEY.
68
+ - **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
69
+ finalized version live instantly.
70
+ - **SPA**: pass `"spaMode":true` so deep links serve index.html.
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
+
99
+ ## Full React / Vue / Svelte / Astro apps — YES, they work
100
+ shiply hosts complete framework apps. Publish the BUILD OUTPUT, never the
101
+ source: run the build, then publish the output dir with SPA mode for
102
+ client-routed apps. The CLI auto-detects source dirs and tells you the
103
+ right command.
104
+ - Vite/React/Vue/Svelte: `npm run build` then `shiply publish dist --spa`
105
+ - Create React App: `npm run build` then `shiply publish build --spa`
106
+ - Next.js: set `output: "export"` in next.config, `npm run build`, then
107
+ `shiply publish out` (no --spa; export emits real HTML per route)
108
+ - Astro/static generators: `shiply publish dist`
109
+ Use the CLI (not inline MCP files) for builds — hashed bundles often exceed
110
+ the 50-file inline cap, and the CLI hash-skips unchanged chunks on updates.
111
+
112
+ ## Limits & references
113
+ ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
114
+ Machine guide: https://shiply.now/llms.txt · OpenAPI:
115
+ https://shiply.now/openapi.json · Docs: https://shiply.now/docs