shiply-cli 0.2.0 → 0.4.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/README.md +5 -1
- package/dist/index.js +67 -35
- package/dist/publish.js +1 -0
- package/dist/skill.js +29 -0
- package/dist/state.js +16 -0
- package/package.json +2 -2
- package/skill/SKILL.md +75 -0
package/README.md
CHANGED
|
@@ -13,12 +13,16 @@ curl -fsSL https://shiply.now/install.sh | bash
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
shiply publish ./dist # publish a directory, print the live URL
|
|
16
|
+
shiply publish ./dist # run it AGAIN → updates the SAME site (no new subdomain)
|
|
16
17
|
shiply publish ./dist --spa # single-page app mode
|
|
17
18
|
shiply login # email a 6-digit code, mint + save an API key
|
|
18
|
-
shiply update ./dist --claim-token <token> # push a new version to an anonymous site
|
|
19
19
|
shiply status <slug-or-domain> [--wait] # SSL + readiness check — confetti when live 🎉
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
shiply remembers each directory's site in `.shiply.json` (slug + update token),
|
|
23
|
+
so repeat publishes always hit the same URL. Use `--new-site` to start fresh;
|
|
24
|
+
gitignore `.shiply.json` in public repos.
|
|
25
|
+
|
|
22
26
|
`shiply status` prints stable `SSL_READY` / `SITE_READY` markers for agents and
|
|
23
27
|
exits 0 only when the certificate is valid and the site serves. `--wait` polls
|
|
24
28
|
until ready (great while a custom domain's certificate issues).
|
package/dist/index.js
CHANGED
|
@@ -4,32 +4,38 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import { confetti } from './confetti.js';
|
|
5
5
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
6
6
|
import { api, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
7
|
+
import { installSkill } from './skill.js';
|
|
8
|
+
import { readState, writeState } from './state.js';
|
|
7
9
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
8
|
-
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
shiply publish <dir> [options] Publish a directory, print the live URL
|
|
12
|
-
|
|
13
|
-
shiply
|
|
14
|
-
shiply
|
|
15
|
-
shiply
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
10
|
+
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
shiply publish <dir> [options] Publish a directory, print the live URL.
|
|
14
|
+
Re-running UPDATES the same site (state in .shiply.json)
|
|
15
|
+
shiply update <dir> Same as publish when .shiply.json exists
|
|
16
|
+
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
17
|
+
shiply skill [--project] Install the shiply skill for your AI agent
|
|
18
|
+
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
19
|
+
shiply login [--email <address>] Email a 6-digit code, mint + save an API key
|
|
20
|
+
shiply help
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--spa Single-page app mode (unknown paths fall back to index.html)
|
|
24
|
+
--claim-token <tok> Update a specific anonymous site (overrides .shiply.json)
|
|
25
|
+
--new-site Ignore .shiply.json and create a fresh site
|
|
26
|
+
--key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
|
|
27
|
+
--anonymous Publish without an API key even if one is saved
|
|
28
|
+
--base <url> API origin (default: ${DEFAULT_BASE})
|
|
29
|
+
--wait (status) poll until the site is ready
|
|
30
|
+
--timeout <seconds> (status --wait) give up after this long (default 300)
|
|
31
|
+
--no-confetti Celebrate quietly
|
|
32
|
+
|
|
33
|
+
\`shiply status\` prints stable machine-readable markers for agents:
|
|
34
|
+
SSL_READY and SITE_READY on success (exit 0); ✖ lines and exit 1 otherwise.
|
|
35
|
+
|
|
36
|
+
With an API key the site is permanent and owned by your account. Without one
|
|
37
|
+
it expires in 24 hours — save the printed claimToken/claimUrl to update or
|
|
38
|
+
claim it later.
|
|
33
39
|
`;
|
|
34
40
|
async function reportReadiness(host, celebrate) {
|
|
35
41
|
const r = await checkReadiness(host);
|
|
@@ -65,6 +71,8 @@ async function main() {
|
|
|
65
71
|
base: { type: 'string' },
|
|
66
72
|
email: { type: 'string' },
|
|
67
73
|
wait: { type: 'boolean' },
|
|
74
|
+
'new-site': { type: 'boolean' },
|
|
75
|
+
project: { type: 'boolean' },
|
|
68
76
|
timeout: { type: 'string' },
|
|
69
77
|
'no-confetti': { type: 'boolean' },
|
|
70
78
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -80,18 +88,34 @@ async function main() {
|
|
|
80
88
|
case 'update': {
|
|
81
89
|
if (!dir)
|
|
82
90
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
83
|
-
if (cmd === 'update' && !values['claim-token']) {
|
|
84
|
-
throw new Error('shiply update needs --claim-token <token> (printed by the original publish)');
|
|
85
|
-
}
|
|
86
91
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
92
|
+
// same command, same URL: reuse this directory's site automatically
|
|
93
|
+
const state = await readState(dir);
|
|
94
|
+
let claimToken = values['claim-token'] ?? state?.claimToken;
|
|
95
|
+
let slug = state?.owned && apiKey ? state.slug : undefined;
|
|
96
|
+
if (values['new-site']) {
|
|
97
|
+
claimToken = values['claim-token'];
|
|
98
|
+
slug = undefined;
|
|
99
|
+
}
|
|
100
|
+
if (cmd === 'update' && !claimToken && !slug) {
|
|
101
|
+
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
102
|
+
}
|
|
103
|
+
const updating = Boolean(claimToken || slug);
|
|
87
104
|
const res = await publish(dir, {
|
|
88
105
|
apiKey,
|
|
89
106
|
base: values.base,
|
|
90
107
|
spaMode: values.spa,
|
|
91
|
-
claimToken
|
|
108
|
+
claimToken,
|
|
109
|
+
slug,
|
|
110
|
+
});
|
|
111
|
+
await writeState(dir, {
|
|
112
|
+
slug: res.slug,
|
|
113
|
+
siteUrl: res.siteUrl,
|
|
114
|
+
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
|
115
|
+
owned: !res.anonymous,
|
|
92
116
|
});
|
|
93
117
|
const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
|
|
94
|
-
console.log(`✔ published ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
118
|
+
console.log(`✔ ${updating ? `updated ${res.slug} in place` : 'published'} — ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
95
119
|
console.log(`\n ${res.siteUrl}\n`);
|
|
96
120
|
// confirm the site actually serves, then celebrate
|
|
97
121
|
try {
|
|
@@ -107,14 +131,14 @@ async function main() {
|
|
|
107
131
|
catch {
|
|
108
132
|
/* serving check is best-effort */
|
|
109
133
|
}
|
|
134
|
+
if (!updating) {
|
|
135
|
+
console.log(` saved .shiply.json — run \`shiply publish ${dir}\` again to UPDATE this same site`);
|
|
136
|
+
console.log(` (gitignore .shiply.json if this folder is public — it can update the site)`);
|
|
137
|
+
}
|
|
110
138
|
if (res.anonymous) {
|
|
111
139
|
console.log(` anonymous site — expires ${res.expiresAt ?? 'in 24h'}`);
|
|
112
140
|
if (res.claimUrl)
|
|
113
|
-
console.log(` claim it (
|
|
114
|
-
if (res.claimToken) {
|
|
115
|
-
console.log(` claimToken (SAVE THIS — shown once): ${res.claimToken}`);
|
|
116
|
-
console.log(` update later: shiply update <dir> --claim-token ${res.claimToken}`);
|
|
117
|
-
}
|
|
141
|
+
console.log(` claim it to KEEP it (free account): ${res.claimUrl}`);
|
|
118
142
|
console.log(` tip: run \`shiply login\` first to publish permanent sites`);
|
|
119
143
|
}
|
|
120
144
|
return;
|
|
@@ -145,6 +169,14 @@ async function main() {
|
|
|
145
169
|
await new Promise((r) => setTimeout(r, 5000));
|
|
146
170
|
}
|
|
147
171
|
}
|
|
172
|
+
case 'skill': {
|
|
173
|
+
const written = await installSkill(Boolean(values.project));
|
|
174
|
+
for (const w of written)
|
|
175
|
+
console.log(`✔ skill installed — ${w}`);
|
|
176
|
+
console.log(' Your agent now knows how to publish, update (same URL!), check SSL, and more.');
|
|
177
|
+
console.log(' Restart the agent session to pick it up. Re-run after CLI upgrades.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
148
180
|
case 'login': {
|
|
149
181
|
const base = resolveBase(values.base);
|
|
150
182
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
package/dist/publish.js
CHANGED
|
@@ -39,6 +39,7 @@ export async function publish(dir, opts = {}) {
|
|
|
39
39
|
files,
|
|
40
40
|
...(opts.spaMode ? { spaMode: true } : {}),
|
|
41
41
|
...(opts.claimToken ? { claimToken: opts.claimToken } : {}),
|
|
42
|
+
...(opts.slug ? { slug: opts.slug } : {}),
|
|
42
43
|
}),
|
|
43
44
|
});
|
|
44
45
|
await uploadAll(dir, created.upload.uploads);
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** The SKILL.md ships inside the npm package (skill/SKILL.md next to dist/). */
|
|
6
|
+
export async function bundledSkill() {
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist
|
|
8
|
+
return readFile(join(here, '..', 'skill', 'SKILL.md'), 'utf8');
|
|
9
|
+
}
|
|
10
|
+
/** Where agents look for skills. Claude Code is the reference layout
|
|
11
|
+
* (~/.claude/skills/<name>/SKILL.md); several other agents read the same
|
|
12
|
+
* format from their own folders. */
|
|
13
|
+
export function installTargets(project) {
|
|
14
|
+
if (project) {
|
|
15
|
+
return [{ agent: 'this project (Claude Code & compatible)', dir: join(process.cwd(), '.claude', 'skills', 'shiply') }];
|
|
16
|
+
}
|
|
17
|
+
return [{ agent: 'Claude Code (all projects)', dir: join(homedir(), '.claude', 'skills', 'shiply') }];
|
|
18
|
+
}
|
|
19
|
+
export async function installSkill(project) {
|
|
20
|
+
const content = await bundledSkill();
|
|
21
|
+
const written = [];
|
|
22
|
+
for (const t of installTargets(project)) {
|
|
23
|
+
await mkdir(t.dir, { recursive: true });
|
|
24
|
+
const file = join(t.dir, 'SKILL.md');
|
|
25
|
+
await writeFile(file, content);
|
|
26
|
+
written.push(`${t.agent}: ${file}`);
|
|
27
|
+
}
|
|
28
|
+
return written;
|
|
29
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const stateFile = (dir) => join(dir, '.shiply.json');
|
|
4
|
+
export async function readState(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await readFile(stateFile(dir), 'utf8');
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return parsed.slug ? parsed : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function writeState(dir, state) {
|
|
15
|
+
await writeFile(stateFile(dir), `${JSON.stringify(state, null, 2)}\n`);
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiply-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
7
7
|
"bin": { "shiply": "dist/index.js" },
|
|
8
|
-
"files": ["dist", "README.md"],
|
|
8
|
+
"files": ["dist", "skill", "README.md"],
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc -p tsconfig.build.json",
|
|
11
11
|
"prepublishOnly": "pnpm build",
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,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
|
+
## 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
|