shiply-cli 0.4.1 → 0.5.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/framework.js +52 -0
- package/dist/index.js +27 -4
- package/package.json +1 -1
- package/skill/SKILL.md +88 -75
|
@@ -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';
|
|
@@ -89,8 +91,29 @@ async function main() {
|
|
|
89
91
|
if (!dir)
|
|
90
92
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
91
93
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
94
|
+
// Full React/Vue/etc apps work great — but you publish the BUILD OUTPUT,
|
|
95
|
+
// not the source. Detect source dirs and do the right thing.
|
|
96
|
+
let publishDir = dir;
|
|
97
|
+
let spa = values.spa;
|
|
98
|
+
const hint = await detectFramework(dir);
|
|
99
|
+
if (hint) {
|
|
100
|
+
const out = await findBuildOutput(dir, hint);
|
|
101
|
+
if (out) {
|
|
102
|
+
publishDir = out;
|
|
103
|
+
if (hint.spa && spa === undefined)
|
|
104
|
+
spa = true;
|
|
105
|
+
console.log(`✔ ${hint.framework} project detected — publishing ${out}${hint.spa ? ' with SPA mode' : ''}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new Error(`this looks like a ${hint.framework} project (source code, not a built site).\n` +
|
|
109
|
+
` Build it first, then publish the output:\n` +
|
|
110
|
+
` ${hint.buildCommand}\n` +
|
|
111
|
+
` shiply ${cmd} ${join(dir, hint.outputDir)}${hint.spa ? ' --spa' : ''}\n` +
|
|
112
|
+
` (or pass the built folder directly if it lives elsewhere)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
92
115
|
// same command, same URL: reuse this directory's site automatically
|
|
93
|
-
const state = await readState(
|
|
116
|
+
const state = await readState(publishDir);
|
|
94
117
|
let claimToken = values['claim-token'] ?? state?.claimToken;
|
|
95
118
|
let slug = state?.owned && apiKey ? state.slug : undefined;
|
|
96
119
|
if (values['new-site']) {
|
|
@@ -101,14 +124,14 @@ async function main() {
|
|
|
101
124
|
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
102
125
|
}
|
|
103
126
|
const updating = Boolean(claimToken || slug);
|
|
104
|
-
const res = await publish(
|
|
127
|
+
const res = await publish(publishDir, {
|
|
105
128
|
apiKey,
|
|
106
129
|
base: values.base,
|
|
107
|
-
spaMode:
|
|
130
|
+
spaMode: spa,
|
|
108
131
|
claimToken,
|
|
109
132
|
slug,
|
|
110
133
|
});
|
|
111
|
-
await writeState(
|
|
134
|
+
await writeState(publishDir, {
|
|
112
135
|
slug: res.slug,
|
|
113
136
|
siteUrl: res.siteUrl,
|
|
114
137
|
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,75 +1,88 @@
|
|
|
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
|
-
##
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
## Full React / Vue / Svelte / Astro apps — YES, they work
|
|
73
|
+
shiply hosts complete framework apps. Publish the BUILD OUTPUT, never the
|
|
74
|
+
source: run the build, then publish the output dir with SPA mode for
|
|
75
|
+
client-routed apps. The CLI auto-detects source dirs and tells you the
|
|
76
|
+
right command.
|
|
77
|
+
- Vite/React/Vue/Svelte: `npm run build` then `shiply publish dist --spa`
|
|
78
|
+
- Create React App: `npm run build` then `shiply publish build --spa`
|
|
79
|
+
- Next.js: set `output: "export"` in next.config, `npm run build`, then
|
|
80
|
+
`shiply publish out` (no --spa; export emits real HTML per route)
|
|
81
|
+
- Astro/static generators: `shiply publish dist`
|
|
82
|
+
Use the CLI (not inline MCP files) for builds — hashed bundles often exceed
|
|
83
|
+
the 50-file inline cap, and the CLI hash-skips unchanged chunks on updates.
|
|
84
|
+
|
|
85
|
+
## Limits & references
|
|
86
|
+
≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
|
|
87
|
+
Machine guide: https://shiply.now/llms.txt · OpenAPI:
|
|
88
|
+
https://shiply.now/openapi.json · Docs: https://shiply.now/docs
|