shiply-cli 0.1.0 → 0.2.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 +43 -38
- package/dist/confetti.js +28 -0
- package/dist/index.js +75 -0
- package/dist/status.js +47 -0
- package/package.json +23 -23
package/README.md
CHANGED
|
@@ -1,38 +1,43 @@
|
|
|
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
|
-
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
shiply status <slug-or-domain> [--wait] # SSL + readiness check — confetti when live 🎉
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`shiply status` prints stable `SSL_READY` / `SITE_READY` markers for agents and
|
|
23
|
+
exits 0 only when the certificate is valid and the site serves. `--wait` polls
|
|
24
|
+
until ready (great while a custom domain's certificate issues).
|
|
25
|
+
|
|
26
|
+
Without an API key, sites are anonymous: live immediately at
|
|
27
|
+
`https://<slug>.shiply.now/`, expire after 24 hours, and print a one-time
|
|
28
|
+
`claimToken`/`claimUrl` so they can be updated or claimed into an account.
|
|
29
|
+
|
|
30
|
+
With an API key (`shiply login`, `$SHIPLY_API_KEY`, or `--key`), sites are
|
|
31
|
+
permanent and owned by your account.
|
|
32
|
+
|
|
33
|
+
Unchanged files are hash-skipped on updates — only diffs are uploaded.
|
|
34
|
+
|
|
35
|
+
## For agents
|
|
36
|
+
|
|
37
|
+
- Machine guide: <https://shiply.now/llms.txt>
|
|
38
|
+
- OpenAPI spec: <https://shiply.now/openapi.json>
|
|
39
|
+
- Docs: <https://shiply.now/docs>
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/dist/confetti.js
ADDED
|
@@ -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,13 +1,16 @@
|
|
|
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';
|
|
7
|
+
import { checkReadiness, targetToHostname } from './status.js';
|
|
6
8
|
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
7
9
|
|
|
8
10
|
Usage:
|
|
9
11
|
shiply publish <dir> [options] Publish a directory, print the live URL
|
|
10
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)
|
|
11
14
|
shiply login [--email <address>] Email a 6-digit code, mint + save an API key
|
|
12
15
|
shiply help
|
|
13
16
|
|
|
@@ -17,11 +20,40 @@ Options:
|
|
|
17
20
|
--key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
|
|
18
21
|
--anonymous Publish without an API key even if one is saved
|
|
19
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.
|
|
20
29
|
|
|
21
30
|
With an API key the site is permanent and owned by your account. Without one
|
|
22
31
|
it expires in 24 hours — save the printed claimToken/claimUrl to update or
|
|
23
32
|
claim it later.
|
|
24
33
|
`;
|
|
34
|
+
async function reportReadiness(host, celebrate) {
|
|
35
|
+
const r = await checkReadiness(host);
|
|
36
|
+
if (r.tls.valid) {
|
|
37
|
+
const detail = [r.tls.issuer, r.tls.daysLeft !== undefined ? `expires in ${r.tls.daysLeft} days` : '']
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(', ');
|
|
40
|
+
console.log(`✔ ssl: valid${detail ? ` — ${detail}` : ''}`);
|
|
41
|
+
console.log(`SSL_READY host=${host}${r.tls.daysLeft !== undefined ? ` days_left=${r.tls.daysLeft}` : ''}`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(`✖ ssl: not ready${r.tls.error ? ` — ${r.tls.error}` : ''}`);
|
|
45
|
+
}
|
|
46
|
+
if (r.http.ok) {
|
|
47
|
+
console.log(`✔ site: ready (HTTP ${r.http.status})`);
|
|
48
|
+
console.log(`SITE_READY url=https://${host}/ status=${r.http.status}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`✖ site: not ready${r.http.status ? ` (HTTP ${r.http.status})` : r.http.error ? ` — ${r.http.error}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
if (r.ready && celebrate)
|
|
54
|
+
console.log(confetti());
|
|
55
|
+
return r.ready;
|
|
56
|
+
}
|
|
25
57
|
async function main() {
|
|
26
58
|
const { values, positionals } = parseArgs({
|
|
27
59
|
allowPositionals: true,
|
|
@@ -32,6 +64,9 @@ async function main() {
|
|
|
32
64
|
anonymous: { type: 'boolean' },
|
|
33
65
|
base: { type: 'string' },
|
|
34
66
|
email: { type: 'string' },
|
|
67
|
+
wait: { type: 'boolean' },
|
|
68
|
+
timeout: { type: 'string' },
|
|
69
|
+
'no-confetti': { type: 'boolean' },
|
|
35
70
|
help: { type: 'boolean', short: 'h' },
|
|
36
71
|
},
|
|
37
72
|
});
|
|
@@ -58,6 +93,20 @@ async function main() {
|
|
|
58
93
|
const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
|
|
59
94
|
console.log(`✔ published ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
60
95
|
console.log(`\n ${res.siteUrl}\n`);
|
|
96
|
+
// confirm the site actually serves, then celebrate
|
|
97
|
+
try {
|
|
98
|
+
const host = new URL(res.siteUrl).hostname;
|
|
99
|
+
const live = await fetch(res.siteUrl, { method: 'GET', redirect: 'follow' });
|
|
100
|
+
void live.body?.cancel();
|
|
101
|
+
if (live.status < 400) {
|
|
102
|
+
console.log(`SITE_READY url=${res.siteUrl} status=${live.status}`);
|
|
103
|
+
if (!values['no-confetti'])
|
|
104
|
+
console.log(confetti(`🎉 ${host} IS LIVE 🎉`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* serving check is best-effort */
|
|
109
|
+
}
|
|
61
110
|
if (res.anonymous) {
|
|
62
111
|
console.log(` anonymous site — expires ${res.expiresAt ?? 'in 24h'}`);
|
|
63
112
|
if (res.claimUrl)
|
|
@@ -70,6 +119,32 @@ async function main() {
|
|
|
70
119
|
}
|
|
71
120
|
return;
|
|
72
121
|
}
|
|
122
|
+
case 'status': {
|
|
123
|
+
if (!dir)
|
|
124
|
+
throw new Error('usage: shiply status <slug-or-domain> [--wait]');
|
|
125
|
+
const host = targetToHostname(dir);
|
|
126
|
+
const celebrate = !values['no-confetti'];
|
|
127
|
+
console.log(`checking https://${host}/ …`);
|
|
128
|
+
if (!values.wait) {
|
|
129
|
+
const ready = await reportReadiness(host, celebrate);
|
|
130
|
+
process.exitCode = ready ? 0 : 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const timeoutMs = Number(values.timeout ?? 300) * 1000;
|
|
134
|
+
const deadline = Date.now() + timeoutMs;
|
|
135
|
+
for (;;) {
|
|
136
|
+
const ready = await reportReadiness(host, celebrate);
|
|
137
|
+
if (ready)
|
|
138
|
+
return;
|
|
139
|
+
if (Date.now() > deadline) {
|
|
140
|
+
console.error(`✖ timed out after ${Math.round(timeoutMs / 1000)}s — still not ready`);
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log(' …waiting 5s (Ctrl-C to stop)');
|
|
145
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
73
148
|
case 'login': {
|
|
74
149
|
const base = resolveBase(values.base);
|
|
75
150
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
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.
|
|
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.2.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
|
+
}
|