shiply-cli 0.1.0 → 0.3.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
@@ -1,38 +1,47 @@
1
- # shiply-cli
2
-
3
- Publish static sites to [shiply.now](https://shiply.now) from the command line —
4
- instant web hosting built for agents.
5
-
6
- ```bash
7
- npm install -g shiply-cli
8
- # or
9
- curl -fsSL https://shiply.now/install.sh | bash
10
- ```
11
-
12
- ## Usage
13
-
14
- ```bash
15
- shiply publish ./dist # publish a directory, print the live URL
16
- shiply publish ./dist --spa # single-page app mode
17
- 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
- ```
20
-
21
- Without an API key, sites are anonymous: live immediately at
22
- `https://<slug>.shiply.now/`, expire after 24 hours, and print a one-time
23
- `claimToken`/`claimUrl` so they can be updated or claimed into an account.
24
-
25
- With an API key (`shiply login`, `$SHIPLY_API_KEY`, or `--key`), sites are
26
- permanent and owned by your account.
27
-
28
- Unchanged files are hash-skipped on updates only diffs are uploaded.
29
-
30
- ## For agents
31
-
32
- - Machine guide: <https://shiply.now/llms.txt>
33
- - OpenAPI spec: <https://shiply.now/openapi.json>
34
- - Docs: <https://shiply.now/docs>
35
-
36
- ## License
37
-
38
- MIT
1
+ # shiply-cli
2
+
3
+ Publish static sites to [shiply.now](https://shiply.now) from the command line —
4
+ instant web hosting built for agents.
5
+
6
+ ```bash
7
+ npm install -g shiply-cli
8
+ # or
9
+ curl -fsSL https://shiply.now/install.sh | bash
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```bash
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)
17
+ shiply publish ./dist --spa # single-page app mode
18
+ shiply login # email a 6-digit code, mint + save an API key
19
+ shiply status <slug-or-domain> [--wait] # SSL + readiness check — confetti when live 🎉
20
+ ```
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
+
26
+ `shiply status` prints stable `SSL_READY` / `SITE_READY` markers for agents and
27
+ exits 0 only when the certificate is valid and the site serves. `--wait` polls
28
+ until ready (great while a custom domain's certificate issues).
29
+
30
+ Without an API key, sites are anonymous: live immediately at
31
+ `https://<slug>.shiply.now/`, expire after 24 hours, and print a one-time
32
+ `claimToken`/`claimUrl` so they can be updated or claimed into an account.
33
+
34
+ With an API key (`shiply login`, `$SHIPLY_API_KEY`, or `--key`), sites are
35
+ permanent and owned by your account.
36
+
37
+ Unchanged files are hash-skipped on updates — only diffs are uploaded.
38
+
39
+ ## For agents
40
+
41
+ - Machine guide: <https://shiply.now/llms.txt>
42
+ - OpenAPI spec: <https://shiply.now/openapi.json>
43
+ - Docs: <https://shiply.now/docs>
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,28 @@
1
+ const GLYPHS = ['✦', '✧', '★', '☆', '●', '◆', '▲', '■', '♥', '✶', '*', 'o', '°'];
2
+ const COLORS = [196, 202, 208, 220, 226, 118, 46, 51, 45, 39, 99, 201, 207, 213];
3
+ const ESC = '\x1b';
4
+ /** Full-width ANSI confetti burst — sites going live deserve a party. */
5
+ export function confetti(message = '🎉 SITE IS LIVE 🎉') {
6
+ const cols = Math.min(process.stdout.columns ?? 80, 100);
7
+ const rows = Math.min(Math.max((process.stdout.rows ?? 24) - 6, 8), 18);
8
+ let out = '\n';
9
+ for (let r = 0; r < rows; r++) {
10
+ let line = '';
11
+ for (let c = 0; c < cols; c++) {
12
+ if (Math.random() < 0.12) {
13
+ const g = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
14
+ const color = COLORS[Math.floor(Math.random() * COLORS.length)];
15
+ line += `${ESC}[38;5;${color}m${g}${ESC}[0m`;
16
+ }
17
+ else {
18
+ line += ' ';
19
+ }
20
+ }
21
+ out += `${line}\n`;
22
+ if (r === Math.floor(rows / 2)) {
23
+ const pad = Math.max(Math.floor((cols - message.length) / 2), 0);
24
+ out += `${' '.repeat(pad)}${ESC}[1m${message}${ESC}[0m\n`;
25
+ }
26
+ }
27
+ return out;
28
+ }
package/dist/index.js CHANGED
@@ -1,27 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline/promises';
3
3
  import { parseArgs } from 'node:util';
4
+ import { confetti } from './confetti.js';
4
5
  import { loadApiKey, saveApiKey } from './config.js';
5
6
  import { api, DEFAULT_BASE, publish, resolveBase } from './publish.js';
6
- const HELP = `shiply instant static hosting for agents (https://shiply.now)
7
-
8
- Usage:
9
- shiply publish <dir> [options] Publish a directory, print the live URL
10
- shiply update <dir> --claim-token <token> Push a new version to an anonymous site
11
- shiply login [--email <address>] Email a 6-digit code, mint + save an API key
12
- shiply help
13
-
14
- Options:
15
- --spa Single-page app mode (unknown paths fall back to index.html)
16
- --claim-token <tok> Update the anonymous site this token belongs to
17
- --key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
18
- --anonymous Publish without an API key even if one is saved
19
- --base <url> API origin (default: ${DEFAULT_BASE})
20
-
21
- With an API key the site is permanent and owned by your account. Without one
22
- it expires in 24 hours save the printed claimToken/claimUrl to update or
23
- claim it later.
7
+ import { readState, writeState } from './state.js';
8
+ import { checkReadiness, targetToHostname } from './status.js';
9
+ const HELP = `shiply — instant static hosting for agents (https://shiply.now)
10
+
11
+ Usage:
12
+ shiply publish <dir> [options] Publish a directory, print the live URL.
13
+ Re-running UPDATES the same site (state in .shiply.json)
14
+ shiply update <dir> Same as publish when .shiply.json exists
15
+ shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
16
+ shiply login [--email <address>] Email a 6-digit code, mint + save an API key
17
+ shiply help
18
+
19
+ Options:
20
+ --spa Single-page app mode (unknown paths fall back to index.html)
21
+ --claim-token <tok> Update a specific anonymous site (overrides .shiply.json)
22
+ --new-site Ignore .shiply.json and create a fresh site
23
+ --key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
24
+ --anonymous Publish without an API key even if one is saved
25
+ --base <url> API origin (default: ${DEFAULT_BASE})
26
+ --wait (status) poll until the site is ready
27
+ --timeout <seconds> (status --wait) give up after this long (default 300)
28
+ --no-confetti Celebrate quietly
29
+
30
+ \`shiply status\` prints stable machine-readable markers for agents:
31
+ SSL_READY and SITE_READY on success (exit 0); ✖ lines and exit 1 otherwise.
32
+
33
+ With an API key the site is permanent and owned by your account. Without one
34
+ it expires in 24 hours — save the printed claimToken/claimUrl to update or
35
+ claim it later.
24
36
  `;
37
+ async function reportReadiness(host, celebrate) {
38
+ const r = await checkReadiness(host);
39
+ if (r.tls.valid) {
40
+ const detail = [r.tls.issuer, r.tls.daysLeft !== undefined ? `expires in ${r.tls.daysLeft} days` : '']
41
+ .filter(Boolean)
42
+ .join(', ');
43
+ console.log(`✔ ssl: valid${detail ? ` — ${detail}` : ''}`);
44
+ console.log(`SSL_READY host=${host}${r.tls.daysLeft !== undefined ? ` days_left=${r.tls.daysLeft}` : ''}`);
45
+ }
46
+ else {
47
+ console.log(`✖ ssl: not ready${r.tls.error ? ` — ${r.tls.error}` : ''}`);
48
+ }
49
+ if (r.http.ok) {
50
+ console.log(`✔ site: ready (HTTP ${r.http.status})`);
51
+ console.log(`SITE_READY url=https://${host}/ status=${r.http.status}`);
52
+ }
53
+ else {
54
+ console.log(`✖ site: not ready${r.http.status ? ` (HTTP ${r.http.status})` : r.http.error ? ` — ${r.http.error}` : ''}`);
55
+ }
56
+ if (r.ready && celebrate)
57
+ console.log(confetti());
58
+ return r.ready;
59
+ }
25
60
  async function main() {
26
61
  const { values, positionals } = parseArgs({
27
62
  allowPositionals: true,
@@ -32,6 +67,10 @@ async function main() {
32
67
  anonymous: { type: 'boolean' },
33
68
  base: { type: 'string' },
34
69
  email: { type: 'string' },
70
+ wait: { type: 'boolean' },
71
+ 'new-site': { type: 'boolean' },
72
+ timeout: { type: 'string' },
73
+ 'no-confetti': { type: 'boolean' },
35
74
  help: { type: 'boolean', short: 'h' },
36
75
  },
37
76
  });
@@ -45,31 +84,87 @@ async function main() {
45
84
  case 'update': {
46
85
  if (!dir)
47
86
  throw new Error(`usage: shiply ${cmd} <dir>`);
48
- if (cmd === 'update' && !values['claim-token']) {
49
- throw new Error('shiply update needs --claim-token <token> (printed by the original publish)');
50
- }
51
87
  const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
88
+ // same command, same URL: reuse this directory's site automatically
89
+ const state = await readState(dir);
90
+ let claimToken = values['claim-token'] ?? state?.claimToken;
91
+ let slug = state?.owned && apiKey ? state.slug : undefined;
92
+ if (values['new-site']) {
93
+ claimToken = values['claim-token'];
94
+ slug = undefined;
95
+ }
96
+ if (cmd === 'update' && !claimToken && !slug) {
97
+ throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
98
+ }
99
+ const updating = Boolean(claimToken || slug);
52
100
  const res = await publish(dir, {
53
101
  apiKey,
54
102
  base: values.base,
55
103
  spaMode: values.spa,
56
- claimToken: values['claim-token'],
104
+ claimToken,
105
+ slug,
106
+ });
107
+ await writeState(dir, {
108
+ slug: res.slug,
109
+ siteUrl: res.siteUrl,
110
+ ...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
111
+ owned: !res.anonymous,
57
112
  });
58
113
  const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
59
- console.log(`✔ published ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
114
+ console.log(`✔ ${updating ? `updated ${res.slug} in place` : 'published'} ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
60
115
  console.log(`\n ${res.siteUrl}\n`);
116
+ // confirm the site actually serves, then celebrate
117
+ try {
118
+ const host = new URL(res.siteUrl).hostname;
119
+ const live = await fetch(res.siteUrl, { method: 'GET', redirect: 'follow' });
120
+ void live.body?.cancel();
121
+ if (live.status < 400) {
122
+ console.log(`SITE_READY url=${res.siteUrl} status=${live.status}`);
123
+ if (!values['no-confetti'])
124
+ console.log(confetti(`🎉 ${host} IS LIVE 🎉`));
125
+ }
126
+ }
127
+ catch {
128
+ /* serving check is best-effort */
129
+ }
130
+ if (!updating) {
131
+ console.log(` saved .shiply.json — run \`shiply publish ${dir}\` again to UPDATE this same site`);
132
+ console.log(` (gitignore .shiply.json if this folder is public — it can update the site)`);
133
+ }
61
134
  if (res.anonymous) {
62
135
  console.log(` anonymous site — expires ${res.expiresAt ?? 'in 24h'}`);
63
136
  if (res.claimUrl)
64
- console.log(` claim it (make permanent): ${res.claimUrl}`);
65
- if (res.claimToken) {
66
- console.log(` claimToken (SAVE THIS — shown once): ${res.claimToken}`);
67
- console.log(` update later: shiply update <dir> --claim-token ${res.claimToken}`);
68
- }
137
+ console.log(` claim it to KEEP it (free account): ${res.claimUrl}`);
69
138
  console.log(` tip: run \`shiply login\` first to publish permanent sites`);
70
139
  }
71
140
  return;
72
141
  }
142
+ case 'status': {
143
+ if (!dir)
144
+ throw new Error('usage: shiply status <slug-or-domain> [--wait]');
145
+ const host = targetToHostname(dir);
146
+ const celebrate = !values['no-confetti'];
147
+ console.log(`checking https://${host}/ …`);
148
+ if (!values.wait) {
149
+ const ready = await reportReadiness(host, celebrate);
150
+ process.exitCode = ready ? 0 : 1;
151
+ return;
152
+ }
153
+ const timeoutMs = Number(values.timeout ?? 300) * 1000;
154
+ const deadline = Date.now() + timeoutMs;
155
+ for (;;) {
156
+ const ready = await reportReadiness(host, celebrate);
157
+ if (ready)
158
+ return;
159
+ if (Date.now() > deadline) {
160
+ console.error(`✖ timed out after ${Math.round(timeoutMs / 1000)}s — still not ready`);
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ console.log(' …waiting 5s (Ctrl-C to stop)');
165
+ await new Promise((r) => setTimeout(r, 5000));
166
+ }
167
+ }
73
168
  case 'login': {
74
169
  const base = resolveBase(values.base);
75
170
  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/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/dist/status.js ADDED
@@ -0,0 +1,47 @@
1
+ import { connect } from 'node:tls';
2
+ /** Normalize a CLI target (slug, hostname, or URL) to a bare hostname. */
3
+ export function targetToHostname(target) {
4
+ const t = target
5
+ .trim()
6
+ .toLowerCase()
7
+ .replace(/^https?:\/\//, '')
8
+ .replace(/\/.*$/, '');
9
+ return t.includes('.') ? t : `${t}.shiply.now`;
10
+ }
11
+ /** TLS handshake + peer certificate inspection (the "ssl checker"). */
12
+ export function checkTls(host, timeoutMs = 10_000) {
13
+ return new Promise((resolve) => {
14
+ const sock = connect({ host, port: 443, servername: host, timeout: timeoutMs }, () => {
15
+ const cert = sock.getPeerCertificate();
16
+ const validTo = cert?.valid_to ? new Date(cert.valid_to) : undefined;
17
+ const daysLeft = validTo
18
+ ? Math.round((validTo.getTime() - Date.now()) / 86_400_000)
19
+ : undefined;
20
+ const issuer = cert?.issuer?.O ??
21
+ cert?.issuer?.CN;
22
+ const valid = sock.authorized;
23
+ const error = valid ? undefined : String(sock.authorizationError ?? 'not authorized');
24
+ sock.end();
25
+ resolve({ valid, issuer, daysLeft, error });
26
+ });
27
+ sock.on('error', (e) => resolve({ valid: false, error: e.message }));
28
+ sock.on('timeout', () => {
29
+ sock.destroy();
30
+ resolve({ valid: false, error: 'TLS handshake timed out' });
31
+ });
32
+ });
33
+ }
34
+ export async function checkHttp(host) {
35
+ try {
36
+ const res = await fetch(`https://${host}/`, { method: 'GET', redirect: 'follow' });
37
+ void res.body?.cancel();
38
+ return { ok: res.status < 400, status: res.status };
39
+ }
40
+ catch (e) {
41
+ return { ok: false, error: e.message };
42
+ }
43
+ }
44
+ export async function checkReadiness(host) {
45
+ const [tls, http] = await Promise.all([checkTls(host), checkHttp(host)]);
46
+ return { host, tls, http, ready: tls.valid && http.ok };
47
+ }
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "shiply-cli",
3
- "version": "0.1.0",
4
- "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": { "shiply": "dist/index.js" },
8
- "files": ["dist", "README.md"],
9
- "scripts": {
10
- "build": "tsc -p tsconfig.build.json",
11
- "prepublishOnly": "pnpm build",
12
- "test": "vitest run",
13
- "typecheck": "tsc --noEmit"
14
- },
15
- "engines": { "node": ">=18" },
16
- "keywords": ["shiply", "hosting", "static", "deploy", "agents", "publish"],
17
- "homepage": "https://shiply.now",
18
- "devDependencies": {
19
- "typescript": "^5.8.0",
20
- "vitest": "^3.1.0",
21
- "@types/node": "^22.15.0"
22
- }
23
- }
1
+ {
2
+ "name": "shiply-cli",
3
+ "version": "0.3.0",
4
+ "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": { "shiply": "dist/index.js" },
8
+ "files": ["dist", "README.md"],
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "prepublishOnly": "pnpm build",
12
+ "test": "vitest run",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "engines": { "node": ">=18" },
16
+ "keywords": ["shiply", "hosting", "static", "deploy", "agents", "publish"],
17
+ "homepage": "https://shiply.now",
18
+ "devDependencies": {
19
+ "typescript": "^5.8.0",
20
+ "vitest": "^3.1.0",
21
+ "@types/node": "^22.15.0"
22
+ }
23
+ }