shiply-cli 0.16.0 → 0.18.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/bundle.js +63 -0
- package/dist/confetti.js +5 -0
- package/dist/ignore.js +65 -0
- package/dist/index.js +187 -73
- package/dist/manifest.js +13 -4
- package/dist/previews.js +26 -0
- package/dist/sites.js +70 -0
- package/dist/ssr-adapters.js +95 -0
- package/dist/verify.js +26 -0
- package/package.json +7 -3
- package/skill/SKILL.md +52 -1
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { build } from 'esbuild';
|
|
5
|
+
const BUNDLE_COMPAT_DATE = '2025-05-05';
|
|
6
|
+
/** Stage a temp publish dir: static assets at root, worker modules under
|
|
7
|
+
* `.shiply/bundle/`, plus `.shiply/bundle.json`. Returns the dir; the caller
|
|
8
|
+
* removes it after publish. The existing publish pipeline handles the rest. */
|
|
9
|
+
export async function prepareBundle(sourceDir, ssr) {
|
|
10
|
+
const out = await mkdtemp(join(tmpdir(), 'shiply-bundle-'));
|
|
11
|
+
const bundleDir = join(out, '.shiply', 'bundle');
|
|
12
|
+
await mkdir(bundleDir, { recursive: true });
|
|
13
|
+
if (ssr.tier0) {
|
|
14
|
+
// Tier 0: esbuild-bundle the worker entry into one ESM module.
|
|
15
|
+
const result = await build({
|
|
16
|
+
entryPoints: [join(ssr.assetsDir, ssr.workerMain)],
|
|
17
|
+
bundle: true,
|
|
18
|
+
format: 'esm',
|
|
19
|
+
platform: 'neutral',
|
|
20
|
+
target: 'es2022',
|
|
21
|
+
write: false,
|
|
22
|
+
logLevel: 'silent',
|
|
23
|
+
});
|
|
24
|
+
await writeFile(join(bundleDir, 'index.js'), result.outputFiles[0].text);
|
|
25
|
+
if (ssr.staticDir) {
|
|
26
|
+
await cp(join(sourceDir, ssr.staticDir), out, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
await writeManifest(out, {
|
|
29
|
+
framework: ssr.framework,
|
|
30
|
+
entry: 'index.js',
|
|
31
|
+
modules: ['index.js'],
|
|
32
|
+
compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
|
|
33
|
+
compatibilityFlags: ssr.compatibilityFlags,
|
|
34
|
+
});
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
// Tier 1: copy the whole assets dir, then RELOCATE worker modules into
|
|
38
|
+
// .shiply/bundle/ so they aren't double-served as static assets.
|
|
39
|
+
await cp(ssr.assetsDir, out, { recursive: true });
|
|
40
|
+
for (const m of ssr.modules) {
|
|
41
|
+
const from = join(out, m);
|
|
42
|
+
const to = join(bundleDir, m);
|
|
43
|
+
await mkdir(dirname(to), { recursive: true });
|
|
44
|
+
await cp(from, to, { recursive: true });
|
|
45
|
+
await rm(from, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
// If the entry lived in a worker subdir (Astro's _worker.js/), drop the
|
|
48
|
+
// original subdir entirely so no leftover files are served as assets.
|
|
49
|
+
if (ssr.entry.includes('/')) {
|
|
50
|
+
await rm(join(out, ssr.entry.split('/')[0]), { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
await writeManifest(out, {
|
|
53
|
+
framework: ssr.framework,
|
|
54
|
+
entry: ssr.entry,
|
|
55
|
+
modules: ssr.modules,
|
|
56
|
+
compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
|
|
57
|
+
compatibilityFlags: ssr.compatibilityFlags,
|
|
58
|
+
});
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
async function writeManifest(out, m) {
|
|
62
|
+
await writeFile(join(out, '.shiply', 'bundle.json'), JSON.stringify({ v: 1, ...m }, null, 2));
|
|
63
|
+
}
|
package/dist/confetti.js
CHANGED
|
@@ -26,3 +26,8 @@ export function confetti(message = '⛵ SITE IS LIVE ⛵') {
|
|
|
26
26
|
}
|
|
27
27
|
return out;
|
|
28
28
|
}
|
|
29
|
+
/** Celebrate only on an interactive terminal and only when not opted out.
|
|
30
|
+
* Piped / redirected stdout (agents, CI, scrapers) never gets ANSI confetti. */
|
|
31
|
+
export function shouldCelebrate(noConfetti, isTty = process.stdout.isTTY) {
|
|
32
|
+
return !noConfetti && isTty === true;
|
|
33
|
+
}
|
package/dist/ignore.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function globBody(glob, anchored) {
|
|
4
|
+
// Escape regex specials, but keep * and / for glob expansion.
|
|
5
|
+
let g = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
g = g
|
|
7
|
+
.replace(/\*\*\//g, '(?:.+/)?') // **/ → any number of leading dirs
|
|
8
|
+
.replace(/\*\*/g, '.*') // ** → anything, crosses slashes
|
|
9
|
+
.replace(/\*/g, '[^/]*'); // * → anything but a slash
|
|
10
|
+
return anchored ? `^${g}` : `(?:^|/)${g}`;
|
|
11
|
+
}
|
|
12
|
+
function parseRule(raw) {
|
|
13
|
+
let pattern = raw.trim();
|
|
14
|
+
const negate = pattern.startsWith('!');
|
|
15
|
+
if (negate)
|
|
16
|
+
pattern = pattern.slice(1);
|
|
17
|
+
const dirOnly = pattern.endsWith('/');
|
|
18
|
+
if (dirOnly)
|
|
19
|
+
pattern = pattern.slice(0, -1);
|
|
20
|
+
const anchored = pattern.startsWith('/');
|
|
21
|
+
if (anchored)
|
|
22
|
+
pattern = pattern.slice(1);
|
|
23
|
+
const body = globBody(pattern, anchored);
|
|
24
|
+
// re: matches the path itself OR anything beneath it.
|
|
25
|
+
// descendantRe: matches ONLY things strictly beneath the named path. A
|
|
26
|
+
// dir-only rule uses re for directories (and their subtrees) but descendantRe
|
|
27
|
+
// for files, so a FILE that merely shares the dir's name is kept.
|
|
28
|
+
return {
|
|
29
|
+
re: new RegExp(`${body}(?:/|$)`),
|
|
30
|
+
descendantRe: new RegExp(`${body}/`),
|
|
31
|
+
negate,
|
|
32
|
+
dirOnly,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Compile .shiplyignore lines (a documented gitignore subset) into a matcher.
|
|
36
|
+
* Supported: # comments, blank lines, trailing-slash dir-only rules,
|
|
37
|
+
* leading-slash root anchoring, * and ** globs, and ! negation. Last match wins. */
|
|
38
|
+
export function compileIgnore(lines) {
|
|
39
|
+
const rules = lines
|
|
40
|
+
.map((l) => l.replace(/\r$/, ''))
|
|
41
|
+
.filter((l) => l.trim() !== '' && !l.trimStart().startsWith('#'))
|
|
42
|
+
.map(parseRule);
|
|
43
|
+
return (relPath, isDir) => {
|
|
44
|
+
let excluded = false;
|
|
45
|
+
for (const r of rules) {
|
|
46
|
+
// A dir-only rule excludes the directory itself (and its subtree) but, for
|
|
47
|
+
// a plain file, only when that file lives BENEATH the named directory —
|
|
48
|
+
// never a file that merely shares the directory's name.
|
|
49
|
+
const re = r.dirOnly && !isDir ? r.descendantRe : r.re;
|
|
50
|
+
if (re.test(relPath))
|
|
51
|
+
excluded = !r.negate;
|
|
52
|
+
}
|
|
53
|
+
return excluded;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Load `.shiplyignore` from the publish root; null when absent. */
|
|
57
|
+
export async function loadIgnore(dir) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(join(dir, '.shiplyignore'), 'utf8');
|
|
60
|
+
return compileIgnore(raw.split('\n'));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,12 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
|
-
import {
|
|
7
|
+
import { rm } from 'node:fs/promises';
|
|
8
|
+
import { confetti, shouldCelebrate } from './confetti.js';
|
|
8
9
|
import { runDetect } from './detect.js';
|
|
9
10
|
import { resolvePayloadDir } from './framework.js';
|
|
11
|
+
import { detectSsr } from './ssr-adapters.js';
|
|
12
|
+
import { prepareBundle } from './bundle.js';
|
|
10
13
|
import { runClaimVerify } from './claim.js';
|
|
11
14
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
12
15
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
@@ -23,8 +26,11 @@ import { loginViaDeviceFlow } from './login.js';
|
|
|
23
26
|
import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
24
27
|
import { installSkill } from './skill.js';
|
|
25
28
|
import { readState, writeState } from './state.js';
|
|
29
|
+
import { readPreview, writePreview } from './previews.js';
|
|
26
30
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
27
31
|
import { runStatus } from './status-cmd.js';
|
|
32
|
+
import { sitesLs, sitesPromote, sitesRm, sitesRollback } from './sites.js';
|
|
33
|
+
import { verifySite } from './verify.js';
|
|
28
34
|
// Resolve the package.json beside the compiled dist/ so `shiply --version`
|
|
29
35
|
// can print the running CLI version (works in dev tsc output + npm install).
|
|
30
36
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -47,10 +53,13 @@ const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
|
47
53
|
Usage:
|
|
48
54
|
shiply publish <dir> [options] Publish a directory, print the live URL.
|
|
49
55
|
Re-running UPDATES the same site (state in .shiply.json)
|
|
56
|
+
shiply publish <dir> --as <name> Stable preview: reuse the same URL across iterations
|
|
50
57
|
shiply update <dir> Same as publish when .shiply.json exists
|
|
51
58
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
52
59
|
(no arg → account/plan status, see below)
|
|
53
60
|
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
61
|
+
shiply promote <preview-slug> --to <dest-slug>
|
|
62
|
+
Copy the exact previewed bytes into a production site (no rebuild)
|
|
54
63
|
shiply detect [dir] Diagnose: print framework + dir that would upload
|
|
55
64
|
(no upload — use --framework=<name> to test an override)
|
|
56
65
|
shiply drive <ls|put|get|rm|publish> Private cloud storage (drive publish → live site)
|
|
@@ -140,11 +149,13 @@ Options:
|
|
|
140
149
|
--base <url> API origin (default: ${DEFAULT_BASE})
|
|
141
150
|
--wait (status) poll until the site is ready
|
|
142
151
|
--timeout <seconds> (status --wait) give up after this long (default 300)
|
|
152
|
+
--json (publish/update) print one JSON line {slug,siteUrl,…}, no decoration
|
|
143
153
|
--json <body> (data insert) JSON object to insert
|
|
144
154
|
--limit <N> (data query) max records to return (default 50)
|
|
145
155
|
--cursor <iso> (data query) pagination cursor from previous response
|
|
146
156
|
--where <json> (data query) client-side filter, e.g. '{"email":"a@b.co"}'
|
|
147
157
|
--yes (data wipe-collection) skip the safety prompt
|
|
158
|
+
--as <name> (publish) stable named preview — same URL every iteration
|
|
148
159
|
--no-confetti Celebrate quietly
|
|
149
160
|
|
|
150
161
|
\`shiply status\` prints stable machine-readable markers for agents:
|
|
@@ -173,7 +184,7 @@ async function reportReadiness(host, celebrate) {
|
|
|
173
184
|
else {
|
|
174
185
|
console.log(`✖ site: not ready${r.http.status ? ` (HTTP ${r.http.status})` : r.http.error ? ` — ${r.http.error}` : ''}`);
|
|
175
186
|
}
|
|
176
|
-
if (r.ready && celebrate)
|
|
187
|
+
if (r.ready && celebrate && shouldCelebrate(false))
|
|
177
188
|
console.log(confetti());
|
|
178
189
|
return r.ready;
|
|
179
190
|
}
|
|
@@ -183,17 +194,23 @@ async function main() {
|
|
|
183
194
|
// Detect the bare flag in raw argv BEFORE parseArgs sees it; strip the
|
|
184
195
|
// token so the parser doesn't complain about a missing string value.
|
|
185
196
|
const rawArgv = process.argv.slice(2);
|
|
186
|
-
|
|
197
|
+
// `--json` is a STRING option (data-insert body). For commands where it's a
|
|
198
|
+
// boolean flag (status/publish/update), detect a BARE --json (no following
|
|
199
|
+
// value) in raw argv and strip it before parseArgs sees it.
|
|
200
|
+
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update']);
|
|
201
|
+
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json') &&
|
|
187
202
|
!rawArgv.some((a, i) => a === '--json' && typeof rawArgv[i + 1] === 'string' && !rawArgv[i + 1].startsWith('-'));
|
|
188
|
-
if (
|
|
203
|
+
if (bareJsonFlag) {
|
|
189
204
|
const idx = rawArgv.indexOf('--json');
|
|
190
205
|
rawArgv.splice(idx, 1);
|
|
191
206
|
}
|
|
207
|
+
const statusJsonFlag = bareJsonFlag && rawArgv[0] === 'status';
|
|
192
208
|
const { values, positionals } = parseArgs({
|
|
193
209
|
args: rawArgv,
|
|
194
210
|
allowPositionals: true,
|
|
195
211
|
options: {
|
|
196
212
|
spa: { type: 'boolean' },
|
|
213
|
+
'no-ssr': { type: 'boolean' },
|
|
197
214
|
framework: { type: 'string' },
|
|
198
215
|
'claim-token': { type: 'string' },
|
|
199
216
|
key: { type: 'string' },
|
|
@@ -201,6 +218,7 @@ async function main() {
|
|
|
201
218
|
base: { type: 'string' },
|
|
202
219
|
email: { type: 'string' },
|
|
203
220
|
name: { type: 'string' },
|
|
221
|
+
as: { type: 'string' },
|
|
204
222
|
wait: { type: 'boolean' },
|
|
205
223
|
'new-site': { type: 'boolean' },
|
|
206
224
|
project: { type: 'boolean' },
|
|
@@ -253,81 +271,133 @@ async function main() {
|
|
|
253
271
|
if (!dir)
|
|
254
272
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
255
273
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
// SSR / worker-bundle lane: a built Cloudflare-adapter project (SvelteKit,
|
|
275
|
+
// Astro) or a wrangler worker (Hono/raw fetch) ships as a worker bundle.
|
|
276
|
+
// Skipped when --no-ssr or --framework (static override) is passed.
|
|
277
|
+
const ssr = values['no-ssr'] || values.framework ? null : await detectSsr(dir);
|
|
278
|
+
let stagingDir = null;
|
|
279
|
+
// Any static site works — publish the BUILD OUTPUT, not the source.
|
|
280
|
+
// resolvePayloadDir picks the right dir (source as-is, a matched
|
|
281
|
+
// framework's build output, or a pre-built fallback like dist/).
|
|
282
|
+
let publishDir;
|
|
283
|
+
let spa;
|
|
284
|
+
let stateDir;
|
|
285
|
+
if (ssr) {
|
|
286
|
+
stagingDir = await prepareBundle(dir, ssr);
|
|
287
|
+
publishDir = stagingDir;
|
|
288
|
+
spa = false; // SSR: an asset miss must 404 so the worker handles the route
|
|
289
|
+
stateDir = dir; // persist .shiply.json in the SOURCE dir, not the temp dir
|
|
290
|
+
if (!bareJsonFlag)
|
|
291
|
+
console.log(`✔ ${ssr.framework} (SSR) detected — deploying worker bundle`);
|
|
264
292
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (cmd === 'update' && !claimToken && !slug) {
|
|
274
|
-
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
293
|
+
else {
|
|
294
|
+
const resolved = await resolvePayloadDir(dir, { framework: values.framework });
|
|
295
|
+
publishDir = resolved.dir;
|
|
296
|
+
spa = values.spa ?? (resolved.hint?.spa ? true : undefined);
|
|
297
|
+
stateDir = publishDir;
|
|
298
|
+
if (resolved.hint && publishDir !== dir && !bareJsonFlag) {
|
|
299
|
+
console.log(`✔ ${resolved.hint.framework} project detected — publishing ${publishDir}${resolved.hint.spa ? ' with SPA mode' : ''}`);
|
|
300
|
+
}
|
|
275
301
|
}
|
|
276
|
-
const updating = Boolean(claimToken || slug);
|
|
277
|
-
// Auto-attach a previously-created database on publish. Anonymous
|
|
278
|
-
// publishes can't attach (no auth subject) — silently skipped server-side.
|
|
279
|
-
const attachDatabaseId = state?.databaseId && apiKey ? state.databaseId : undefined;
|
|
280
|
-
// Opt-in: bind THIS publish version to a Neon branch's URI. Server
|
|
281
|
-
// validates ownership + that the branch lives under a project attached
|
|
282
|
-
// to this site; a bogus/foreign id silently falls back to the parent URI.
|
|
283
|
-
const previewBranchDbId = values['preview-branch'];
|
|
284
|
-
const res = await publish(publishDir, {
|
|
285
|
-
apiKey,
|
|
286
|
-
base: values.base,
|
|
287
|
-
spaMode: spa,
|
|
288
|
-
claimToken,
|
|
289
|
-
slug,
|
|
290
|
-
attachDatabaseId,
|
|
291
|
-
previewBranchDbId,
|
|
292
|
-
});
|
|
293
|
-
await writeState(publishDir, {
|
|
294
|
-
slug: res.slug,
|
|
295
|
-
siteUrl: res.siteUrl,
|
|
296
|
-
// siteId only present on 0.14.4+ servers — prefer the new response,
|
|
297
|
-
// fall back to a prior state file so re-publishes don't drop it.
|
|
298
|
-
...(res.siteId ? { siteId: res.siteId } : state?.siteId ? { siteId: state.siteId } : {}),
|
|
299
|
-
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
|
300
|
-
owned: !res.anonymous,
|
|
301
|
-
...(state?.databaseId ? { databaseId: state.databaseId } : {}),
|
|
302
|
-
});
|
|
303
|
-
const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
|
|
304
|
-
console.log(`✔ ${updating ? `updated ${res.slug} in place` : 'published'} — ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
305
|
-
console.log(`\n ${res.siteUrl}\n`);
|
|
306
|
-
// confirm the site actually serves, then celebrate
|
|
307
302
|
try {
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
303
|
+
// same command, same URL: reuse this directory's site automatically
|
|
304
|
+
const state = await readState(stateDir);
|
|
305
|
+
// --as <name>: a stable named preview. The alias registry remembers which
|
|
306
|
+
// slug this name last published to, so the URL is reused every iteration —
|
|
307
|
+
// independent of which directory the bytes came from.
|
|
308
|
+
const aliasName = values.as;
|
|
309
|
+
const alias = aliasName ? await readPreview(aliasName) : null;
|
|
310
|
+
let claimToken = values['claim-token'] ?? alias?.claimToken ?? state?.claimToken;
|
|
311
|
+
let slug = (alias?.owned && apiKey ? alias.slug : undefined) ?? (state?.owned && apiKey ? state.slug : undefined);
|
|
312
|
+
if (values['new-site']) {
|
|
313
|
+
claimToken = values['claim-token'];
|
|
314
|
+
slug = undefined;
|
|
315
315
|
}
|
|
316
|
+
if (cmd === 'update' && !claimToken && !slug) {
|
|
317
|
+
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
318
|
+
}
|
|
319
|
+
const updating = Boolean(claimToken || slug);
|
|
320
|
+
// Auto-attach a previously-created database on publish. Anonymous
|
|
321
|
+
// publishes can't attach (no auth subject) — silently skipped server-side.
|
|
322
|
+
const attachDatabaseId = state?.databaseId && apiKey ? state.databaseId : undefined;
|
|
323
|
+
// Opt-in: bind THIS publish version to a Neon branch's URI. Server
|
|
324
|
+
// validates ownership + that the branch lives under a project attached
|
|
325
|
+
// to this site; a bogus/foreign id silently falls back to the parent URI.
|
|
326
|
+
const previewBranchDbId = values['preview-branch'];
|
|
327
|
+
const res = await publish(publishDir, {
|
|
328
|
+
apiKey,
|
|
329
|
+
base: values.base,
|
|
330
|
+
spaMode: spa,
|
|
331
|
+
claimToken,
|
|
332
|
+
slug,
|
|
333
|
+
attachDatabaseId,
|
|
334
|
+
previewBranchDbId,
|
|
335
|
+
});
|
|
336
|
+
await writeState(stateDir, {
|
|
337
|
+
slug: res.slug,
|
|
338
|
+
siteUrl: res.siteUrl,
|
|
339
|
+
// siteId only present on 0.14.4+ servers — prefer the new response,
|
|
340
|
+
// fall back to a prior state file so re-publishes don't drop it.
|
|
341
|
+
...(res.siteId ? { siteId: res.siteId } : state?.siteId ? { siteId: state.siteId } : {}),
|
|
342
|
+
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
|
343
|
+
owned: !res.anonymous,
|
|
344
|
+
...(state?.databaseId ? { databaseId: state.databaseId } : {}),
|
|
345
|
+
});
|
|
346
|
+
if (aliasName) {
|
|
347
|
+
await writePreview(aliasName, {
|
|
348
|
+
slug: res.slug,
|
|
349
|
+
owned: !res.anonymous,
|
|
350
|
+
...(res.siteId ? { siteId: res.siteId } : {}),
|
|
351
|
+
...(res.claimToken ? { claimToken: res.claimToken } : alias?.claimToken ? { claimToken: alias.claimToken } : {}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (bareJsonFlag) {
|
|
355
|
+
// Single-line machine output — no banners, no confetti, no readiness chatter.
|
|
356
|
+
process.stdout.write(`${JSON.stringify({
|
|
357
|
+
slug: res.slug,
|
|
358
|
+
siteUrl: res.siteUrl,
|
|
359
|
+
siteId: res.siteId ?? null,
|
|
360
|
+
uploaded: res.uploaded,
|
|
361
|
+
skipped: res.skipped,
|
|
362
|
+
anonymous: res.anonymous,
|
|
363
|
+
expiresAt: res.expiresAt,
|
|
364
|
+
updated: updating,
|
|
365
|
+
})}\n`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
|
|
369
|
+
console.log(`✔ ${updating ? `updated ${res.slug} in place` : 'published'} — ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
370
|
+
console.log(`\n ${res.siteUrl}\n`);
|
|
371
|
+
// confirm the site actually serves, then celebrate
|
|
372
|
+
try {
|
|
373
|
+
const host = new URL(res.siteUrl).hostname;
|
|
374
|
+
const live = await fetch(res.siteUrl, { method: 'GET', redirect: 'follow' });
|
|
375
|
+
void live.body?.cancel();
|
|
376
|
+
if (live.status < 400) {
|
|
377
|
+
console.log(`SITE_READY url=${res.siteUrl} status=${live.status}`);
|
|
378
|
+
if (shouldCelebrate(Boolean(values['no-confetti'])))
|
|
379
|
+
console.log(confetti(`⛵ ${host} IS LIVE ⛵`));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* serving check is best-effort */
|
|
384
|
+
}
|
|
385
|
+
if (!updating) {
|
|
386
|
+
console.log(` saved .shiply.json — run \`shiply publish ${dir}\` again to UPDATE this same site`);
|
|
387
|
+
console.log(` (gitignore .shiply.json if this folder is public — it can update the site)`);
|
|
388
|
+
}
|
|
389
|
+
if (res.anonymous) {
|
|
390
|
+
console.log(` anonymous site — expires ${res.expiresAt ?? 'in 24h'}`);
|
|
391
|
+
if (res.claimUrl)
|
|
392
|
+
console.log(` claim it to KEEP it (free account): ${res.claimUrl}`);
|
|
393
|
+
console.log(` tip: run \`shiply login\` first to publish permanent sites`);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
316
396
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!updating) {
|
|
321
|
-
console.log(` saved .shiply.json — run \`shiply publish ${dir}\` again to UPDATE this same site`);
|
|
322
|
-
console.log(` (gitignore .shiply.json if this folder is public — it can update the site)`);
|
|
323
|
-
}
|
|
324
|
-
if (res.anonymous) {
|
|
325
|
-
console.log(` anonymous site — expires ${res.expiresAt ?? 'in 24h'}`);
|
|
326
|
-
if (res.claimUrl)
|
|
327
|
-
console.log(` claim it to KEEP it (free account): ${res.claimUrl}`);
|
|
328
|
-
console.log(` tip: run \`shiply login\` first to publish permanent sites`);
|
|
397
|
+
finally {
|
|
398
|
+
if (stagingDir)
|
|
399
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
329
400
|
}
|
|
330
|
-
return;
|
|
331
401
|
}
|
|
332
402
|
case 'status': {
|
|
333
403
|
if (!dir) {
|
|
@@ -378,6 +448,50 @@ async function main() {
|
|
|
378
448
|
console.log(`\n ${out.siteUrl}\n`);
|
|
379
449
|
return;
|
|
380
450
|
}
|
|
451
|
+
case 'ls': {
|
|
452
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
453
|
+
if (!apiKey)
|
|
454
|
+
throw new Error('ls needs an API key — run `shiply login` first');
|
|
455
|
+
await sitesLs({ base: values.base, apiKey });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
case 'rm': {
|
|
459
|
+
if (!dir)
|
|
460
|
+
throw new Error('usage: shiply rm <slug> --yes');
|
|
461
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
462
|
+
if (!apiKey)
|
|
463
|
+
throw new Error('rm needs an API key — run `shiply login` first');
|
|
464
|
+
await sitesRm({ base: values.base, apiKey }, dir, { yes: Boolean(values.yes) });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
case 'rollback': {
|
|
468
|
+
if (!dir)
|
|
469
|
+
throw new Error('usage: shiply rollback <slug> [versionId] (omit versionId to list versions)');
|
|
470
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
471
|
+
if (!apiKey)
|
|
472
|
+
throw new Error('rollback needs an API key — run `shiply login` first');
|
|
473
|
+
await sitesRollback({ base: values.base, apiKey }, dir, positionals[2]);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
case 'verify': {
|
|
477
|
+
if (!dir)
|
|
478
|
+
throw new Error('usage: shiply verify <slug>');
|
|
479
|
+
// exit 0 when LIVE, 1 when PENDING — same contract as `status`, so an agent
|
|
480
|
+
// can chain `shiply verify <slug> && <next step>` safely.
|
|
481
|
+
const ready = await verifySite({ base: values.base }, dir);
|
|
482
|
+
process.exitCode = ready ? 0 : 1;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
case 'promote': {
|
|
486
|
+
const dest = values.to;
|
|
487
|
+
if (!dir || !dest)
|
|
488
|
+
throw new Error('usage: shiply promote <preview-slug> --to <dest-slug>');
|
|
489
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
490
|
+
if (!apiKey)
|
|
491
|
+
throw new Error('promote needs an API key — run `shiply login` first');
|
|
492
|
+
await sitesPromote({ base: values.base, apiKey }, dir, dest);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
381
495
|
case 'detect': {
|
|
382
496
|
await runDetect(dir ?? '.', { framework: values.framework });
|
|
383
497
|
return;
|
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { extname, join } from 'node:path';
|
|
4
|
+
import { loadIgnore } from './ignore.js';
|
|
4
5
|
const MIME = {
|
|
5
6
|
'.html': 'text/html',
|
|
6
7
|
'.htm': 'text/html',
|
|
@@ -40,22 +41,30 @@ const SKIP_DIRS = new Set(['node_modules']);
|
|
|
40
41
|
* config directory (proxy.json, data.json), which the server consumes. */
|
|
41
42
|
export async function buildManifest(dir) {
|
|
42
43
|
const out = [];
|
|
43
|
-
await
|
|
44
|
+
const ignore = await loadIgnore(dir);
|
|
45
|
+
await walk(dir, '', out, ignore);
|
|
44
46
|
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
45
47
|
}
|
|
46
|
-
async function walk(abs, rel, out) {
|
|
48
|
+
async function walk(abs, rel, out, ignore) {
|
|
47
49
|
const entries = await readdir(abs, { withFileTypes: true });
|
|
48
50
|
for (const e of entries) {
|
|
49
51
|
if (e.name === '.shiply' && e.isDirectory() && rel === '') {
|
|
50
|
-
|
|
52
|
+
// The .shiply/ config dir (proxy.json, data.json) is fully exempt from
|
|
53
|
+
// .shiplyignore — the server needs it, and a broad user rule like
|
|
54
|
+
// `*.json` must not be able to silently drop the site's own config.
|
|
55
|
+
// Pass a null matcher so nothing under .shiply/ is ignore-filtered.
|
|
56
|
+
await walk(join(abs, e.name), '.shiply', out, null);
|
|
51
57
|
continue;
|
|
52
58
|
}
|
|
53
59
|
if (e.name.startsWith('.') || SKIP_DIRS.has(e.name))
|
|
54
60
|
continue;
|
|
55
61
|
const childAbs = join(abs, e.name);
|
|
56
62
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
63
|
+
// .shiplyignore: skip matched paths (and whole subtrees for dir matches).
|
|
64
|
+
if (ignore?.(childRel, e.isDirectory()))
|
|
65
|
+
continue;
|
|
57
66
|
if (e.isDirectory()) {
|
|
58
|
-
await walk(childAbs, childRel, out);
|
|
67
|
+
await walk(childAbs, childRel, out, ignore);
|
|
59
68
|
}
|
|
60
69
|
else if (e.isFile()) {
|
|
61
70
|
const buf = await readFile(childAbs);
|
package/dist/previews.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const registryPath = (home) => join(home, '.shiply', 'previews.json');
|
|
5
|
+
async function readAll(home) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(await readFile(registryPath(home), 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Look up the slug/credentials previously bound to a `--as <name>` alias. */
|
|
14
|
+
export async function readPreview(name, home = homedir()) {
|
|
15
|
+
const all = await readAll(home);
|
|
16
|
+
return all[name] ?? null;
|
|
17
|
+
}
|
|
18
|
+
/** Remember the slug a `--as <name>` alias published to, so the next publish
|
|
19
|
+
* with the same name UPDATES the same URL (stable preview). */
|
|
20
|
+
export async function writePreview(name, entry, home = homedir()) {
|
|
21
|
+
const all = await readAll(home);
|
|
22
|
+
all[name] = entry;
|
|
23
|
+
const path = registryPath(home);
|
|
24
|
+
await mkdir(dirname(path), { recursive: true });
|
|
25
|
+
await writeFile(path, `${JSON.stringify(all, null, 2)}\n`);
|
|
26
|
+
}
|
package/dist/sites.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
const headers = (apiKey) => ({
|
|
3
|
+
'content-type': 'application/json',
|
|
4
|
+
authorization: `Bearer ${apiKey}`,
|
|
5
|
+
});
|
|
6
|
+
export async function sitesLs(ctx) {
|
|
7
|
+
const base = resolveBase(ctx.base);
|
|
8
|
+
const { sites } = await api(`${base}/api/v1/publishes`, {
|
|
9
|
+
headers: headers(ctx.apiKey),
|
|
10
|
+
});
|
|
11
|
+
if (sites.length === 0) {
|
|
12
|
+
console.log(' no sites yet — run `shiply publish <dir>` to create one');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const s of sites) {
|
|
16
|
+
const title = s.title ? ` ${s.title}` : '';
|
|
17
|
+
console.log(` ${s.slug} [${s.status}] https://${s.slug}.shiply.now${title}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function sitesRm(ctx, slug, opts) {
|
|
21
|
+
if (!opts.yes) {
|
|
22
|
+
console.error(` refusing — \`shiply rm\` PERMANENTLY deletes ${slug} and its files. Re-run with --yes to confirm.`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const base = resolveBase(ctx.base);
|
|
26
|
+
await api(`${base}/api/v1/publish/${slug}`, {
|
|
27
|
+
method: 'DELETE',
|
|
28
|
+
headers: headers(ctx.apiKey),
|
|
29
|
+
});
|
|
30
|
+
console.log(`✔ deleted ${slug}`);
|
|
31
|
+
}
|
|
32
|
+
export async function sitesRollback(ctx, slug, versionId) {
|
|
33
|
+
const base = resolveBase(ctx.base);
|
|
34
|
+
// No versionId → print the version history so the agent can pick one.
|
|
35
|
+
if (!versionId) {
|
|
36
|
+
const detail = await api(`${base}/api/v1/publish/${slug}`, { headers: headers(ctx.apiKey) });
|
|
37
|
+
const finalized = detail.versions.filter((v) => v.status === 'finalized');
|
|
38
|
+
if (finalized.length === 0) {
|
|
39
|
+
console.log(' no finalized versions to roll back to');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(` versions for ${slug} (newest first):`);
|
|
43
|
+
for (const v of finalized) {
|
|
44
|
+
const mark = v.id === detail.currentVersionId ? ' ← current (live)' : '';
|
|
45
|
+
console.log(` ${v.id} ${v.fileCount} files ${v.finalizedAt ?? ''}${mark}`);
|
|
46
|
+
}
|
|
47
|
+
console.log(`\n roll back with: shiply rollback ${slug} <versionId>`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const res = await api(`${base}/api/v1/publish/${slug}/rollback`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: headers(ctx.apiKey),
|
|
53
|
+
body: JSON.stringify({ versionId }),
|
|
54
|
+
});
|
|
55
|
+
if (!res.changed) {
|
|
56
|
+
console.log(` ${slug} is already serving ${versionId} — nothing to do`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(`✔ rolled ${res.slug} to ${res.currentVersionId} — live now`);
|
|
60
|
+
}
|
|
61
|
+
export async function sitesPromote(ctx, srcSlug, destSlug) {
|
|
62
|
+
const base = resolveBase(ctx.base);
|
|
63
|
+
const r = await api(`${base}/api/v1/publish/${srcSlug}/promote`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: headers(ctx.apiKey),
|
|
66
|
+
body: JSON.stringify({ destSlug }),
|
|
67
|
+
});
|
|
68
|
+
console.log(`✔ promoted ${r.sourceSlug} → ${r.slug} (${r.filesCount} files) — live now`);
|
|
69
|
+
console.log(`\n https://${r.slug}.shiply.now\n`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { parse as parseToml } from 'smol-toml';
|
|
4
|
+
const NODE_COMPAT = ['nodejs_compat'];
|
|
5
|
+
const exists = (p) => stat(p).then(() => true, () => false);
|
|
6
|
+
async function deps(dir) {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
|
|
9
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Recursively list every file under `root`, as posix paths relative to `base`. */
|
|
16
|
+
async function listFiles(root, base = root) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const e of await readdir(root, { withFileTypes: true })) {
|
|
19
|
+
const abs = join(root, e.name);
|
|
20
|
+
if (e.isDirectory())
|
|
21
|
+
out.push(...(await listFiles(abs, base)));
|
|
22
|
+
else
|
|
23
|
+
out.push(relative(base, abs).split('\\').join('/'));
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Detect a built SSR project (Cloudflare-adapter output or a wrangler worker)
|
|
28
|
+
* in `dir`. Returns null for plain static dirs. */
|
|
29
|
+
export async function detectSsr(dir) {
|
|
30
|
+
const d = await deps(dir);
|
|
31
|
+
// Tier 1: SvelteKit (single _worker.js + sibling assets)
|
|
32
|
+
if ('@sveltejs/adapter-cloudflare' in d) {
|
|
33
|
+
const out = join(dir, '.svelte-kit', 'cloudflare');
|
|
34
|
+
if (await exists(join(out, '_worker.js'))) {
|
|
35
|
+
return {
|
|
36
|
+
framework: 'sveltekit',
|
|
37
|
+
assetsDir: out,
|
|
38
|
+
entry: '_worker.js',
|
|
39
|
+
modules: ['_worker.js'],
|
|
40
|
+
compatibilityFlags: NODE_COMPAT,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Tier 1: Astro (dist/_worker.js/ multi-module + dist assets)
|
|
45
|
+
if ('@astrojs/cloudflare' in d) {
|
|
46
|
+
const out = join(dir, 'dist');
|
|
47
|
+
const workerDir = join(out, '_worker.js');
|
|
48
|
+
if (await exists(join(workerDir, 'index.js'))) {
|
|
49
|
+
const modules = (await listFiles(workerDir))
|
|
50
|
+
.filter((p) => p.endsWith('.js'))
|
|
51
|
+
.map((p) => `_worker.js/${p}`);
|
|
52
|
+
return {
|
|
53
|
+
framework: 'astro',
|
|
54
|
+
assetsDir: out,
|
|
55
|
+
entry: '_worker.js/index.js',
|
|
56
|
+
modules,
|
|
57
|
+
compatibilityFlags: NODE_COMPAT,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Tier 0: generic Cloudflare Worker via wrangler config
|
|
62
|
+
return detectWrangler(dir);
|
|
63
|
+
}
|
|
64
|
+
async function detectWrangler(dir) {
|
|
65
|
+
let cfg = null;
|
|
66
|
+
if (await exists(join(dir, 'wrangler.toml'))) {
|
|
67
|
+
cfg = parseToml(await readFile(join(dir, 'wrangler.toml'), 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
for (const f of ['wrangler.json', 'wrangler.jsonc']) {
|
|
71
|
+
if (await exists(join(dir, f))) {
|
|
72
|
+
cfg = JSON.parse(stripJsonc(await readFile(join(dir, f), 'utf8')));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!cfg || typeof cfg.main !== 'string')
|
|
78
|
+
return null;
|
|
79
|
+
const staticDir = cfg.assets?.directory ?? cfg.site?.bucket;
|
|
80
|
+
return {
|
|
81
|
+
framework: 'cloudflare-worker',
|
|
82
|
+
assetsDir: dir,
|
|
83
|
+
tier0: true,
|
|
84
|
+
workerMain: cfg.main,
|
|
85
|
+
staticDir: typeof staticDir === 'string' ? staticDir : undefined,
|
|
86
|
+
compatibilityDate: typeof cfg.compatibility_date === 'string' ? cfg.compatibility_date : undefined,
|
|
87
|
+
compatibilityFlags: Array.isArray(cfg.compatibility_flags) && cfg.compatibility_flags.length
|
|
88
|
+
? cfg.compatibility_flags
|
|
89
|
+
: NODE_COMPAT,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Strip // and block comments so wrangler.jsonc parses with JSON.parse. */
|
|
93
|
+
function stripJsonc(s) {
|
|
94
|
+
return s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
95
|
+
}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// packages/cli/src/verify.ts
|
|
2
|
+
import { api, resolveBase } from './publish.js';
|
|
3
|
+
/** Edge-check a shiply slug: SSL, HTTP reachability, and a rendered thumbnail.
|
|
4
|
+
* Headless agents use this to confirm what they shipped is actually serving.
|
|
5
|
+
* Returns true when LIVE, false when PENDING — the caller sets the exit code
|
|
6
|
+
* (0 / 1) so `shiply verify <slug> && <next>` only proceeds once it's serving. */
|
|
7
|
+
export async function verifySite(ctx, slug) {
|
|
8
|
+
if (slug.includes('.')) {
|
|
9
|
+
throw new Error(`verify takes a shiply slug, not a hostname — for custom domains use \`shiply status ${slug}\``);
|
|
10
|
+
}
|
|
11
|
+
const base = resolveBase(ctx.base);
|
|
12
|
+
const r = await api(`${base}/api/v1/sites/${slug}/verify`, {
|
|
13
|
+
headers: { 'content-type': 'application/json' },
|
|
14
|
+
});
|
|
15
|
+
const sslLine = r.ssl.valid
|
|
16
|
+
? `valid${r.ssl.issuer ? ` (${r.ssl.issuer}` : ''}${r.ssl.daysLeft !== undefined ? `, ${r.ssl.daysLeft}d left)` : r.ssl.issuer ? ')' : ''}`
|
|
17
|
+
: `not ready${r.ssl.error ? ` — ${r.ssl.error}` : ''}`;
|
|
18
|
+
console.log(`${r.status === 'LIVE' ? '✔' : '…'} ${slug}.shiply.now — ${r.status}`);
|
|
19
|
+
console.log(` ssl: ${sslLine}`);
|
|
20
|
+
console.log(` http: ${r.http.ok ? `ok (HTTP ${r.http.status})` : `not ready${r.http.status ? ` (HTTP ${r.http.status})` : r.http.error ? ` — ${r.http.error}` : ''}`}`);
|
|
21
|
+
if (r.thumbnailUrl)
|
|
22
|
+
console.log(` shot: ${r.thumbnailUrl}`);
|
|
23
|
+
// Stable machine marker for agents/scrapers (mirrors SITE_READY in `status`).
|
|
24
|
+
console.log(`VERIFY status=${r.status} http=${r.http.status ?? 0} ssl=${r.ssl.valid ? 'valid' : 'pending'}${r.thumbnailUrl ? ` thumb=${r.thumbnailUrl}` : ''}`);
|
|
25
|
+
return r.status === 'LIVE';
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiply-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -31,8 +31,12 @@
|
|
|
31
31
|
],
|
|
32
32
|
"homepage": "https://shiply.now",
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.15.0",
|
|
34
35
|
"typescript": "^5.8.0",
|
|
35
|
-
"vitest": "^3.1.0"
|
|
36
|
-
|
|
36
|
+
"vitest": "^3.1.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"esbuild": "^0.25.12",
|
|
40
|
+
"smol-toml": "^1.7.0"
|
|
37
41
|
}
|
|
38
42
|
}
|
package/skill/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ description: Publish static sites to the web instantly with shiply.now and manag
|
|
|
18
18
|
> fresh — your cached copy may be missing features like databases, projects,
|
|
19
19
|
> marketplace, or sending domains.
|
|
20
20
|
>
|
|
21
|
-
> **Last updated: 2026-06-
|
|
21
|
+
> **Last updated: 2026-06-23**
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -144,6 +144,57 @@ so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
|
144
144
|
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
145
145
|
lines for automation; exit code 0 = ready.
|
|
146
146
|
|
|
147
|
+
### SSR / server frameworks (live: SvelteKit, Astro, Hono/raw Workers)
|
|
148
|
+
|
|
149
|
+
`shiply publish` auto-detects a built Cloudflare-adapter project (SvelteKit
|
|
150
|
+
`@sveltejs/adapter-cloudflare`, Astro `@astrojs/cloudflare`) or a wrangler
|
|
151
|
+
worker (`wrangler.toml` with `main` — Hono, itty-router, raw `fetch`) and
|
|
152
|
+
deploys the worker bundle with `nodejs_compat`, serving static assets from the
|
|
153
|
+
edge. **Build first** (`npm run build`), then `shiply publish .`. Requires
|
|
154
|
+
sign-in + Developer plan. Force the static path with `--framework=<name>`, or
|
|
155
|
+
skip SSR detection entirely with `--no-ssr`. Next.js: waitlist (not yet live).
|
|
156
|
+
|
|
157
|
+
## Manage & verify sites (CLI)
|
|
158
|
+
|
|
159
|
+
List, delete, roll back, verify, and promote owned sites — all over the same
|
|
160
|
+
Bearer API key. Great for agents that ship iteratively and need to keep their
|
|
161
|
+
subdomains tidy.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
shiply ls # list your sites: slug, status, URL
|
|
165
|
+
shiply rm <slug> --yes # PERMANENTLY delete a site + its files (--yes required)
|
|
166
|
+
shiply rollback <slug> # list finalized versions (current one marked)
|
|
167
|
+
shiply rollback <slug> <versionId> # re-point the site to that version — live instantly
|
|
168
|
+
shiply verify <slug> # edge SSL + HTTP + thumbnail readiness check
|
|
169
|
+
shiply publish <dir> --as <name> # STABLE preview: same URL every iteration (local alias)
|
|
170
|
+
shiply promote <preview-slug> --to <dest-slug> # copy the preview's exact live bytes onto a prod site
|
|
171
|
+
shiply publish <dir> --json # one machine-readable JSON line, no confetti/banners
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- **`shiply verify <slug>`** prints a human report PLUS a stable machine marker
|
|
175
|
+
line `VERIFY status=LIVE http=200 ssl=valid thumb=…` (or `status=PENDING`).
|
|
176
|
+
Headless agents should parse that `VERIFY status=…` line — it's the
|
|
177
|
+
contract, like `SITE_READY` in `status`.
|
|
178
|
+
- **`shiply publish <dir> --as <name>`** gives a stable preview URL that the
|
|
179
|
+
next publish with the same `--as <name>` reuses — no more changing URLs or
|
|
180
|
+
orphaned scratch sites across iterations, independent of the source dir.
|
|
181
|
+
- **`shiply promote <preview-slug> --to <dest-slug>`** copies the *exact bytes*
|
|
182
|
+
you previewed onto an existing owned destination site (keeps its slug,
|
|
183
|
+
domains, access) — atomic preview→production, no rebuild.
|
|
184
|
+
- **`--json`** on `publish`/`update` emits a single JSON line
|
|
185
|
+
`{slug,siteUrl,siteId,uploaded,skipped,anonymous,expiresAt,updated}` and
|
|
186
|
+
suppresses all human output + confetti — use it in scripts/agents.
|
|
187
|
+
- **`.shiplyignore`** in the publish root excludes files from upload
|
|
188
|
+
(documented gitignore subset: `# comments`, `secret.txt`, `internal/`,
|
|
189
|
+
`*.log`, `/anchored`, `**` globs, `!negation`). Keep `.env`, notes, and
|
|
190
|
+
internal files out of the published bytes.
|
|
191
|
+
|
|
192
|
+
To verify captured signups/form data without the dashboard, the data
|
|
193
|
+
subcommands already exist: `shiply data list <slug>` (collections + counts),
|
|
194
|
+
`shiply data query <slug> <coll> [--limit N] [--where '<json>']`, and
|
|
195
|
+
`shiply data export <slug> <coll> [--out file.ndjson]`. Confetti is
|
|
196
|
+
suppressed automatically on non-TTY stdout; `--no-confetti` forces it off.
|
|
197
|
+
|
|
147
198
|
### 3. Raw HTTP (no installs)
|
|
148
199
|
```
|
|
149
200
|
1. POST https://shiply.now/api/v1/publish
|