shiply-cli 0.19.0 → 0.20.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 CHANGED
@@ -57,12 +57,12 @@ async function bundleWorker(entryAbs, opts = {}) {
57
57
  * self-contained — `_worker.js` imports server code from sibling dirs
58
58
  * (`../output/server`, `../cloudflare-tmp`) — so we bundle it exactly as
59
59
  * `wrangler deploy` would, rather than upload it as-is. */
60
- export async function prepareBundle(sourceDir, ssr) {
60
+ export async function prepareBundle(sourceDir, ssr, extras = {}) {
61
61
  const out = await mkdtemp(join(tmpdir(), 'shiply-bundle-'));
62
62
  const bundleDir = join(out, '.shiply', 'bundle');
63
63
  await mkdir(bundleDir, { recursive: true });
64
64
  if (ssr.bundleWith === 'wrangler') {
65
- await stageWranglerBundle(sourceDir, ssr, out, bundleDir);
65
+ await stageWranglerBundle(sourceDir, ssr, out, bundleDir, extras);
66
66
  return out;
67
67
  }
68
68
  // Bundle the worker entry into one self-contained ESM module.
@@ -87,6 +87,7 @@ export async function prepareBundle(sourceDir, ssr) {
87
87
  modules: ['index.js'],
88
88
  compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
89
89
  compatibilityFlags: ssr.compatibilityFlags,
90
+ isr: extras.isr,
90
91
  });
91
92
  return out;
92
93
  }
@@ -94,7 +95,7 @@ export async function prepareBundle(sourceDir, ssr) {
94
95
  * bundler converts dynamic require('node:*') to top-level imports — esbuild
95
96
  * can't), then deploy that worker multi-module with a hand-written asset-first
96
97
  * entry. Two modules: index.js (entry) imports ./worker.js (wrangler's bundle). */
97
- async function stageWranglerBundle(sourceDir, ssr, out, bundleDir) {
98
+ async function stageWranglerBundle(sourceDir, ssr, out, bundleDir, extras = {}) {
98
99
  const wtmp = await mkdtemp(join(tmpdir(), 'shiply-wrangler-'));
99
100
  // `--dry-run` bundles without deploying (no CF auth needed). Local wrangler
100
101
  // (OpenNext projects depend on it) is preferred via npx. shell:true → npx.cmd on Windows.
@@ -145,9 +146,13 @@ async function stageWranglerBundle(sourceDir, ssr, out, bundleDir) {
145
146
  modules: ['index.js', 'worker.js'],
146
147
  compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
147
148
  compatibilityFlags: ssr.compatibilityFlags,
149
+ isr: extras.isr,
148
150
  });
149
151
  await rm(wtmp, { recursive: true, force: true });
150
152
  }
151
153
  async function writeManifest(out, m) {
152
- await writeFile(join(out, '.shiply', 'bundle.json'), JSON.stringify({ v: 1, ...m }, null, 2));
154
+ // Drop an undefined `isr` so static/non-Next bundles keep a clean manifest.
155
+ const { isr, ...rest } = m;
156
+ const doc = isr ? { v: 1, ...rest, isr } : { v: 1, ...rest };
157
+ await writeFile(join(out, '.shiply', 'bundle.json'), JSON.stringify(doc, null, 2));
153
158
  }
package/dist/functions.js CHANGED
@@ -161,6 +161,62 @@ async function removeFnCmd(argv, inherited) {
161
161
  }
162
162
  console.log(`✔ removed worker for ${slug} — static-only routing restored`);
163
163
  }
164
+ // --- shiply logs ------------------------------------------------------------
165
+ const LOGS_USAGE = [
166
+ 'Usage: shiply logs <slug> [--limit N] [--since MIN] [--json]',
167
+ ' shiply logs <slug> # 50 newest events from the last 60 min',
168
+ ' shiply logs <slug> --since 1440 # last 24h (max 10080 = 7 days)',
169
+ ' shiply logs <slug> --limit 200 --json',
170
+ ].join('\n');
171
+ /** Render one log event as a single tail-style line. Pure → unit-testable. */
172
+ export function formatLogLine(e) {
173
+ const ts = new Date(e.timestamp).toISOString();
174
+ const lvl = (e.level || 'info').toUpperCase().padEnd(5);
175
+ const status = e.statusCode ? ` ${e.statusCode}` : '';
176
+ const outcome = e.outcome && e.outcome !== 'ok' ? ` !${e.outcome}` : '';
177
+ const timing = e.wallTimeMs != null ? ` (${e.cpuTimeMs ?? '?'}ms cpu / ${e.wallTimeMs}ms wall)` : '';
178
+ return `${ts} ${lvl} ${e.message}${status}${outcome}${timing}`;
179
+ }
180
+ /** Render a logs payload to the lines `shiply logs` prints: oldest→newest
181
+ * (tail-style, newest at the bottom) + a footer, or a single empty-window hint.
182
+ * The input is newest-first (the API + core both guarantee it), so we reverse.
183
+ * Pure → unit-testable without spawning the process. */
184
+ export function formatLogs(logs, scriptName, slug) {
185
+ if (logs.length === 0) {
186
+ return [
187
+ `no logs for ${slug} in the window — the function may not have been hit recently (try --since 1440)`,
188
+ ];
189
+ }
190
+ const lines = [...logs].reverse().map(formatLogLine);
191
+ lines.push(`\n— ${logs.length} event${logs.length === 1 ? '' : 's'} from ${scriptName}`);
192
+ return lines;
193
+ }
194
+ /** `shiply logs <slug>` — read recent runtime logs for a site's per-site Worker
195
+ * (Cloudflare Workers Observability, 7-day retention). Prints chronological
196
+ * (newest at the bottom, tail-style); `--json` dumps the raw payload. */
197
+ export async function runLogs(argv, inherited = {}) {
198
+ const { rest, flags } = readFlags(argv, inherited);
199
+ const slug = rest[0];
200
+ if (!slug) {
201
+ console.error(LOGS_USAGE);
202
+ process.exit(1);
203
+ }
204
+ const apiKey = await getApiKey(flags, 'logs');
205
+ const base = resolveBase(flags.base);
206
+ const qs = new URLSearchParams();
207
+ if (inherited.limit)
208
+ qs.set('limit', inherited.limit);
209
+ if (inherited.since)
210
+ qs.set('since', inherited.since);
211
+ const suffix = qs.toString() ? `?${qs.toString()}` : '';
212
+ const r = await api(`${base}/api/v1/sites/${slug}/logs${suffix}`, { headers: headers(apiKey) });
213
+ if (flags.json) {
214
+ console.log(JSON.stringify(r, null, 2));
215
+ return;
216
+ }
217
+ for (const line of formatLogs(r.logs, r.scriptName, slug))
218
+ console.log(line);
219
+ }
164
220
  // --- shiply secret ----------------------------------------------------------
165
221
  const SECRET_USAGE = [
166
222
  'Usage: shiply secret <set|ls|rm> <slug> [args] [--json]',
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { runDetect } from './detect.js';
10
10
  import { resolvePayloadDir } from './framework.js';
11
11
  import { detectSsr } from './ssr-adapters.js';
12
12
  import { prepareBundle } from './bundle.js';
13
+ import { detectIsr } from './isr.js';
13
14
  import { runClaimVerify } from './claim.js';
14
15
  import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
15
16
  import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
@@ -18,7 +19,7 @@ import { runProject } from './projects.js';
18
19
  import { runListing } from './listings.js';
19
20
  import { runSendingDomain } from './sending-domains.js';
20
21
  import { contractAmend, contractDraft, contractList, contractPdf, contractRetract, contractSend, contractShow, } from './contract.js';
21
- import { runCron, runFunction, runSecret } from './functions.js';
22
+ import { runCron, runFunction, runLogs, runSecret } from './functions.js';
22
23
  import { runEmail } from './email.js';
23
24
  import { runMailbox } from './mailbox.js';
24
25
  import { loadApiKey, saveApiKey } from './config.js';
@@ -96,6 +97,9 @@ Usage:
96
97
  in your worker on next deploy/restart.
97
98
  shiply cron <ls|set|rm> <slug> Manage cron triggers on the deployed worker
98
99
  shiply cron set <slug> /api/cron/foo "0 9 * * *"
100
+ shiply logs <slug> [--limit N] [--since MIN] Read recent runtime logs for the site's worker
101
+ (Cloudflare Workers Observability, 7-day retention).
102
+ Newest at the bottom; --since in minutes; --json for raw.
99
103
  shiply contract <list|draft|show|send|pdf|amend|retract>
100
104
  Customer contracts — full lifecycle CLI (B14).
101
105
  list <project-id> — parent + amendments table
@@ -197,7 +201,7 @@ async function main() {
197
201
  // `--json` is a STRING option (data-insert body). For commands where it's a
198
202
  // boolean flag (status/publish/update), detect a BARE --json (no following
199
203
  // value) in raw argv and strip it before parseArgs sees it.
200
- const BARE_JSON_CMDS = new Set(['status', 'publish', 'update']);
204
+ const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs']);
201
205
  const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json') &&
202
206
  !rawArgv.some((a, i) => a === '--json' && typeof rawArgv[i + 1] === 'string' && !rawArgv[i + 1].startsWith('-'));
203
207
  if (bareJsonFlag) {
@@ -235,6 +239,7 @@ async function main() {
235
239
  'no-confetti': { type: 'boolean' },
236
240
  json: { type: 'string' },
237
241
  limit: { type: 'string' },
242
+ since: { type: 'string' },
238
243
  cursor: { type: 'string' },
239
244
  where: { type: 'string' },
240
245
  scope: { type: 'string' },
@@ -283,12 +288,22 @@ async function main() {
283
288
  let spa;
284
289
  let stateDir;
285
290
  if (ssr) {
286
- stagingDir = await prepareBundle(dir, ssr);
291
+ // Next ISR demand instrument: measure whether this app asks to
292
+ // revalidate (no infra is provisioned for it yet — those routes serve as
293
+ // live SSR). Records the signal in the bundle for the server-side count
294
+ // and tells the user plainly. Only Next emits a prerender manifest.
295
+ const isr = ssr.framework === 'next' ? ((await detectIsr(dir)) ?? undefined) : undefined;
296
+ stagingDir = await prepareBundle(dir, ssr, { isr });
287
297
  publishDir = stagingDir;
288
298
  spa = false; // SSR: an asset miss must 404 so the worker handles the route
289
299
  stateDir = dir; // persist .shiply.json in the SOURCE dir, not the temp dir
290
- if (!bareJsonFlag)
300
+ if (!bareJsonFlag) {
291
301
  console.log(`✔ ${ssr.framework} (SSR) detected — deploying worker bundle`);
302
+ if (isr && isr.timeBased > 0) {
303
+ console.log(`ℹ Next ISR/revalidate detected on ${isr.timeBased} route(s) (e.g. ${isr.routes[0]}) — ` +
304
+ `shiply serves these as live SSR (re-rendered each request) for now; persistent ISR caching is on the roadmap.`);
305
+ }
306
+ }
292
307
  }
293
308
  else {
294
309
  const resolved = await resolvePayloadDir(dir, { framework: values.framework });
@@ -744,6 +759,18 @@ async function main() {
744
759
  });
745
760
  break;
746
761
  }
762
+ case 'logs': {
763
+ // `--json` on `logs` is a BARE flag — stripped from rawArgv before parseArgs
764
+ // (see BARE_JSON_CMDS), so detect it via bareJsonFlag, not values.json.
765
+ await runLogs(positionals.slice(1), {
766
+ base: values.base,
767
+ key: values.key,
768
+ json: bareJsonFlag || (typeof values.json === 'string' && values.json.length > 0),
769
+ limit: values.limit,
770
+ since: values.since,
771
+ });
772
+ break;
773
+ }
747
774
  case 'cron': {
748
775
  await runCron(positionals.slice(1), {
749
776
  base: values.base,
package/dist/isr.js ADDED
@@ -0,0 +1,50 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const ROUTE_SAMPLE_CAP = 20;
4
+ /** Parse a Next `prerender-manifest.json` and count routes that want time-based
5
+ * revalidation. Pure + total: any malformed/empty input yields zero demand so a
6
+ * publish can never break on it. On-demand revalidation (revalidateTag/Path) is
7
+ * NOT separately detected here — it doesn't live in the prerender manifest. */
8
+ export function scanIsr(prerenderManifestJson) {
9
+ let doc;
10
+ try {
11
+ doc = JSON.parse(prerenderManifestJson);
12
+ }
13
+ catch {
14
+ return { timeBased: 0, routes: [] };
15
+ }
16
+ const buckets = [
17
+ doc?.routes,
18
+ doc?.dynamicRoutes,
19
+ ];
20
+ const hits = new Set();
21
+ for (const bucket of buckets) {
22
+ if (!bucket || typeof bucket !== 'object')
23
+ continue;
24
+ for (const [route, meta] of Object.entries(bucket)) {
25
+ const r = meta?.initialRevalidateSeconds;
26
+ if (typeof r === 'number' && Number.isFinite(r) && r > 0)
27
+ hits.add(route);
28
+ }
29
+ }
30
+ const routes = [...hits].sort();
31
+ return { timeBased: routes.length, routes: routes.slice(0, ROUTE_SAMPLE_CAP) };
32
+ }
33
+ /** Read a built Next app's prerender manifest and scan it for ISR demand.
34
+ * Returns null when there's no manifest (not a Next build, or build skipped it).
35
+ * Never throws. */
36
+ export async function detectIsr(dir) {
37
+ // `next build` always writes `.next/prerender-manifest.json`; OpenNext keeps
38
+ // `.next` alongside its `.open-next` output. Check both, source-of-truth first.
39
+ for (const rel of ['.next/prerender-manifest.json', '.open-next/.next/prerender-manifest.json']) {
40
+ let text;
41
+ try {
42
+ text = await readFile(join(dir, rel), 'utf8');
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ return scanIsr(text);
48
+ }
49
+ return null;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",
package/skill/SKILL.md CHANGED
@@ -557,7 +557,7 @@ secret names (values set via CLI/MCP, never reach the publish payload).
557
557
  | `remove_function` | Strip function + secrets + crons; fall back to static |
558
558
  | `set_secret` / `list_secrets` / `remove_secret` | Manage CF Worker secrets |
559
559
  | `set_cron` / `list_crons` / `remove_cron` | Manage cron triggers |
560
- | `get_function_logs` | Deep-link to CF dashboard for live tail |
560
+ | `get_function_logs` | Read recent runtime logs (CF Observability, 7-day, newest-first) + a dashboard deep-link |
561
561
 
562
562
  ### CLI
563
563
 
@@ -567,11 +567,12 @@ shiply function deploy <slug> # alternative: upload worker.js with
567
567
  shiply function deploy <slug> --ts # uploads worker.ts (server-side compile)
568
568
  shiply secret set <slug> STRIPE_KEY sk_xxx # set secret value
569
569
  shiply cron set <slug> /api/daily "0 9 * * *"
570
+ shiply logs <slug> # recent worker logs (newest-first); --limit N --since MIN --json
570
571
  ```
571
572
 
572
573
  ### REST
573
574
 
574
- `POST/GET/DELETE /api/v1/sites/{slug}/function` · `POST/GET /api/v1/sites/{slug}/secrets` · `DELETE /api/v1/sites/{slug}/secrets/{name}` · `GET/POST/DELETE /api/v1/sites/{slug}/crons`
575
+ `POST/GET/DELETE /api/v1/sites/{slug}/function` · `POST/GET /api/v1/sites/{slug}/secrets` · `DELETE /api/v1/sites/{slug}/secrets/{name}` · `GET/POST/DELETE /api/v1/sites/{slug}/crons` · `GET /api/v1/sites/{slug}/logs?limit&since`
575
576
 
576
577
  ### Limits + plan
577
578