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 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
- shiply update <dir> --claim-token <token> Push a new version to an anonymous site
13
- shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
14
- shiply login [--email <address>] Email a 6-digit code, mint + save an API key
15
- shiply help
16
-
17
- Options:
18
- --spa Single-page app mode (unknown paths fall back to index.html)
19
- --claim-token <tok> Update the anonymous site this token belongs to
20
- --key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
21
- --anonymous Publish without an API key even if one is saved
22
- --base <url> API origin (default: ${DEFAULT_BASE})
23
- --wait (status) poll until the site is ready
24
- --timeout <seconds> (status --wait) give up after this long (default 300)
25
- --no-confetti Celebrate quietly
26
-
27
- \`shiply status\` prints stable machine-readable markers for agents:
28
- SSL_READY and SITE_READY on success (exit 0); lines and exit 1 otherwise.
29
-
30
- With an API key the site is permanent and owned by your account. Without one
31
- it expires in 24 hours save the printed claimToken/claimUrl to update or
32
- claim it later.
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: values['claim-token'],
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 (make permanent): ${res.claimUrl}`);
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.2.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