koguma 2.2.3 → 2.2.4

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/cli/dev-sync.ts CHANGED
@@ -233,8 +233,13 @@ function startMediaWatcher(opts: MediaWatcherOptions): { stop: () => void } {
233
233
  // Remove from local R2
234
234
  const { ensureWranglerConfig } = require('./config.ts');
235
235
  const configPath = ensureWranglerConfig(root);
236
+ const localBin = existsSync(
237
+ resolve(root, 'node_modules/.bin/wrangler')
238
+ )
239
+ ? resolve(root, 'node_modules/.bin/wrangler')
240
+ : 'bunx wrangler';
236
241
  run(
237
- `bunx wrangler r2 object delete ${key} --config ${configPath} --local`,
242
+ `${localBin} r2 object delete ${key} --config ${configPath} --local`,
238
243
  { cwd: root, silent: true }
239
244
  );
240
245
  } catch {
@@ -250,8 +255,13 @@ function startMediaWatcher(opts: MediaWatcherOptions): { stop: () => void } {
250
255
  const { run } = require('./exec.ts');
251
256
  const { ensureWranglerConfig } = require('./config.ts');
252
257
  const configPath = ensureWranglerConfig(root);
258
+ const localBin = existsSync(
259
+ resolve(root, 'node_modules/.bin/wrangler')
260
+ )
261
+ ? resolve(root, 'node_modules/.bin/wrangler')
262
+ : 'bunx wrangler';
253
263
  run(
254
- `bunx wrangler d1 execute ${dbName} --local --config ${configPath} --command "DELETE FROM assets WHERE id = '${id}'"`,
264
+ `${localBin} d1 execute ${dbName} --local --config ${configPath} --command "DELETE FROM assets WHERE id = '${id}'"`,
255
265
  { cwd: root, silent: true }
256
266
  );
257
267
  } catch {
package/cli/preflight.ts CHANGED
@@ -14,6 +14,12 @@ import { fail, warn, log, ANSI } from './log.ts';
14
14
  import { CONFIG_FILE, SITE_CONFIG_FILE, KOGUMA_DIR } from './constants.ts';
15
15
  import { runCapture } from './exec.ts';
16
16
 
17
+ /** Prefer locally installed wrangler; fall back to bunx. */
18
+ function wranglerBin(root: string): string {
19
+ const local = resolve(root, 'node_modules/.bin/wrangler');
20
+ return existsSync(local) ? local : 'bunx wrangler';
21
+ }
22
+
17
23
  export interface PreflightOptions {
18
24
  /** Command needs wrangler login (remote D1/R2 operations) */
19
25
  needsAuth?: boolean;
@@ -67,12 +73,13 @@ function requireSiteConfig(root: string): void {
67
73
  }
68
74
 
69
75
  function requireWranglerAuth(root: string): void {
76
+ const bin = wranglerBin(root);
70
77
  try {
71
- runCapture('bunx wrangler whoami', root);
78
+ runCapture(`${bin} whoami`, root);
72
79
  } catch {
73
80
  fail(
74
81
  `Not logged in to Cloudflare.\n` +
75
- ` Run ${ANSI.CYAN}bunx wrangler login${ANSI.RESET} first, ` +
82
+ ` Run ${ANSI.CYAN}${bin} login${ANSI.RESET} first, ` +
76
83
  `or ${ANSI.CYAN}koguma init${ANSI.RESET} which handles login for you.`
77
84
  );
78
85
  process.exit(1);
package/cli/wrangler.ts CHANGED
@@ -14,6 +14,21 @@ import { run, runCapture, runAsync } from './exec.ts';
14
14
  import { ok, warn, fail, log } from './log.ts';
15
15
  import { DB_DIR, MIGRATION_FILE } from './constants.ts';
16
16
 
17
+ // ── Wrangler binary resolution ─────────────────────────────────────
18
+
19
+ /**
20
+ * Resolve the wrangler binary to use.
21
+ *
22
+ * Prefers the locally installed copy (node_modules/.bin/wrangler) so the
23
+ * project controls its own wrangler version via package.json devDependencies.
24
+ * Falls back to `bunx wrangler` when no local binary is found (e.g. before
25
+ * `bun install` has been run or in CI with a fresh checkout).
26
+ */
27
+ function wranglerBin(root: string): string {
28
+ const localBin = resolve(root, 'node_modules/.bin/wrangler');
29
+ return existsSync(localBin) ? localBin : 'bunx wrangler';
30
+ }
31
+
17
32
  // ── Preflight checks ───────────────────────────────────────────────
18
33
 
19
34
  /**
@@ -21,11 +36,12 @@ import { DB_DIR, MIGRATION_FILE } from './constants.ts';
21
36
  * Gives a clear, actionable error message if not logged in.
22
37
  */
23
38
  export function checkWranglerAuth(root: string): void {
39
+ const bin = wranglerBin(root);
24
40
  try {
25
- runCapture('bunx wrangler whoami', root);
41
+ runCapture(`${bin} whoami`, root);
26
42
  } catch {
27
43
  fail(
28
- "Not logged in to Cloudflare. Run 'bunx wrangler login' first, " +
44
+ `Not logged in to Cloudflare. Run '${bin} login' first, ` +
29
45
  "or run 'koguma init' which handles login for you."
30
46
  );
31
47
  process.exit(1);
@@ -56,7 +72,7 @@ export function d1Execute(
56
72
  command: string
57
73
  ): void {
58
74
  run(
59
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --command "${wrapForShell(command)}"`,
75
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --command "${wrapForShell(command)}"`,
60
76
  { cwd: root, silent: true }
61
77
  );
62
78
  }
@@ -71,7 +87,7 @@ export function d1ExecuteFile(
71
87
  filePath: string
72
88
  ): void {
73
89
  run(
74
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${filePath}`,
90
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --file=${filePath}`,
75
91
  { cwd: root, silent: true }
76
92
  );
77
93
  }
@@ -86,7 +102,7 @@ export function d1Query(
86
102
  query: string
87
103
  ): Record<string, unknown>[] {
88
104
  const output = runCapture(
89
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --command "${query}" --json`,
105
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --command "${query}" --json`,
90
106
  root
91
107
  );
92
108
  const parsed = JSON.parse(output);
@@ -161,7 +177,7 @@ export async function applySchemaAsync(
161
177
  const sqlFile = resolve(dbDir, MIGRATION_FILE);
162
178
  writeFileSync(sqlFile, INIT_SQL);
163
179
  await runAsync(
164
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
180
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
165
181
  { cwd: root, silent: true }
166
182
  );
167
183
  }
@@ -185,7 +201,7 @@ export async function d1InsertBatchAsync(
185
201
  const statements = rows.map(row => buildInsertSql(table, row) + ';');
186
202
  writeFileSync(sqlFile, statements.join('\n'));
187
203
  await runAsync(
188
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
204
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
189
205
  { cwd: root, silent: true }
190
206
  );
191
207
  }
@@ -212,7 +228,7 @@ export async function d1ExecuteBatchSqlAsync(
212
228
  statements.map(s => (s.endsWith(';') ? s : s + ';')).join('\n')
213
229
  );
214
230
  await runAsync(
215
- `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
231
+ `${wranglerBin(root)} d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
216
232
  { cwd: root, silent: true }
217
233
  );
218
234
  }
@@ -227,7 +243,7 @@ export function r2PutLocal(
227
243
  filePath: string
228
244
  ): void {
229
245
  run(
230
- `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
246
+ `${wranglerBin(root)} r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
231
247
  { cwd: root, silent: true }
232
248
  );
233
249
  }
@@ -242,7 +258,7 @@ export async function r2PutLocalAsync(
242
258
  filePath: string
243
259
  ): Promise<void> {
244
260
  await runAsync(
245
- `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
261
+ `${wranglerBin(root)} r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
246
262
  { cwd: root, silent: true }
247
263
  );
248
264
  }
@@ -257,7 +273,7 @@ export function r2PutRemote(
257
273
  filePath: string
258
274
  ): void {
259
275
  run(
260
- `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath}`,
276
+ `${wranglerBin(root)} r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath}`,
261
277
  { cwd: root, silent: true }
262
278
  );
263
279
  }
@@ -272,7 +288,7 @@ export async function r2PutRemoteAsync(
272
288
  filePath: string
273
289
  ): Promise<void> {
274
290
  await runAsync(
275
- `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath}`,
291
+ `${wranglerBin(root)} r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath}`,
276
292
  { cwd: root, silent: true }
277
293
  );
278
294
  }
@@ -329,21 +345,49 @@ export function wranglerDev(
329
345
  const shouldSuppress = (line: string): boolean =>
330
346
  suppressPatterns.some(p => p.test(line));
331
347
 
332
- // ── In-place status line for transient events ──
348
+ // ── Animated spinner for reload status ──
349
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
333
350
  let reloadCount = 0;
334
351
  let hasStatusLine = false;
352
+ let spinnerFrame = 0;
353
+ let spinnerTimer: ReturnType<typeof setInterval> | null = null;
335
354
  const isTTY = process.stdout.isTTY;
336
355
 
337
- /** Write a transient status that overwrites itself on the next call */
338
- const writeStatus = (text: string) => {
339
- if (isTTY) {
340
- process.stdout.write(`\r\x1b[K ${text}`);
341
- hasStatusLine = true;
356
+ const drawStatus = (suffix = '') => {
357
+ if (!isTTY) return;
358
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]!;
359
+ process.stdout.write(`\r\x1b[K ${frame} reloading${suffix}`);
360
+ hasStatusLine = true;
361
+ };
362
+
363
+ const startSpinner = () => {
364
+ if (!isTTY) return;
365
+ if (spinnerTimer) return; // already running
366
+ drawStatus();
367
+ spinnerTimer = setInterval(() => {
368
+ spinnerFrame++;
369
+ drawStatus();
370
+ }, 80);
371
+ };
372
+
373
+ const stopSpinner = (finalMsg?: string) => {
374
+ if (spinnerTimer) {
375
+ clearInterval(spinnerTimer);
376
+ spinnerTimer = null;
377
+ }
378
+ if (!isTTY) return;
379
+ if (finalMsg) {
380
+ process.stdout.write(`\r\x1b[K ✓ ${finalMsg}\n`);
381
+ hasStatusLine = false;
342
382
  }
343
383
  };
344
384
 
345
385
  /** Clear the status line before printing a permanent line */
346
386
  const clearStatus = () => {
387
+ if (spinnerTimer) {
388
+ clearInterval(spinnerTimer);
389
+ spinnerTimer = null;
390
+ }
347
391
  if (hasStatusLine && isTTY) {
348
392
  process.stdout.write('\r\x1b[K');
349
393
  hasStatusLine = false;
@@ -360,19 +404,20 @@ export function wranglerDev(
360
404
  if (trimmed.includes('Ready on http')) {
361
405
  const urlMatch = trimmed.match(/(https?:\/\/[^\s]+)/);
362
406
  const url = urlMatch?.[1] ?? 'http://localhost:8787';
407
+ stopSpinner();
363
408
  clearStatus();
364
409
  ok(`Server ready → ${url}`);
365
410
  continue;
366
411
  }
367
412
 
368
- // Reload events → transient status line (overwrites in place)
413
+ // Reload events → animated spinner (overwrites in place)
369
414
  if (/Reloading local server/.test(trimmed)) {
370
415
  reloadCount++;
371
- writeStatus(`reload #${reloadCount}`);
416
+ startSpinner();
372
417
  continue;
373
418
  }
374
419
  if (/Local server updated/.test(trimmed)) {
375
- writeStatus(`reload #${reloadCount} ✓`);
420
+ stopSpinner(`reload #${reloadCount}`);
376
421
  continue;
377
422
  }
378
423
 
@@ -411,7 +456,7 @@ export function wranglerDev(
411
456
  * Deploy via wrangler.
412
457
  */
413
458
  export function wranglerDeploy(root: string): void {
414
- run(`bunx wrangler deploy ${configFlag(root)}`, { cwd: root });
459
+ run(`${wranglerBin(root)} deploy ${configFlag(root)}`, { cwd: root });
415
460
  }
416
461
 
417
462
  // ── D1 / R2 resource creation ──────────────────────────────────────
@@ -421,7 +466,7 @@ export function wranglerDeploy(root: string): void {
421
466
  */
422
467
  export function createD1Database(root: string, dbName: string): string | null {
423
468
  try {
424
- const output = runCapture(`bunx wrangler d1 create ${dbName}`, root);
469
+ const output = runCapture(`${wranglerBin(root)} d1 create ${dbName}`, root);
425
470
  const idMatch = output.match(/database_id\s*=\s*"([^"]+)"/);
426
471
  return idMatch?.[1] ?? null;
427
472
  } catch {
@@ -434,9 +479,9 @@ export function createD1Database(root: string, dbName: string): string | null {
434
479
  */
435
480
  export function ensureR2Bucket(root: string, bucketName: string): boolean {
436
481
  try {
437
- const buckets = runCapture('bunx wrangler r2 bucket list', root);
482
+ const buckets = runCapture(`${wranglerBin(root)} r2 bucket list`, root);
438
483
  if (buckets.includes(bucketName)) return true;
439
- runCapture(`bunx wrangler r2 bucket create ${bucketName}`, root);
484
+ runCapture(`${wranglerBin(root)} r2 bucket create ${bucketName}`, root);
440
485
  return true;
441
486
  } catch {
442
487
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koguma",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "🐻 A little CMS with big heart — schema-driven, runs on Cloudflare's free tier",
5
5
  "type": "module",
6
6
  "license": "MIT",