naro-cli 0.24.0 → 0.26.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.
@@ -7,7 +7,7 @@ import {
7
7
  import { existsSync as existsSync10 } from "fs";
8
8
  import { mkdtemp as mkdtemp2, readFile as readFile15, rm as rm5 } from "fs/promises";
9
9
  import { tmpdir as tmpdir2 } from "os";
10
- import { basename as basename5, dirname as dirname10, join as join32, resolve as resolve14 } from "path";
10
+ import { basename as basename5, dirname as dirname10, join as join31, resolve as resolve14 } from "path";
11
11
  import { fileURLToPath as fileURLToPath4 } from "url";
12
12
 
13
13
  // ../../packages/build/src/artifact-upload.ts
@@ -289,7 +289,7 @@ function hashAsset(data, contentType2) {
289
289
  // ../../packages/build/src/local-build-executor.ts
290
290
  import { access as access8, mkdtemp, readFile as readFile13, rm as rm3 } from "fs/promises";
291
291
  import { tmpdir } from "os";
292
- import { dirname as dirname4, join as join18, resolve as resolve7 } from "path";
292
+ import { dirname as dirname4, join as join17, resolve as resolve7 } from "path";
293
293
 
294
294
  // ../../packages/core/src/route-patterns.ts
295
295
  var ROUTE_PATTERN = /^\/[A-Za-z0-9\-._~%!$&()*+,;=:@/[\]]*$/;
@@ -310,6 +310,1925 @@ function sanitizeRoutes(value) {
310
310
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
311
311
  var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
312
312
 
313
+ // ../../packages/core/src/brand-case.ts
314
+ var CLI_WORDS = "analytics|auth|backend|cancel|confirm|db|deploy|deployments|domains|env|environments|filters|functions|gen|git|insights|inspect|link|login|logout|logs|migration|policy|project|projects|promote|protection|realtime|secrets|storage|token|unlink|usage|whoami|dev|init|help|version|rollback";
315
+ var IDENTIFIER = new RegExp(
316
+ `(?:[@_\\-/.:$]naro\\b|\\bnaro[_\\-/:]|\\bnaro\\.\\w|\\bnaro (?:${CLI_WORDS}) (?:[a-z][a-z-]*|--)|\\bnaro --|\\bnaro (?:\u2026|\\.\\.\\.)|\\bnaro [a-z][a-z-]*:)`,
317
+ "g"
318
+ );
319
+
320
+ // ../../packages/core/src/capabilities.ts
321
+ var REASON_CODES = {
322
+ "secret-value": {
323
+ clients: ["mcp"],
324
+ why: "\uC2DC\uD06C\uB9BF \uAC12\uC744 \uBC1B\uAC70\uB098 \uB3CC\uB824\uC900\uB2E4. \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4 (apps/mcp/src/function-tools.ts:34)."
325
+ },
326
+ irreversible: {
327
+ clients: ["mcp"],
328
+ why: "\uB418\uB3CC\uB9B4 \uC218 \uC5C6\uACE0 \uD655\uC778\uC73C\uB85C\uB9CC \uB9C9\uC744 \uC218 \uC788\uB2E4 (project_cli_mcp_backlog #6)."
329
+ },
330
+ "privilege-escalation": {
331
+ clients: ["mcp"],
332
+ why: "\uAD8C\uD55C\xB7\uC5ED\uD560\xB7\uBA64\uBC84\uC2ED\uC744 \uBC14\uAFBC\uB2E4."
333
+ },
334
+ "payment-flow": {
335
+ clients: ["cli", "mcp"],
336
+ why: "\uCE74\uB4DC \uC815\uBCF4\uAC00 \uC9C0\uB098\uAC04\uB2E4. \uD638\uC2A4\uD305 \uACB0\uC81C\uCC3D\uC774 \uBC1B\uB294\uB2E4."
337
+ },
338
+ "browser-required": {
339
+ clients: ["cli", "mcp"],
340
+ why: "OAuth \uB9AC\uB2E4\uC774\uB809\uD2B8\uAC00 \uD544\uC694\uD558\uB2E4."
341
+ },
342
+ "local-filesystem": {
343
+ clients: ["ui"],
344
+ why: "\uB85C\uCEEC \uD30C\uC77C\uC744 \uC77D\uAC70\uB098 \uC4F4\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
345
+ },
346
+ "ui-affordance": {
347
+ clients: ["cli", "mcp"],
348
+ why: "\uD654\uBA74 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4."
349
+ },
350
+ "agent-native": {
351
+ clients: ["mcp"],
352
+ // "naro CLI로 할 수 있다"는 사유가 아니다 — 그러면 CLI에 있는 모든 기능을
353
+ // 이 코드로 뺄 수 있다(PR #423 리뷰). 컨트롤 플레인에 닿는 일이면 planned다.
354
+ why: "\uC774 \uCEF4\uD4E8\uD130\uC758 \uD30C\uC77C\uB9CC \uC4F0\uB294 \uC77C\uC774\uB2E4. \uC5D0\uC774\uC804\uD2B8\uB294 \uC81C \uB3C4\uAD6C\uB85C \uD30C\uC77C\uC744 \uC4F4\uB2E4 \u2014 MCP \uD234\uB85C \uAC10\uC2F8\uBA74 \uC4F0\uAE30 \uACBD\uB85C \uAC80\uC0AC\uB9CC \uB294\uB2E4."
355
+ },
356
+ "user-session": {
357
+ clients: ["mcp"],
358
+ why: "\uC0AC\uC6A9\uC790 \uC790\uC2E0\uC758 \uB85C\uADF8\uC778 \uC138\uC158\uC744 \uBC14\uAFBC\uB2E4. \uC0AC\uB78C\uC774 \uD130\uBBF8\uB110\uC5D0\uC11C \uD55C\uB2E4."
359
+ },
360
+ "dashboard-itself": {
361
+ clients: ["ui"],
362
+ why: "\uB300\uC2DC\uBCF4\uB4DC\uB85C \uAC00\uB294 \uAE38\uC744 \uC54C\uB824 \uC8FC\uB294 \uAE30\uB2A5\uC774\uB2E4. \uB300\uC2DC\uBCF4\uB4DC \uC548\uC5D0\uC11C\uB294 \uB73B\uC774 \uC5C6\uB2E4."
363
+ }
364
+ };
365
+ var CAPABILITIES = [
366
+ {
367
+ id: "auth-whoami",
368
+ domain: "auth",
369
+ title: "\uB0B4 \uACC4\uC815 \uD655\uC778",
370
+ ui: { kind: "built" },
371
+ cli: { kind: "built", commands: ["naro whoami"] },
372
+ mcp: { kind: "built", tools: ["whoami"] }
373
+ },
374
+ {
375
+ id: "auth-ci-token",
376
+ domain: "auth",
377
+ title: "CI \uD1A0\uD070 \uBC1C\uAE09",
378
+ ui: {
379
+ kind: "excluded",
380
+ reason: "local-filesystem",
381
+ note: "naro login\uC774 \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uB0A8\uAE34 \uC138\uC158(~/.naro/credentials.json)\uC5D0\uC11C \uD1A0\uD070\uC744 \uAEBC\uB0B8\uB2E4. \uBE0C\uB77C\uC6B0\uC800 \uC138\uC158\uC5D0\uB294 \uADF8 \uD30C\uC77C\uC774 \uC5C6\uB2E4."
382
+ },
383
+ cli: { kind: "built", commands: ["naro token"] },
384
+ mcp: {
385
+ kind: "excluded",
386
+ reason: "secret-value",
387
+ note: "\uD1A0\uD070 \uC790\uCCB4\uAC00 \uC2DC\uD06C\uB9BF\uC774\uB77C \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
388
+ }
389
+ },
390
+ {
391
+ id: "auth-login",
392
+ domain: "auth",
393
+ title: "\uB85C\uADF8\uC778",
394
+ dashboard: "/login",
395
+ ui: { kind: "built" },
396
+ cli: { kind: "built", commands: ["naro login"] },
397
+ mcp: {
398
+ kind: "excluded",
399
+ reason: "browser-required",
400
+ note: "apps/mcp/src/tools.ts \u2014 \uC774 \uC11C\uBC84\uB294 \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uB2E4. \uC0AC\uC6A9\uC790\uAC00 \uD130\uBBF8\uB110\uC5D0\uC11C naro login\uC744 \uB3CC\uB9AC\uAC8C \uD55C\uB2E4."
401
+ }
402
+ },
403
+ {
404
+ id: "auth-logout",
405
+ domain: "auth",
406
+ title: "\uB85C\uADF8\uC544\uC6C3",
407
+ dashboard: null,
408
+ ui: { kind: "built" },
409
+ cli: { kind: "built", commands: ["naro logout"] },
410
+ mcp: {
411
+ kind: "excluded",
412
+ reason: "user-session",
413
+ note: "naro login \uC138\uC158\uC740 \uC0AC\uB78C\uC774 \uB2E4\uB8EC\uB2E4. 2026-11-03 \uACB0\uC815."
414
+ }
415
+ },
416
+ {
417
+ id: "team-list",
418
+ domain: "teams",
419
+ title: "\uD300 \uBAA9\uB85D",
420
+ routes: ["GET teams"],
421
+ ui: { kind: "built" },
422
+ cli: { kind: "built", commands: ["naro teams"] },
423
+ mcp: { kind: "built", tools: ["list_teams"] }
424
+ },
425
+ {
426
+ id: "team-member-list",
427
+ domain: "teams",
428
+ title: "\uBA64\uBC84 \uBAA9\uB85D",
429
+ routes: ["GET teams/[teamSlug]/members"],
430
+ ui: { kind: "built" },
431
+ cli: { kind: "built", commands: ["naro team members"] },
432
+ mcp: { kind: "built", tools: ["list_team_members"] }
433
+ },
434
+ {
435
+ id: "team-create",
436
+ domain: "teams",
437
+ title: "\uD300 \uC0DD\uC131",
438
+ routes: ["POST teams"],
439
+ ui: { kind: "built" },
440
+ cli: { kind: "built", commands: ["naro team create"] },
441
+ mcp: { kind: "built", tools: ["create_team"] }
442
+ },
443
+ {
444
+ id: "team-update",
445
+ domain: "teams",
446
+ title: "\uD300 \uC124\uC815 \uBCC0\uACBD",
447
+ routes: ["PATCH teams/[teamSlug]"],
448
+ ui: { kind: "built" },
449
+ cli: { kind: "built", commands: ["naro team set"] },
450
+ mcp: { kind: "built", tools: ["update_team"] }
451
+ },
452
+ {
453
+ id: "team-delete",
454
+ domain: "teams",
455
+ title: "\uD300 \uC0AD\uC81C",
456
+ routes: ["DELETE teams/[teamSlug]"],
457
+ dashboard: "/[teamSlug]/settings",
458
+ ui: { kind: "built" },
459
+ cli: { kind: "built", commands: ["naro team delete"] },
460
+ mcp: {
461
+ kind: "excluded",
462
+ reason: "irreversible",
463
+ note: "\uD300\uC758 \uD504\uB85C\uC81D\uD2B8\xB7\uB370\uC774\uD130\uAC00 \uD568\uAED8 \uC0AC\uB77C\uC9C4\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
464
+ }
465
+ },
466
+ {
467
+ id: "team-member-update",
468
+ domain: "teams",
469
+ title: "\uBA64\uBC84 \uC5ED\uD560 \uBCC0\uACBD",
470
+ routes: ["PATCH teams/[teamSlug]/members/[userId]"],
471
+ dashboard: "/[teamSlug]/settings",
472
+ ui: { kind: "built" },
473
+ cli: { kind: "built", commands: ["naro team members set"] },
474
+ mcp: {
475
+ kind: "excluded",
476
+ reason: "privilege-escalation",
477
+ note: "\uC5ED\uD560\uC744 \uBC14\uAFBC\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
478
+ }
479
+ },
480
+ {
481
+ id: "team-member-remove",
482
+ domain: "teams",
483
+ title: "\uBA64\uBC84 \uC81C\uAC70",
484
+ routes: ["DELETE teams/[teamSlug]/members/[userId]"],
485
+ dashboard: "/[teamSlug]/settings",
486
+ ui: { kind: "built" },
487
+ cli: { kind: "built", commands: ["naro team members remove"] },
488
+ mcp: {
489
+ kind: "excluded",
490
+ reason: "privilege-escalation",
491
+ note: "\uBA64\uBC84\uC2ED\uC744 \uBC14\uAFBC\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
492
+ }
493
+ },
494
+ {
495
+ id: "team-invite-list",
496
+ domain: "teams",
497
+ title: "\uCD08\uB300 \uBAA9\uB85D",
498
+ routes: ["GET teams/[teamSlug]/invites"],
499
+ ui: { kind: "built" },
500
+ cli: { kind: "built", commands: ["naro team invites"] },
501
+ mcp: { kind: "built", tools: ["list_team_invites"] }
502
+ },
503
+ {
504
+ id: "team-invite-create",
505
+ domain: "teams",
506
+ title: "\uCD08\uB300 \uBCF4\uB0B4\uAE30",
507
+ routes: ["POST teams/[teamSlug]/invites"],
508
+ dashboard: "/[teamSlug]/settings",
509
+ ui: { kind: "built" },
510
+ cli: { kind: "built", commands: ["naro team invites create"] },
511
+ mcp: {
512
+ kind: "excluded",
513
+ reason: "privilege-escalation",
514
+ note: "\uD300\uC5D0 \uC0AC\uB78C\uC744 \uB4E4\uC778\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
515
+ }
516
+ },
517
+ {
518
+ id: "team-invite-revoke",
519
+ domain: "teams",
520
+ title: "\uCD08\uB300 \uCDE8\uC18C",
521
+ routes: ["DELETE teams/[teamSlug]/invites/[inviteId]"],
522
+ ui: { kind: "built" },
523
+ cli: { kind: "built", commands: ["naro team invites revoke"] },
524
+ mcp: { kind: "built", tools: ["revoke_team_invite"] }
525
+ },
526
+ {
527
+ id: "team-usage",
528
+ domain: "teams",
529
+ title: "\uD300 \uC0AC\uC6A9\uB7C9",
530
+ routes: ["GET teams/[teamSlug]/usage"],
531
+ ui: { kind: "built" },
532
+ cli: { kind: "built", commands: ["naro usage"] },
533
+ mcp: { kind: "built", tools: ["get_team_usage"] }
534
+ },
535
+ {
536
+ id: "billing-view",
537
+ domain: "billing",
538
+ title: "\uD50C\uB79C\xB7\uAD6C\uB3C5 \uC870\uD68C",
539
+ routes: ["GET teams/[teamSlug]/billing"],
540
+ ui: { kind: "built" },
541
+ cli: { kind: "built", commands: ["naro billing"] },
542
+ mcp: { kind: "built", tools: ["get_team_billing"] }
543
+ },
544
+ {
545
+ id: "billing-checkout",
546
+ domain: "billing",
547
+ title: "\uCCB4\uD06C\uC544\uC6C3",
548
+ routes: ["POST teams/[teamSlug]/billing/checkout"],
549
+ dashboard: "/[teamSlug]/usage",
550
+ ui: { kind: "built" },
551
+ cli: {
552
+ kind: "excluded",
553
+ reason: "payment-flow",
554
+ note: "\uCE74\uB4DC \uC815\uBCF4\uB294 Paddle\uC758 \uD638\uC2A4\uD305 \uACB0\uC81C\uCC3D\uC774 \uBC1B\uB294\uB2E4."
555
+ },
556
+ mcp: {
557
+ kind: "excluded",
558
+ reason: "payment-flow",
559
+ note: "\uCE74\uB4DC \uC815\uBCF4\uB294 Paddle\uC758 \uD638\uC2A4\uD305 \uACB0\uC81C\uCC3D\uC774 \uBC1B\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
560
+ }
561
+ },
562
+ {
563
+ id: "billing-portal",
564
+ domain: "billing",
565
+ title: "\uACB0\uC81C \uD3EC\uD138",
566
+ routes: ["POST teams/[teamSlug]/billing/portal"],
567
+ dashboard: "/[teamSlug]/usage",
568
+ ui: { kind: "built" },
569
+ cli: {
570
+ kind: "excluded",
571
+ reason: "payment-flow",
572
+ note: "\uACB0\uC81C \uC218\uB2E8 \uBCC0\uACBD\uC740 Paddle \uD3EC\uD138\uC5D0\uC11C \uD55C\uB2E4."
573
+ },
574
+ mcp: {
575
+ kind: "excluded",
576
+ reason: "payment-flow",
577
+ note: "\uACB0\uC81C \uC218\uB2E8 \uBCC0\uACBD\uC740 Paddle \uD3EC\uD138\uC5D0\uC11C \uD55C\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
578
+ }
579
+ },
580
+ {
581
+ id: "project-list",
582
+ domain: "projects",
583
+ title: "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D",
584
+ routes: ["GET projects"],
585
+ ui: { kind: "built" },
586
+ cli: { kind: "built", commands: ["naro projects"] },
587
+ mcp: { kind: "built", tools: ["list_projects"] }
588
+ },
589
+ {
590
+ id: "project-create",
591
+ domain: "projects",
592
+ title: "\uD504\uB85C\uC81D\uD2B8 \uC0DD\uC131",
593
+ routes: ["POST projects"],
594
+ ui: { kind: "built" },
595
+ // naro deploy가 링크가 없을 때 만든다(POST cli/deployments).
596
+ cli: { kind: "built" },
597
+ // deploy가 링크가 없을 때 만든다(POST cli/deployments).
598
+ mcp: { kind: "built" }
599
+ },
600
+ {
601
+ id: "project-get",
602
+ domain: "projects",
603
+ title: "\uD504\uB85C\uC81D\uD2B8 \uC870\uD68C",
604
+ routes: ["GET projects/[projectId]"],
605
+ ui: { kind: "built" },
606
+ cli: { kind: "built", commands: ["naro project inspect"] },
607
+ mcp: { kind: "built", tools: ["get_project"] }
608
+ },
609
+ {
610
+ id: "project-link",
611
+ domain: "projects",
612
+ title: "\uB514\uB809\uD130\uB9AC\uB97C \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9C1\uD06C",
613
+ ui: {
614
+ kind: "excluded",
615
+ reason: "local-filesystem",
616
+ note: ".naro/project.json\uC5D0 \uC4F4\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
617
+ },
618
+ cli: { kind: "built", commands: ["naro link", "naro init"] },
619
+ mcp: { kind: "built", tools: ["link_project"] }
620
+ },
621
+ {
622
+ id: "project-unlink",
623
+ domain: "projects",
624
+ title: "\uB9C1\uD06C \uD574\uC81C",
625
+ ui: {
626
+ kind: "excluded",
627
+ reason: "local-filesystem",
628
+ note: ".naro/\uB97C \uC9C0\uC6B4\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
629
+ },
630
+ cli: { kind: "built", commands: ["naro unlink"] },
631
+ mcp: {
632
+ kind: "excluded",
633
+ reason: "agent-native",
634
+ note: ".naro/\uB97C \uC9C0\uC6B0\uB294 \uC77C\uC774\uB2E4. \uC9DD\uC778 link_project\uAC00 \uD234\uC778 \uAC83\uC740 \uD504\uB85C\uC81D\uD2B8\uB97C \uACE0\uB974\uB824\uBA74 \uCEE8\uD2B8\uB864 \uD50C\uB808\uC778\uC744 \uC77D\uC5B4\uC57C \uD574\uC11C\uB2E4 \u2014 unlink\uB294 \uD30C\uC77C\uB9CC \uC9C0\uC6B4\uB2E4. 2026-11-03 \uACB0\uC815."
635
+ }
636
+ },
637
+ {
638
+ id: "project-update",
639
+ domain: "projects",
640
+ title: "\uD504\uB85C\uC81D\uD2B8 \uC124\uC815 \uBCC0\uACBD(\uD504\uB808\uC784\uC6CC\uD06C\xB7\uC11C\uBE0C\uB3C4\uBA54\uC778)",
641
+ routes: ["PATCH projects/[projectId]"],
642
+ ui: { kind: "built" },
643
+ cli: { kind: "built", commands: ["naro project set"] },
644
+ // 서브도메인은 빼고(PR #433 리뷰): 옛 이름이 바로 비어 누구나 가져갈 수 있어
645
+ // 북마크·OAuth 리다이렉트가 남의 앱으로 간다. 대시보드와 CLI(먼저 묻는다)만.
646
+ mcp: { kind: "built", tools: ["update_project"] }
647
+ },
648
+ {
649
+ id: "project-delete",
650
+ domain: "projects",
651
+ title: "\uD504\uB85C\uC81D\uD2B8 \uC0AD\uC81C",
652
+ routes: ["DELETE projects/[projectId]"],
653
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
654
+ ui: { kind: "built" },
655
+ cli: { kind: "built", commands: ["naro project delete"] },
656
+ mcp: {
657
+ kind: "excluded",
658
+ reason: "irreversible",
659
+ note: "\uC6CC\uCEE4\xB7D1\xB7R2\xB7\uB3C4\uBA54\uC778\xB7\uBC30\uD3EC \uC774\uB825\uC774 cascade\uB85C \uB0A0\uC544\uAC04\uB2E4. \uC5D0\uC774\uC804\uD2B8\uB294 \uD504\uB86C\uD504\uD2B8 \uC778\uC81D\uC158\uC5D0 \uB178\uCD9C\uB418\uBBC0\uB85C \uD655\uC778\uC73C\uB85C \uB9C9\uC744 \uC218 \uC5C6\uB2E4(project_cli_mcp_backlog #6)."
660
+ }
661
+ },
662
+ {
663
+ id: "project-subdomain-check",
664
+ domain: "projects",
665
+ title: "\uC11C\uBE0C\uB3C4\uBA54\uC778 \uC0AC\uC6A9 \uAC00\uB2A5 \uC5EC\uBD80",
666
+ routes: ["GET subdomains/availability"],
667
+ ui: { kind: "built" },
668
+ cli: { kind: "built", commands: ["naro project check"] },
669
+ mcp: { kind: "built", tools: ["check_subdomain"] }
670
+ },
671
+ {
672
+ id: "project-access-list",
673
+ domain: "projects",
674
+ title: "\uBA64\uBC84\uBCC4 \uD504\uB85C\uC81D\uD2B8 \uC811\uADFC \uC870\uD68C",
675
+ routes: ["GET projects/[projectId]/access"],
676
+ ui: { kind: "built" },
677
+ cli: { kind: "built", commands: ["naro project access"] },
678
+ mcp: { kind: "built", tools: ["list_project_access"] }
679
+ },
680
+ {
681
+ id: "project-access-grant",
682
+ domain: "projects",
683
+ title: "\uBA64\uBC84\uC5D0\uAC8C \uD504\uB85C\uC81D\uD2B8 \uC811\uADFC \uBD80\uC5EC",
684
+ routes: ["PUT projects/[projectId]/access"],
685
+ dashboard: "/[teamSlug]/projects/[projectSlug]/access",
686
+ ui: { kind: "built" },
687
+ cli: { kind: "built", commands: ["naro project access grant"] },
688
+ mcp: {
689
+ kind: "excluded",
690
+ reason: "privilege-escalation",
691
+ note: "\uBA64\uBC84\uC758 \uD504\uB85C\uC81D\uD2B8 \uAD8C\uD55C\uC744 \uBC14\uAFBC\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
692
+ }
693
+ },
694
+ {
695
+ id: "project-access-revoke",
696
+ domain: "projects",
697
+ title: "\uBA64\uBC84\uC758 \uD504\uB85C\uC81D\uD2B8 \uC811\uADFC \uD68C\uC218",
698
+ routes: ["DELETE projects/[projectId]/access"],
699
+ dashboard: "/[teamSlug]/projects/[projectSlug]/access",
700
+ ui: { kind: "built" },
701
+ cli: { kind: "built", commands: ["naro project access revoke"] },
702
+ mcp: {
703
+ kind: "excluded",
704
+ reason: "privilege-escalation",
705
+ note: "\uBA64\uBC84\uC758 \uD504\uB85C\uC81D\uD2B8 \uAD8C\uD55C\uC744 \uBC14\uAFBC\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
706
+ }
707
+ },
708
+ {
709
+ id: "deployment-list",
710
+ domain: "deployments",
711
+ title: "\uBC30\uD3EC \uBAA9\uB85D",
712
+ routes: ["GET projects/[projectId]/deployments"],
713
+ ui: { kind: "built" },
714
+ cli: { kind: "built", commands: ["naro deployments"] },
715
+ mcp: { kind: "built", tools: ["list_deployments"] }
716
+ },
717
+ {
718
+ id: "deployment-create",
719
+ domain: "deployments",
720
+ title: "\uBE0C\uB79C\uCE58\uC5D0\uC11C \uC0C8\uB85C \uBC30\uD3EC",
721
+ routes: ["POST projects/[projectId]/deployments"],
722
+ ui: { kind: "built" },
723
+ cli: { kind: "built", commands: ["naro deployments create"] },
724
+ mcp: { kind: "built", tools: ["deploy_from_git"] }
725
+ },
726
+ {
727
+ id: "deployment-local",
728
+ domain: "deployments",
729
+ title: "\uB85C\uCEEC\uC5D0\uC11C \uBE4C\uB4DC\uD574 \uBC30\uD3EC",
730
+ routes: ["POST cli/deployments"],
731
+ ui: {
732
+ kind: "excluded",
733
+ reason: "local-filesystem",
734
+ note: "\uB85C\uCEEC \uC791\uC5C5 \uD2B8\uB9AC\uC5D0\uC11C \uBE4C\uB4DC\uD55C \uC0B0\uCD9C\uBB3C\uC744 \uC62C\uB9B0\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uB294 \uBE4C\uB4DC\uD560 \uC791\uC5C5 \uD2B8\uB9AC\uAC00 \uC5C6\uB2E4."
735
+ },
736
+ cli: { kind: "built", commands: ["naro deploy"] },
737
+ mcp: { kind: "built", tools: ["deploy"] }
738
+ },
739
+ {
740
+ id: "deployment-get",
741
+ domain: "deployments",
742
+ title: "\uBC30\uD3EC \uC0C1\uC138",
743
+ routes: ["GET deployments/[deploymentId]"],
744
+ ui: { kind: "built" },
745
+ cli: { kind: "built", commands: ["naro inspect"] },
746
+ mcp: { kind: "built", tools: ["get_deployment", "get_build_logs"] }
747
+ },
748
+ {
749
+ id: "deployment-cancel",
750
+ domain: "deployments",
751
+ title: "\uBE4C\uB4DC \uCDE8\uC18C",
752
+ routes: ["POST projects/[projectId]/deployments/[deploymentId]/cancel"],
753
+ ui: { kind: "built" },
754
+ cli: { kind: "built", commands: ["naro cancel"] },
755
+ mcp: { kind: "built", tools: ["cancel_deployment"] }
756
+ },
757
+ {
758
+ id: "deployment-promote-preview",
759
+ domain: "deployments",
760
+ title: "\uC2B9\uACA9 \uBBF8\uB9AC\uBCF4\uAE30",
761
+ routes: ["GET projects/[projectId]/deployments/[deploymentId]/promote"],
762
+ ui: { kind: "built" },
763
+ // naro promote가 묻기 전에 무엇을 할지 보여 준다(client.deployments.promoteCheck).
764
+ cli: { kind: "built" },
765
+ mcp: { kind: "built", tools: ["check_promotion"] }
766
+ },
767
+ {
768
+ id: "deployment-promote",
769
+ domain: "deployments",
770
+ title: "\uD504\uB85C\uB355\uC158\xB7\uD658\uACBD\uC73C\uB85C \uC2B9\uACA9(\uB864\uBC31 \uD3EC\uD568)",
771
+ routes: ["POST projects/[projectId]/deployments/[deploymentId]/promote"],
772
+ ui: { kind: "built" },
773
+ cli: { kind: "built", commands: ["naro promote", "naro rollback"] },
774
+ mcp: {
775
+ kind: "built",
776
+ tools: ["promote_deployment", "rollback_deployment"]
777
+ }
778
+ },
779
+ {
780
+ id: "git-app-install",
781
+ domain: "git",
782
+ title: "GitHub App \uC124\uCE58",
783
+ routes: ["GET github/authorize", "GET github/install", "GET github/setup"],
784
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
785
+ ui: { kind: "built" },
786
+ cli: {
787
+ kind: "excluded",
788
+ reason: "browser-required",
789
+ note: "GitHub App \uC124\uCE58\uB294 OAuth \uB9AC\uB2E4\uC774\uB809\uD2B8\uB85C \uB05D\uB09C\uB2E4. naro git connect\uAC00 \uC124\uCE58 URL\uC744 \uCC0D\uC5B4 \uC900\uB2E4."
790
+ },
791
+ mcp: {
792
+ kind: "excluded",
793
+ reason: "browser-required",
794
+ note: "apps/mcp/src/tools.ts\uC758 connect_git_repository \u2014 \uC124\uCE58 URL\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC8FC\uACE0 \uBE0C\uB77C\uC6B0\uC800\uB294 \uC9C1\uC811 \uC5F4\uC9C0 \uC54A\uB294\uB2E4."
795
+ }
796
+ },
797
+ {
798
+ id: "git-repository-list",
799
+ domain: "git",
800
+ title: "GitHub \uC800\uC7A5\uC18C \uBAA9\uB85D",
801
+ routes: ["GET github/repositories"],
802
+ ui: { kind: "built" },
803
+ // naro git connect가 저장소를 고를 때 읽는다(client.github.listRepositories).
804
+ cli: { kind: "built" },
805
+ mcp: { kind: "built", tools: ["list_git_repositories"] }
806
+ },
807
+ {
808
+ id: "git-connect",
809
+ domain: "git",
810
+ title: "\uC800\uC7A5\uC18C \uC5F0\uACB0",
811
+ routes: ["POST projects/[projectId]/git"],
812
+ ui: { kind: "built" },
813
+ cli: { kind: "built", commands: ["naro git connect"] },
814
+ mcp: { kind: "built", tools: ["connect_git_repository"] }
815
+ },
816
+ {
817
+ id: "git-disconnect",
818
+ domain: "git",
819
+ title: "\uC800\uC7A5\uC18C \uC5F0\uACB0 \uD574\uC81C",
820
+ routes: ["DELETE projects/[projectId]/git"],
821
+ ui: { kind: "built" },
822
+ cli: { kind: "built", commands: ["naro git disconnect"] },
823
+ mcp: { kind: "built", tools: ["disconnect_git_repository"] }
824
+ },
825
+ {
826
+ id: "env-list",
827
+ domain: "env",
828
+ title: "\uD658\uACBD\uBCC0\uC218 \uBAA9\uB85D",
829
+ routes: ["GET projects/[projectId]/env"],
830
+ ui: { kind: "built" },
831
+ cli: { kind: "built", commands: ["naro env list"] },
832
+ mcp: { kind: "built", tools: ["list_environment_variables"] }
833
+ },
834
+ {
835
+ id: "env-set",
836
+ domain: "env",
837
+ title: "\uD658\uACBD\uBCC0\uC218 \uCD94\uAC00\xB7\uC218\uC815",
838
+ routes: ["POST projects/[projectId]/env"],
839
+ ui: { kind: "built" },
840
+ cli: { kind: "built", commands: ["naro env add", "naro env import"] },
841
+ mcp: { kind: "built", tools: ["set_environment_variable"] }
842
+ },
843
+ {
844
+ id: "env-remove",
845
+ domain: "env",
846
+ title: "\uD658\uACBD\uBCC0\uC218 \uC81C\uAC70",
847
+ routes: ["DELETE projects/[projectId]/env/[envVarId]"],
848
+ ui: { kind: "built" },
849
+ cli: { kind: "built", commands: ["naro env remove"] },
850
+ mcp: { kind: "built", tools: ["remove_environment_variable"] }
851
+ },
852
+ // 앱 메일의 Resend 연결(spec 2026-11-04 resend-email-integration). 연결은
853
+ // env 두 행이라 env 도메인에 둔다. CLI·MCP는 그 스펙의 PR 2다.
854
+ {
855
+ id: "email-status",
856
+ domain: "env",
857
+ title: "Resend \uC5F0\uACB0 \uBCF4\uAE30",
858
+ routes: ["GET projects/[projectId]/email"],
859
+ ui: { kind: "built" },
860
+ cli: { kind: "planned", note: "naro email status \u2014 \uC2A4\uD399 \xA713 PR 2." },
861
+ mcp: { kind: "planned", note: "get_email \u2014 \uC2A4\uD399 \xA713 PR 2." }
862
+ },
863
+ {
864
+ id: "email-connect",
865
+ domain: "env",
866
+ title: "Resend \uC5F0\uACB0\xB7\uBC14\uAFB8\uAE30",
867
+ routes: ["PUT projects/[projectId]/email"],
868
+ dashboard: "/[teamSlug]/projects/[projectSlug]/email",
869
+ ui: { kind: "built" },
870
+ cli: {
871
+ kind: "planned",
872
+ note: "naro email connect --api-key-stdin \u2014 \uC2A4\uD399 \xA713 PR 2."
873
+ },
874
+ mcp: {
875
+ kind: "excluded",
876
+ reason: "secret-value",
877
+ note: "Resend API \uD0A4\uB97C \uBC1B\uB294\uB2E4. \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4 \u2014 get_email\uC774 naro email connect\uB098 \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC548\uB0B4\uD55C\uB2E4(\uC2A4\uD399 \xA73-8)."
878
+ }
879
+ },
880
+ {
881
+ id: "email-disconnect",
882
+ domain: "env",
883
+ title: "Resend \uC5F0\uACB0 \uB04A\uAE30",
884
+ routes: ["DELETE projects/[projectId]/email"],
885
+ ui: { kind: "built" },
886
+ cli: { kind: "planned", note: "naro email disconnect \u2014 \uC2A4\uD399 \xA713 PR 2." },
887
+ mcp: { kind: "planned", note: "disconnect_email \u2014 \uC2A4\uD399 \xA713 PR 2." }
888
+ },
889
+ {
890
+ id: "env-pull",
891
+ domain: "env",
892
+ title: "\uD658\uACBD\uBCC0\uC218\uB97C \uB85C\uCEEC\uB85C \uB0B4\uB9AC\uAE30",
893
+ ui: {
894
+ kind: "excluded",
895
+ reason: "local-filesystem",
896
+ note: ".env.local\uC5D0 \uC4F4\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
897
+ },
898
+ cli: { kind: "built", commands: ["naro env pull"] },
899
+ mcp: {
900
+ kind: "excluded",
901
+ reason: "secret-value",
902
+ note: "\uBCF5\uD638\uD654\uD55C \uAC12\uC744 .env.local\uC5D0 \uC4F4\uB2E4. list_environment_variables\uB294 \uAC12\uC744 \uAC00\uB9B0\uB2E4. 2026-11-03 \uACB0\uC815."
903
+ }
904
+ },
905
+ {
906
+ id: "supabase-connect",
907
+ domain: "env",
908
+ title: "\uAE30\uC874 Supabase \uD504\uB85C\uC81D\uD2B8 \uC5F0\uACB0\xB7\uD0A4 \uB2E4\uC2DC \uBC1B\uAE30",
909
+ routes: [
910
+ "GET supabase/authorize",
911
+ "GET supabase/callback",
912
+ "GET projects/[projectId]/supabase/projects",
913
+ "PUT projects/[projectId]/supabase"
914
+ ],
915
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
916
+ ui: { kind: "built" },
917
+ cli: {
918
+ kind: "excluded",
919
+ reason: "browser-required",
920
+ note: "Supabase OAuth \uB9AC\uB2E4\uC774\uB809\uD2B8\uB85C \uB05D\uB09C\uB2E4. \uD1A0\uD070\uC740 10\uBD84 \uCFE0\uD0A4\uC5D0\uB9CC \uC788\uB2E4(\uC124\uACC4 2026-11-04 \xA72.1)."
921
+ },
922
+ mcp: {
923
+ kind: "excluded",
924
+ reason: "browser-required",
925
+ note: "Supabase OAuth \uB9AC\uB2E4\uC774\uB809\uD2B8\uB85C \uB05D\uB09C\uB2E4. \uB300\uC2DC\uBCF4\uB4DC\uC758 \uD504\uB85C\uC81D\uD2B8 \uC124\uC815 \uD0ED\uC5D0\uC11C \uC5F0\uACB0\uD55C\uB2E4."
926
+ }
927
+ },
928
+ {
929
+ id: "supabase-link-list",
930
+ domain: "env",
931
+ title: "Supabase \uC5F0\uACB0 \uBCF4\uAE30",
932
+ routes: ["GET projects/[projectId]/supabase"],
933
+ ui: { kind: "built" },
934
+ cli: { kind: "built", commands: ["naro supabase"] },
935
+ mcp: { kind: "built", tools: ["list_supabase_links"] }
936
+ },
937
+ {
938
+ id: "supabase-disconnect",
939
+ domain: "env",
940
+ title: "Supabase \uC5F0\uACB0 \uD574\uC81C",
941
+ routes: ["DELETE projects/[projectId]/supabase"],
942
+ ui: { kind: "built" },
943
+ cli: { kind: "built", commands: ["naro supabase disconnect"] },
944
+ mcp: { kind: "built", tools: ["disconnect_supabase"] }
945
+ },
946
+ {
947
+ id: "environment-list",
948
+ domain: "environments",
949
+ title: "\uD658\uACBD \uBAA9\uB85D",
950
+ routes: ["GET projects/[projectId]/environments"],
951
+ ui: { kind: "built" },
952
+ cli: { kind: "built", commands: ["naro environments"] },
953
+ mcp: { kind: "built", tools: ["list_environments"] }
954
+ },
955
+ {
956
+ id: "environment-create",
957
+ domain: "environments",
958
+ title: "\uD658\uACBD \uB9CC\uB4E4\uAE30",
959
+ routes: ["POST projects/[projectId]/environments"],
960
+ ui: { kind: "built" },
961
+ cli: { kind: "built", commands: ["naro environments create"] },
962
+ mcp: { kind: "built", tools: ["create_environment"] }
963
+ },
964
+ {
965
+ id: "environment-update",
966
+ domain: "environments",
967
+ title: "\uD658\uACBD \uC124\uC815 \uBCC0\uACBD(\uBE0C\uB79C\uCE58\xB7\uBCF4\uD638)",
968
+ routes: ["PATCH projects/[projectId]/environments/[environmentId]"],
969
+ ui: { kind: "built" },
970
+ cli: { kind: "built", commands: ["naro environments set"] },
971
+ mcp: { kind: "built", tools: ["update_environment"] }
972
+ },
973
+ {
974
+ id: "environment-delete",
975
+ domain: "environments",
976
+ title: "\uD658\uACBD \uC0AD\uC81C",
977
+ routes: ["DELETE projects/[projectId]/environments/[environmentId]"],
978
+ ui: { kind: "built" },
979
+ cli: { kind: "built", commands: ["naro environments delete"] },
980
+ mcp: { kind: "built", tools: ["delete_environment"] }
981
+ },
982
+ {
983
+ id: "environment-provision",
984
+ domain: "environments",
985
+ title: "\uD658\uACBD \uB370\uC774\uD130 \uD504\uB85C\uBE44\uC800\uB2DD",
986
+ routes: [
987
+ "POST projects/[projectId]/environments/[environmentId]/provision"
988
+ ],
989
+ ui: { kind: "built" },
990
+ cli: { kind: "built", commands: ["naro environments provision"] },
991
+ mcp: { kind: "built", tools: ["provision_environment"] }
992
+ },
993
+ {
994
+ id: "domain-list",
995
+ domain: "domains",
996
+ title: "\uB3C4\uBA54\uC778 \uBAA9\uB85D",
997
+ routes: ["GET projects/[projectId]/domains"],
998
+ ui: { kind: "built" },
999
+ cli: { kind: "built", commands: ["naro domains list"] },
1000
+ mcp: { kind: "built", tools: ["list_domains"] }
1001
+ },
1002
+ {
1003
+ id: "domain-add",
1004
+ domain: "domains",
1005
+ title: "\uB3C4\uBA54\uC778 \uCD94\uAC00",
1006
+ routes: ["POST projects/[projectId]/domains"],
1007
+ ui: { kind: "built" },
1008
+ cli: { kind: "built", commands: ["naro domains add"] },
1009
+ mcp: { kind: "built", tools: ["add_domain"] }
1010
+ },
1011
+ {
1012
+ id: "domain-refresh",
1013
+ domain: "domains",
1014
+ title: "\uB3C4\uBA54\uC778 \uC0C1\uD0DC \uC0C8\uB85C\uACE0\uCE68",
1015
+ routes: ["PATCH projects/[projectId]/domains/[domainId]"],
1016
+ ui: { kind: "built" },
1017
+ // naro domains verify가 Cloudflare 재확인까지 해서 refresh(저장된 상태 다시 읽기)의
1018
+ // 상위다. 같은 PATCH는 부르지 않는다.
1019
+ cli: { kind: "built" },
1020
+ // verify_domain이 같은 일을 더 한다(재확인 + DNS 점검).
1021
+ mcp: { kind: "built" }
1022
+ },
1023
+ {
1024
+ id: "domain-remove",
1025
+ domain: "domains",
1026
+ title: "\uB3C4\uBA54\uC778 \uC81C\uAC70",
1027
+ routes: ["DELETE projects/[projectId]/domains/[domainId]"],
1028
+ ui: { kind: "built" },
1029
+ cli: { kind: "built", commands: ["naro domains remove"] },
1030
+ mcp: { kind: "built", tools: ["remove_domain"] }
1031
+ },
1032
+ {
1033
+ id: "domain-verify",
1034
+ domain: "domains",
1035
+ title: "DNS \uC7AC\uAC80\uC99D",
1036
+ routes: ["POST projects/[projectId]/domains/[domainId]/verify"],
1037
+ ui: { kind: "built" },
1038
+ cli: { kind: "built", commands: ["naro domains verify"] },
1039
+ mcp: { kind: "built", tools: ["verify_domain"] }
1040
+ },
1041
+ {
1042
+ id: "domain-claim-list",
1043
+ domain: "domains",
1044
+ title: "\uB2E4\uB978 \uD504\uB85C\uC81D\uD2B8\uAC00 \uAC00\uC9C4 \uB3C4\uBA54\uC778\uC758 \uC18C\uC720\uAD8C \uD655\uC778 \uBAA9\uB85D",
1045
+ routes: ["GET projects/[projectId]/domain-claims"],
1046
+ ui: { kind: "built" },
1047
+ // naro domains verify가 소유권 확인을 함께 읽는다(client.domains.listClaims).
1048
+ cli: { kind: "built" },
1049
+ // verify_domain이 소유권 확인을 함께 읽는다(client.domains.listClaims).
1050
+ mcp: { kind: "built" }
1051
+ },
1052
+ {
1053
+ id: "domain-claim-verify",
1054
+ domain: "domains",
1055
+ title: "\uB3C4\uBA54\uC778 \uC18C\uC720\uAD8C \uD655\uC778",
1056
+ routes: ["POST projects/[projectId]/domain-claims/[claimId]/verify"],
1057
+ ui: { kind: "built" },
1058
+ // naro domains verify가 소유권 확인을 함께 다시 본다(client.domains.verifyClaim).
1059
+ cli: { kind: "built" },
1060
+ // verify_domain이 소유권 확인을 함께 다시 본다(client.domains.verifyClaim).
1061
+ mcp: { kind: "built" }
1062
+ },
1063
+ {
1064
+ id: "domain-claim-cancel",
1065
+ domain: "domains",
1066
+ title: "\uB3C4\uBA54\uC778 \uC18C\uC720\uAD8C \uD655\uC778 \uCDE8\uC18C",
1067
+ routes: ["DELETE projects/[projectId]/domain-claims/[claimId]"],
1068
+ ui: { kind: "built" },
1069
+ // naro domains remove가 붙어 있지 않은 호스트의 소유권 확인을 취소한다.
1070
+ cli: { kind: "built" },
1071
+ // remove_domain이 붙어 있지 않은 호스트의 소유권 확인을 취소한다(claimCancelled).
1072
+ mcp: { kind: "built" }
1073
+ },
1074
+ {
1075
+ id: "dns-zone-list",
1076
+ domain: "dns",
1077
+ title: "DNS \uC874 \uBAA9\uB85D",
1078
+ routes: ["GET teams/[teamSlug]/dns-zones"],
1079
+ ui: { kind: "built" },
1080
+ cli: { kind: "built", commands: ["naro dns"] },
1081
+ mcp: { kind: "built", tools: ["get_dns"] }
1082
+ },
1083
+ {
1084
+ id: "dns-zone-add",
1085
+ domain: "dns",
1086
+ title: "DNS \uC874 \uCD94\uAC00",
1087
+ routes: ["POST teams/[teamSlug]/dns-zones"],
1088
+ ui: { kind: "built" },
1089
+ cli: { kind: "built", commands: ["naro dns add"] },
1090
+ mcp: { kind: "built", tools: ["add_dns_zone"] }
1091
+ },
1092
+ {
1093
+ id: "dns-zone-remove",
1094
+ domain: "dns",
1095
+ title: "DNS \uC874 \uC0AD\uC81C",
1096
+ routes: ["DELETE teams/[teamSlug]/dns-zones/[zoneId]"],
1097
+ ui: { kind: "built" },
1098
+ cli: { kind: "built", commands: ["naro dns remove"] },
1099
+ mcp: { kind: "built", tools: ["remove_dns_zone"] }
1100
+ },
1101
+ {
1102
+ id: "dns-zone-check",
1103
+ domain: "dns",
1104
+ title: "\uB124\uC784\uC11C\uBC84 \uC774\uAD00 \uD655\uC778",
1105
+ routes: ["POST teams/[teamSlug]/dns-zones/[zoneId]/check"],
1106
+ ui: { kind: "built" },
1107
+ cli: { kind: "built", commands: ["naro dns check"] },
1108
+ mcp: { kind: "built", tools: ["check_dns_zone"] }
1109
+ },
1110
+ {
1111
+ id: "dns-zone-precheck",
1112
+ domain: "dns",
1113
+ title: "\uC774\uAD00 \uC804 \uBA54\uC77C \uD655\uC778",
1114
+ routes: ["GET teams/[teamSlug]/dns-zones/[zoneId]/precheck"],
1115
+ ui: { kind: "built" },
1116
+ cli: { kind: "built", commands: ["naro dns mail"] },
1117
+ mcp: { kind: "built", tools: ["check_dns_mail"] }
1118
+ },
1119
+ {
1120
+ id: "dns-zone-scan",
1121
+ domain: "dns",
1122
+ title: "\uAE30\uC874 DNS \uB808\uCF54\uB4DC \uC2A4\uCE94 \uACB0\uACFC",
1123
+ routes: ["GET teams/[teamSlug]/dns-zones/[zoneId]/scan"],
1124
+ ui: { kind: "built" },
1125
+ cli: { kind: "built", commands: ["naro dns scan"] },
1126
+ mcp: { kind: "built", tools: ["get_dns_scan"] }
1127
+ },
1128
+ {
1129
+ id: "dns-zone-scan-import",
1130
+ domain: "dns",
1131
+ title: "\uC2A4\uCE94\uD55C \uB808\uCF54\uB4DC \uC77C\uAD04 \uAC00\uC838\uC624\uAE30",
1132
+ routes: ["POST teams/[teamSlug]/dns-zones/[zoneId]/scan"],
1133
+ ui: { kind: "built" },
1134
+ cli: { kind: "built", commands: ["naro dns import"] },
1135
+ mcp: { kind: "built", tools: ["import_dns_scan"] }
1136
+ },
1137
+ {
1138
+ id: "dns-record-list",
1139
+ domain: "dns",
1140
+ title: "DNS \uB808\uCF54\uB4DC \uBAA9\uB85D",
1141
+ routes: ["GET teams/[teamSlug]/dns-zones/[zoneId]/records"],
1142
+ ui: { kind: "built" },
1143
+ cli: { kind: "built", commands: ["naro dns records"] },
1144
+ mcp: { kind: "built", tools: ["list_dns_records"] }
1145
+ },
1146
+ {
1147
+ id: "dns-record-add",
1148
+ domain: "dns",
1149
+ title: "DNS \uB808\uCF54\uB4DC \uCD94\uAC00",
1150
+ routes: ["POST teams/[teamSlug]/dns-zones/[zoneId]/records"],
1151
+ ui: { kind: "built" },
1152
+ cli: { kind: "built", commands: ["naro dns records add"] },
1153
+ mcp: { kind: "built", tools: ["add_dns_record"] }
1154
+ },
1155
+ {
1156
+ id: "dns-record-update",
1157
+ domain: "dns",
1158
+ title: "DNS \uB808\uCF54\uB4DC \uC218\uC815",
1159
+ routes: ["PATCH teams/[teamSlug]/dns-zones/[zoneId]/records/[recordId]"],
1160
+ ui: { kind: "built" },
1161
+ cli: { kind: "built", commands: ["naro dns records update"] },
1162
+ mcp: { kind: "built", tools: ["update_dns_record"] }
1163
+ },
1164
+ {
1165
+ id: "dns-record-remove",
1166
+ domain: "dns",
1167
+ title: "DNS \uB808\uCF54\uB4DC \uC0AD\uC81C",
1168
+ routes: ["DELETE teams/[teamSlug]/dns-zones/[zoneId]/records/[recordId]"],
1169
+ ui: { kind: "built" },
1170
+ cli: { kind: "built", commands: ["naro dns records remove"] },
1171
+ mcp: { kind: "built", tools: ["remove_dns_record"] }
1172
+ },
1173
+ {
1174
+ id: "protection-get",
1175
+ domain: "protection",
1176
+ title: "\uD504\uB9AC\uBDF0 \uBCF4\uD638 \uC0C1\uD0DC",
1177
+ routes: ["GET projects/[projectId]/preview-protection"],
1178
+ ui: { kind: "built" },
1179
+ cli: { kind: "built", commands: ["naro protection"] },
1180
+ mcp: { kind: "built", tools: ["get_preview_protection"] }
1181
+ },
1182
+ {
1183
+ id: "protection-set",
1184
+ domain: "protection",
1185
+ title: "\uD504\uB9AC\uBDF0 \uBCF4\uD638 \uCF1C\uAE30\xB7\uB044\uAE30",
1186
+ routes: ["PUT projects/[projectId]/preview-protection"],
1187
+ ui: { kind: "built" },
1188
+ cli: {
1189
+ kind: "built",
1190
+ commands: ["naro protection enable", "naro protection disable"]
1191
+ },
1192
+ mcp: { kind: "built", tools: ["set_preview_protection"] }
1193
+ },
1194
+ {
1195
+ id: "protection-bypass-create",
1196
+ domain: "protection",
1197
+ title: "\uC6B0\uD68C \uC2DC\uD06C\uB9BF \uB9CC\uB4E4\uAE30",
1198
+ routes: ["POST projects/[projectId]/preview-protection/bypass"],
1199
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
1200
+ ui: { kind: "built" },
1201
+ cli: { kind: "built", commands: ["naro protection bypass"] },
1202
+ mcp: {
1203
+ kind: "excluded",
1204
+ reason: "secret-value",
1205
+ note: "apps/mcp/src/protection-tools.ts \u2014 \uC6B0\uD68C \uC2DC\uD06C\uB9BF\uC740 \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4. \uB300\uC2DC\uBCF4\uB4DC\uB098 naro protection bypass\uB85C \uB9CC\uB4E0\uB2E4."
1206
+ }
1207
+ },
1208
+ {
1209
+ id: "protection-bypass-remove",
1210
+ domain: "protection",
1211
+ title: "\uC6B0\uD68C \uC2DC\uD06C\uB9BF \uC81C\uAC70",
1212
+ routes: ["DELETE projects/[projectId]/preview-protection/bypass"],
1213
+ ui: { kind: "built" },
1214
+ cli: { kind: "built", commands: ["naro protection bypass remove"] },
1215
+ mcp: { kind: "built", tools: ["remove_preview_protection_bypass"] }
1216
+ },
1217
+ {
1218
+ id: "database-info",
1219
+ domain: "database",
1220
+ title: "\uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC0C1\uD0DC\xB7\uC6A9\uB7C9\xB7\uD55C\uB3C4",
1221
+ routes: ["GET projects/[projectId]/database"],
1222
+ ui: { kind: "built" },
1223
+ cli: { kind: "built", commands: ["naro db info"] },
1224
+ mcp: { kind: "built", tools: ["get_database"] }
1225
+ },
1226
+ {
1227
+ id: "database-enable",
1228
+ domain: "database",
1229
+ title: "\uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uB9CC\uB4E4\uAE30",
1230
+ routes: ["POST projects/[projectId]/database"],
1231
+ ui: { kind: "built" },
1232
+ cli: { kind: "built", commands: ["naro db enable"] },
1233
+ mcp: { kind: "built", tools: ["enable_database"] }
1234
+ },
1235
+ {
1236
+ id: "database-query",
1237
+ domain: "database",
1238
+ title: "SQL \uC2E4\uD589",
1239
+ routes: ["POST projects/[projectId]/database/query"],
1240
+ ui: { kind: "built" },
1241
+ cli: { kind: "built", commands: ["naro db query"] },
1242
+ mcp: { kind: "built", tools: ["execute_sql"] }
1243
+ },
1244
+ {
1245
+ id: "database-tables",
1246
+ domain: "database",
1247
+ title: "\uD14C\uC774\uBE14 \uBAA9\uB85D",
1248
+ routes: ["GET projects/[projectId]/database/tables"],
1249
+ ui: { kind: "built" },
1250
+ // naro db 명령들이 테이블 목록을 읽는다(client.db.tables).
1251
+ cli: { kind: "built" },
1252
+ mcp: { kind: "built", tools: ["list_tables"] }
1253
+ },
1254
+ {
1255
+ id: "database-schema-change",
1256
+ domain: "database",
1257
+ title: "\uD14C\uC774\uBE14\xB7\uC778\uB371\uC2A4 \uBC14\uAFB8\uAE30",
1258
+ routes: ["POST projects/[projectId]/database/schema"],
1259
+ ui: { kind: "built" },
1260
+ cli: {
1261
+ kind: "built",
1262
+ commands: [
1263
+ "naro db table create",
1264
+ "naro db table add-column",
1265
+ "naro db table rename-column",
1266
+ "naro db table rename",
1267
+ "naro db table drop-column",
1268
+ "naro db table drop",
1269
+ "naro db index create",
1270
+ "naro db index drop"
1271
+ ]
1272
+ },
1273
+ mcp: {
1274
+ kind: "built",
1275
+ tools: [
1276
+ "create_table",
1277
+ "alter_table",
1278
+ "drop_table",
1279
+ "create_index",
1280
+ "drop_index"
1281
+ ]
1282
+ }
1283
+ },
1284
+ {
1285
+ id: "database-migration-list",
1286
+ domain: "database",
1287
+ title: "\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uBAA9\uB85D",
1288
+ routes: ["GET projects/[projectId]/database/migrations"],
1289
+ ui: { kind: "built" },
1290
+ cli: { kind: "built", commands: ["naro migration list"] },
1291
+ mcp: { kind: "built", tools: ["list_migrations"] }
1292
+ },
1293
+ {
1294
+ id: "database-migration-apply",
1295
+ domain: "database",
1296
+ title: "\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC801\uC6A9",
1297
+ routes: ["POST projects/[projectId]/database/migrations"],
1298
+ ui: { kind: "built" },
1299
+ cli: { kind: "built", commands: ["naro db push", "naro migration up"] },
1300
+ mcp: { kind: "built", tools: ["apply_migration"] }
1301
+ },
1302
+ {
1303
+ id: "database-export",
1304
+ domain: "database",
1305
+ title: "\uB364\uD504 \uB0B4\uB824\uBC1B\uAE30",
1306
+ routes: ["POST projects/[projectId]/database/export"],
1307
+ ui: { kind: "built" },
1308
+ cli: { kind: "built", commands: ["naro db dump"] },
1309
+ mcp: { kind: "built", tools: ["export_database"] }
1310
+ },
1311
+ {
1312
+ id: "database-restore",
1313
+ domain: "database",
1314
+ title: "Time Travel \uBCF5\uAD6C",
1315
+ routes: ["POST projects/[projectId]/database/restore"],
1316
+ ui: { kind: "built" },
1317
+ cli: { kind: "built", commands: ["naro db restore"] },
1318
+ mcp: { kind: "built", tools: ["restore_database"] }
1319
+ },
1320
+ {
1321
+ id: "database-bookmark",
1322
+ domain: "database",
1323
+ title: "\uBCF5\uAD6C \uC9C0\uC810(\uBD81\uB9C8\uD06C) \uC870\uD68C",
1324
+ routes: ["GET projects/[projectId]/database/bookmark"],
1325
+ ui: { kind: "built" },
1326
+ // naro db restore가 되돌릴 북마크를 먼저 읽는다(client.db.bookmark).
1327
+ cli: { kind: "built" },
1328
+ // restore_database가 되돌릴 북마크를 먼저 읽는다(client.db.bookmark).
1329
+ mcp: { kind: "built" }
1330
+ },
1331
+ {
1332
+ id: "database-pull",
1333
+ domain: "database",
1334
+ title: "\uC6D0\uACA9 \uC2A4\uD0A4\uB9C8\uB97C \uBCA0\uC774\uC2A4\uB77C\uC778 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC73C\uB85C",
1335
+ routes: ["POST projects/[projectId]/database/pull"],
1336
+ ui: {
1337
+ kind: "excluded",
1338
+ reason: "local-filesystem",
1339
+ note: "\uC6D0\uACA9 \uC2A4\uD0A4\uB9C8\uB97C ./migrations\uC758 \uCCAB \uD30C\uC77C\uB85C \uC4F4\uB2E4 \u2014 \uB0B4 \uCEF4\uD4E8\uD130\uC758 \uD3F4\uB354\uAC00 \uC788\uC5B4\uC57C \uD558\uB294 \uC77C\uC774\uB77C \uD654\uBA74\uC5D0\uC11C\uB294 \uD560 \uC218 \uC5C6\uB2E4."
1340
+ },
1341
+ cli: { kind: "built", commands: ["naro db pull"] },
1342
+ mcp: { kind: "built", tools: ["pull_database_schema"] }
1343
+ },
1344
+ {
1345
+ id: "database-reset",
1346
+ domain: "database",
1347
+ title: "\uC804\uCCB4 \uB9AC\uC14B \uD6C4 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uB2E4\uC2DC \uC801\uC6A9",
1348
+ ui: {
1349
+ kind: "excluded",
1350
+ reason: "local-filesystem",
1351
+ note: "./migrations\uB97C \uCC98\uC74C\uBD80\uD130 \uB2E4\uC2DC \uC801\uC6A9\uD55C\uB2E4 \u2014 \uB0B4 \uCEF4\uD4E8\uD130\uC758 \uD3F4\uB354\uAC00 \uC788\uC5B4\uC57C \uD558\uB294 \uC77C\uC774\uB77C \uD654\uBA74\uC5D0\uC11C\uB294 \uD560 \uC218 \uC5C6\uB2E4."
1352
+ },
1353
+ cli: { kind: "built", commands: ["naro db reset"] },
1354
+ mcp: {
1355
+ kind: "excluded",
1356
+ reason: "irreversible",
1357
+ note: "\uBAA8\uB4E0 \uD14C\uC774\uBE14\uACFC \uC815\uCC45\uC744 \uC9C0\uC6B0\uACE0 \uB2E4\uC2DC \uB9CC\uB4E0\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1358
+ }
1359
+ },
1360
+ {
1361
+ id: "database-lint",
1362
+ domain: "database",
1363
+ title: "\uC2A4\uD0A4\uB9C8\xB7RLS \uC815\uCC45 \uB9B0\uD2B8",
1364
+ ui: { kind: "built" },
1365
+ cli: { kind: "built", commands: ["naro db lint"] },
1366
+ mcp: { kind: "built", tools: ["lint_database"] }
1367
+ },
1368
+ {
1369
+ id: "database-gen-types",
1370
+ domain: "database",
1371
+ title: "\uC2A4\uD0A4\uB9C8\uC5D0\uC11C TypeScript \uD0C0\uC785 \uC0DD\uC131",
1372
+ ui: { kind: "built" },
1373
+ cli: { kind: "built", commands: ["naro gen types"] },
1374
+ mcp: { kind: "built", tools: ["generate_typescript_types"] }
1375
+ },
1376
+ {
1377
+ id: "database-migration-new",
1378
+ domain: "database",
1379
+ title: "\uC0C8 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD30C\uC77C",
1380
+ ui: {
1381
+ kind: "excluded",
1382
+ reason: "local-filesystem",
1383
+ note: "./migrations\uC5D0 \uD30C\uC77C\uC744 \uB9CC\uB4E0\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
1384
+ },
1385
+ cli: { kind: "built", commands: ["naro migration new"] },
1386
+ mcp: {
1387
+ kind: "excluded",
1388
+ reason: "agent-native",
1389
+ note: "./migrations\uC5D0 \uD30C\uC77C \uD558\uB098\uB97C \uB9CC\uB4E0\uB2E4. apply_migration\uC740 \uC801\uC6A9\uD558\uBA74\uC11C \uD30C\uC77C\uC744 \uC4F4\uB2E4. 2026-11-03 \uACB0\uC815."
1390
+ }
1391
+ },
1392
+ {
1393
+ id: "database-migration-repair",
1394
+ domain: "database",
1395
+ title: "\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC6D0\uC7A5 \uC218\uB9AC",
1396
+ dashboard: "/[teamSlug]/projects/[projectSlug]/database/migrations",
1397
+ ui: { kind: "built" },
1398
+ cli: { kind: "built", commands: ["naro migration repair"] },
1399
+ mcp: {
1400
+ kind: "excluded",
1401
+ reason: "irreversible",
1402
+ note: "\uC6D0\uC7A5\uB9CC \uACE0\uCCD0 \uC801\uC6A9 \uAE30\uB85D\uACFC \uC2E4\uC81C \uC2A4\uD0A4\uB9C8\uAC00 \uC5B4\uAE0B\uB0A0 \uC218 \uC788\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1403
+ }
1404
+ },
1405
+ {
1406
+ id: "database-saved-query-list",
1407
+ domain: "database",
1408
+ title: "\uC800\uC7A5\uD55C \uCFFC\uB9AC \uBAA9\uB85D",
1409
+ routes: ["GET projects/[projectId]/database/saved-queries"],
1410
+ dashboard: "/[teamSlug]/projects/[projectSlug]/database/sql",
1411
+ ui: { kind: "built" },
1412
+ cli: {
1413
+ kind: "excluded",
1414
+ reason: "ui-affordance",
1415
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. CLI\uB294 \uCFFC\uB9AC\uB97C \uD30C\uC77C\uC5D0 \uB450\uACE0 naro db query --file\uB85C \uB3CC\uB9B0\uB2E4."
1416
+ },
1417
+ mcp: {
1418
+ kind: "excluded",
1419
+ reason: "ui-affordance",
1420
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1421
+ }
1422
+ },
1423
+ {
1424
+ id: "database-saved-query-create",
1425
+ domain: "database",
1426
+ title: "\uCFFC\uB9AC \uC800\uC7A5",
1427
+ routes: ["POST projects/[projectId]/database/saved-queries"],
1428
+ dashboard: "/[teamSlug]/projects/[projectSlug]/database/sql",
1429
+ ui: { kind: "built" },
1430
+ cli: {
1431
+ kind: "excluded",
1432
+ reason: "ui-affordance",
1433
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. CLI\uB294 \uCFFC\uB9AC\uB97C \uD30C\uC77C\uC5D0 \uB450\uACE0 naro db query --file\uB85C \uB3CC\uB9B0\uB2E4."
1434
+ },
1435
+ mcp: {
1436
+ kind: "excluded",
1437
+ reason: "ui-affordance",
1438
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1439
+ }
1440
+ },
1441
+ {
1442
+ id: "database-saved-query-update",
1443
+ domain: "database",
1444
+ title: "\uC800\uC7A5\uD55C \uCFFC\uB9AC \uC218\uC815",
1445
+ routes: [
1446
+ "PATCH projects/[projectId]/database/saved-queries/[savedQueryId]"
1447
+ ],
1448
+ dashboard: "/[teamSlug]/projects/[projectSlug]/database/sql",
1449
+ ui: { kind: "built" },
1450
+ cli: {
1451
+ kind: "excluded",
1452
+ reason: "ui-affordance",
1453
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. CLI\uB294 \uCFFC\uB9AC\uB97C \uD30C\uC77C\uC5D0 \uB450\uACE0 naro db query --file\uB85C \uB3CC\uB9B0\uB2E4."
1454
+ },
1455
+ mcp: {
1456
+ kind: "excluded",
1457
+ reason: "ui-affordance",
1458
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1459
+ }
1460
+ },
1461
+ {
1462
+ id: "database-saved-query-delete",
1463
+ domain: "database",
1464
+ title: "\uC800\uC7A5\uD55C \uCFFC\uB9AC \uC0AD\uC81C",
1465
+ routes: [
1466
+ "DELETE projects/[projectId]/database/saved-queries/[savedQueryId]"
1467
+ ],
1468
+ dashboard: "/[teamSlug]/projects/[projectSlug]/database/sql",
1469
+ ui: { kind: "built" },
1470
+ cli: {
1471
+ kind: "excluded",
1472
+ reason: "ui-affordance",
1473
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. CLI\uB294 \uCFFC\uB9AC\uB97C \uD30C\uC77C\uC5D0 \uB450\uACE0 naro db query --file\uB85C \uB3CC\uB9B0\uB2E4."
1474
+ },
1475
+ mcp: {
1476
+ kind: "excluded",
1477
+ reason: "ui-affordance",
1478
+ note: "SQL \uD3B8\uC9D1\uAE30 \uC548\uC5D0\uC11C\uB9CC \uB73B\uC774 \uC788\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1479
+ }
1480
+ },
1481
+ {
1482
+ // 대시보드 정책 화면(백엔드 메뉴 Policies)은 2026-11-04에 생겼다.
1483
+ id: "rls-table-policy-list",
1484
+ domain: "rls",
1485
+ title: "\uD14C\uC774\uBE14\uBCC4 \uC815\uCC45 \uC870\uD68C",
1486
+ routes: ["GET projects/[projectId]/backend/policies"],
1487
+ ui: { kind: "built" },
1488
+ cli: { kind: "built", commands: ["naro policy rules"] },
1489
+ mcp: { kind: "built", tools: ["list_policy_rules"] }
1490
+ },
1491
+ {
1492
+ id: "rls-table-policy-set",
1493
+ domain: "rls",
1494
+ title: "\uD14C\uC774\uBE14\uC758 \uC561\uC158\uBCC4 \uADDC\uCE59 \uC124\uC815",
1495
+ routes: ["PUT projects/[projectId]/backend/policies/[table]"],
1496
+ ui: { kind: "built" },
1497
+ cli: { kind: "built", commands: ["naro policy set"] },
1498
+ // 규칙은 CREATE POLICY로, owner 규칙은 COMMENT ON POLICY p ON t IS 'naro:owner <column>'로 apply_migration에 쓴다.
1499
+ mcp: { kind: "built" }
1500
+ },
1501
+ {
1502
+ id: "rls-table-policy-remove",
1503
+ domain: "rls",
1504
+ title: "\uD14C\uC774\uBE14\uC758 \uC815\uCC45 \uC804\uBD80 \uC81C\uAC70",
1505
+ routes: ["DELETE projects/[projectId]/backend/policies/[table]"],
1506
+ ui: { kind: "built" },
1507
+ cli: { kind: "built", commands: ["naro policy remove"] },
1508
+ // apply_migration에 그 테이블의 DROP POLICY를 쓴다.
1509
+ mcp: { kind: "built" }
1510
+ },
1511
+ {
1512
+ id: "rls-policy-list",
1513
+ domain: "rls",
1514
+ title: "\uC815\uCC45 \uBAA9\uB85D(CREATE POLICY \uAF34)",
1515
+ routes: ["GET projects/[projectId]/backend/rls/policies"],
1516
+ ui: { kind: "built" },
1517
+ cli: { kind: "built", commands: ["naro policy list"] },
1518
+ mcp: { kind: "built", tools: ["list_policies"] }
1519
+ },
1520
+ {
1521
+ id: "rls-policy-create",
1522
+ domain: "rls",
1523
+ title: "\uC815\uCC45 \uB9CC\uB4E4\uAE30(CREATE POLICY)",
1524
+ routes: ["POST projects/[projectId]/backend/rls/policies"],
1525
+ ui: { kind: "built" },
1526
+ cli: { kind: "built", commands: ["naro policy create"] },
1527
+ // apply_migration·execute_sql이 CREATE POLICY를 받는다. 테이블과 같은 마이그레이션에 쓰라고 apply_migration이 안내한다.
1528
+ mcp: { kind: "built" }
1529
+ },
1530
+ {
1531
+ id: "rls-policy-replace",
1532
+ domain: "rls",
1533
+ title: "\uC815\uCC45 \uBC14\uAFB8\uAE30",
1534
+ routes: ["PUT projects/[projectId]/backend/rls/policies/[table]/[name]"],
1535
+ ui: { kind: "built" },
1536
+ // naro migration up·db push(client.db.applyMigration)나 naro db query
1537
+ // (client.db.query) 한 번에 DROP POLICY와 CREATE POLICY를 함께 쓴다.
1538
+ // MCP와 같은 경로다(PR #414 리뷰).
1539
+ cli: { kind: "built" },
1540
+ // apply_migration 한 번에 DROP POLICY와 CREATE POLICY를 함께 쓴다.
1541
+ mcp: { kind: "built" }
1542
+ },
1543
+ {
1544
+ id: "rls-policy-drop",
1545
+ domain: "rls",
1546
+ title: "\uC815\uCC45 \uC0AD\uC81C(DROP POLICY)",
1547
+ routes: ["DELETE projects/[projectId]/backend/rls/policies/[table]/[name]"],
1548
+ ui: { kind: "built" },
1549
+ cli: { kind: "built", commands: ["naro policy drop"] },
1550
+ // apply_migration·execute_sql이 DROP POLICY를 받는다.
1551
+ mcp: { kind: "built" }
1552
+ },
1553
+ {
1554
+ id: "storage-info",
1555
+ domain: "storage",
1556
+ title: "Storage \uC0C1\uD0DC",
1557
+ routes: ["GET projects/[projectId]/backend/storage"],
1558
+ ui: { kind: "built" },
1559
+ cli: { kind: "built", commands: ["naro storage"] },
1560
+ mcp: { kind: "built", tools: ["get_storage"] }
1561
+ },
1562
+ {
1563
+ id: "storage-enable",
1564
+ domain: "storage",
1565
+ title: "Storage \uCF1C\uAE30",
1566
+ routes: ["POST projects/[projectId]/backend/storage"],
1567
+ ui: { kind: "built" },
1568
+ cli: { kind: "built", commands: ["naro storage enable"] },
1569
+ mcp: { kind: "built", tools: ["enable_storage"] }
1570
+ },
1571
+ {
1572
+ id: "storage-disable",
1573
+ domain: "storage",
1574
+ title: "Storage \uB044\uAE30",
1575
+ routes: ["DELETE projects/[projectId]/backend/storage"],
1576
+ ui: { kind: "built" },
1577
+ cli: { kind: "built", commands: ["naro storage disable"] },
1578
+ mcp: { kind: "built", tools: ["disable_storage"] }
1579
+ },
1580
+ {
1581
+ id: "storage-bucket-list",
1582
+ domain: "storage",
1583
+ title: "\uBC84\uD0B7 \uBAA9\uB85D",
1584
+ routes: ["GET projects/[projectId]/backend/storage/buckets"],
1585
+ ui: { kind: "built" },
1586
+ cli: { kind: "built", commands: ["naro storage bucket list"] },
1587
+ mcp: { kind: "built", tools: ["list_storage_buckets"] }
1588
+ },
1589
+ {
1590
+ id: "storage-bucket-create",
1591
+ domain: "storage",
1592
+ title: "\uBC84\uD0B7 \uB9CC\uB4E4\uAE30",
1593
+ routes: ["POST projects/[projectId]/backend/storage/buckets"],
1594
+ ui: { kind: "built" },
1595
+ cli: { kind: "built", commands: ["naro storage bucket create"] },
1596
+ mcp: { kind: "built", tools: ["create_storage_bucket"] }
1597
+ },
1598
+ {
1599
+ id: "storage-bucket-get",
1600
+ domain: "storage",
1601
+ title: "\uBC84\uD0B7 \uC870\uD68C",
1602
+ routes: ["GET projects/[projectId]/backend/storage/buckets/[bucket]"],
1603
+ ui: { kind: "built" },
1604
+ // naro storage bucket update가 바꾸기 전에 읽는다(client.storage.getBucket).
1605
+ cli: { kind: "built" },
1606
+ mcp: { kind: "built", tools: ["get_storage_bucket"] }
1607
+ },
1608
+ {
1609
+ id: "storage-bucket-update",
1610
+ domain: "storage",
1611
+ title: "\uBC84\uD0B7 \uC124\uC815 \uBCC0\uACBD",
1612
+ routes: ["PATCH projects/[projectId]/backend/storage/buckets/[bucket]"],
1613
+ ui: { kind: "built" },
1614
+ cli: { kind: "built", commands: ["naro storage bucket update"] },
1615
+ mcp: { kind: "built", tools: ["update_storage_bucket"] }
1616
+ },
1617
+ {
1618
+ id: "storage-bucket-remove",
1619
+ domain: "storage",
1620
+ title: "\uBC84\uD0B7 \uC0AD\uC81C",
1621
+ routes: ["DELETE projects/[projectId]/backend/storage/buckets/[bucket]"],
1622
+ ui: { kind: "built" },
1623
+ cli: { kind: "built", commands: ["naro storage bucket remove"] },
1624
+ mcp: { kind: "built", tools: ["delete_storage_bucket"] }
1625
+ },
1626
+ {
1627
+ id: "storage-bucket-empty",
1628
+ domain: "storage",
1629
+ title: "\uBC84\uD0B7 \uBE44\uC6B0\uAE30",
1630
+ routes: [
1631
+ "POST projects/[projectId]/backend/storage/buckets/[bucket]/empty"
1632
+ ],
1633
+ ui: { kind: "built" },
1634
+ cli: { kind: "built", commands: ["naro storage bucket empty"] },
1635
+ mcp: { kind: "built", tools: ["empty_storage_bucket"] }
1636
+ },
1637
+ {
1638
+ id: "storage-object-list",
1639
+ domain: "storage",
1640
+ title: "\uAC1D\uCCB4 \uBAA9\uB85D",
1641
+ routes: [
1642
+ "POST projects/[projectId]/backend/storage/buckets/[bucket]/objects/list"
1643
+ ],
1644
+ ui: { kind: "built" },
1645
+ cli: { kind: "built", commands: ["naro storage ls"] },
1646
+ mcp: { kind: "built", tools: ["list_storage_objects"] }
1647
+ },
1648
+ {
1649
+ id: "storage-object-remove",
1650
+ domain: "storage",
1651
+ title: "\uAC1D\uCCB4 \uC0AD\uC81C",
1652
+ routes: [
1653
+ "DELETE projects/[projectId]/backend/storage/buckets/[bucket]/objects"
1654
+ ],
1655
+ ui: { kind: "built" },
1656
+ cli: { kind: "built", commands: ["naro storage rm"] },
1657
+ mcp: { kind: "built", tools: ["delete_storage_objects"] }
1658
+ },
1659
+ {
1660
+ id: "storage-object-sign",
1661
+ domain: "storage",
1662
+ title: "\uB2E4\uC6B4\uB85C\uB4DC \uC11C\uBA85 URL",
1663
+ routes: [
1664
+ "POST projects/[projectId]/backend/storage/buckets/[bucket]/objects/sign"
1665
+ ],
1666
+ ui: { kind: "built" },
1667
+ cli: { kind: "built", commands: ["naro storage sign"] },
1668
+ mcp: { kind: "built", tools: ["create_storage_signed_url"] }
1669
+ },
1670
+ {
1671
+ id: "storage-object-upload",
1672
+ domain: "storage",
1673
+ title: "\uC5C5\uB85C\uB4DC URL \uBC1C\uAE09",
1674
+ routes: [
1675
+ "POST projects/[projectId]/backend/storage/buckets/[bucket]/objects/upload-url"
1676
+ ],
1677
+ ui: { kind: "built" },
1678
+ cli: { kind: "built", commands: ["naro storage cp"] },
1679
+ mcp: { kind: "built", tools: ["create_storage_upload_url"] }
1680
+ },
1681
+ {
1682
+ id: "storage-object-move",
1683
+ domain: "storage",
1684
+ title: "\uAC1D\uCCB4 \uC774\uB3D9",
1685
+ routes: [
1686
+ "POST projects/[projectId]/backend/storage/buckets/[bucket]/objects/move"
1687
+ ],
1688
+ ui: { kind: "built" },
1689
+ cli: { kind: "built", commands: ["naro storage mv"] },
1690
+ mcp: { kind: "built", tools: ["move_storage_object"] }
1691
+ },
1692
+ {
1693
+ id: "realtime-info",
1694
+ domain: "realtime",
1695
+ title: "Realtime \uC0C1\uD0DC\xB7\uC5F0\uACB0 \uC218",
1696
+ routes: ["GET projects/[projectId]/backend/realtime"],
1697
+ ui: { kind: "built" },
1698
+ cli: { kind: "built", commands: ["naro realtime"] },
1699
+ mcp: { kind: "built", tools: ["get_realtime"] }
1700
+ },
1701
+ {
1702
+ id: "realtime-enable",
1703
+ domain: "realtime",
1704
+ title: "Realtime \uCF1C\uAE30",
1705
+ routes: ["POST projects/[projectId]/backend/realtime"],
1706
+ ui: { kind: "built" },
1707
+ cli: { kind: "built", commands: ["naro realtime enable"] },
1708
+ mcp: { kind: "built", tools: ["enable_realtime"] }
1709
+ },
1710
+ {
1711
+ id: "realtime-table-list",
1712
+ domain: "realtime",
1713
+ title: "\uACF5\uAC1C \uD14C\uC774\uBE14 \uBAA9\uB85D",
1714
+ routes: ["GET projects/[projectId]/backend/realtime/tables"],
1715
+ ui: { kind: "built" },
1716
+ cli: { kind: "built", commands: ["naro realtime tables"] },
1717
+ // get_realtime이 공개 테이블을 상태와 함께 돌려준다.
1718
+ mcp: { kind: "built" }
1719
+ },
1720
+ {
1721
+ id: "realtime-table-add",
1722
+ domain: "realtime",
1723
+ title: "\uD14C\uC774\uBE14 \uBCC0\uACBD \uACF5\uAC1C",
1724
+ routes: ["PUT projects/[projectId]/backend/realtime/tables/[table]"],
1725
+ ui: { kind: "built" },
1726
+ cli: { kind: "built", commands: ["naro realtime tables add"] },
1727
+ mcp: { kind: "built", tools: ["publish_realtime_table"] }
1728
+ },
1729
+ {
1730
+ id: "realtime-table-remove",
1731
+ domain: "realtime",
1732
+ title: "\uD14C\uC774\uBE14 \uBCC0\uACBD \uACF5\uAC1C \uC911\uC9C0",
1733
+ routes: ["DELETE projects/[projectId]/backend/realtime/tables/[table]"],
1734
+ ui: { kind: "built" },
1735
+ cli: { kind: "built", commands: ["naro realtime tables remove"] },
1736
+ mcp: { kind: "built", tools: ["unpublish_realtime_table"] }
1737
+ },
1738
+ {
1739
+ // 대시보드 Functions 화면은 2026-11-04에 생겼다. 그 전의 built는 틀렸다.
1740
+ id: "function-list",
1741
+ domain: "functions",
1742
+ title: "\uD568\uC218 \uBAA9\uB85D",
1743
+ routes: ["GET projects/[projectId]/backend/functions"],
1744
+ ui: { kind: "built" },
1745
+ cli: { kind: "built", commands: ["naro functions"] },
1746
+ mcp: { kind: "built", tools: ["list_functions"] }
1747
+ },
1748
+ {
1749
+ id: "function-scaffold",
1750
+ domain: "functions",
1751
+ title: "\uD568\uC218 \uBF08\uB300 \uB9CC\uB4E4\uAE30",
1752
+ ui: {
1753
+ kind: "excluded",
1754
+ reason: "local-filesystem",
1755
+ note: "naro/functions/<name>/index.ts\uB97C \uC4F4\uB2E4. \uC11C\uBC84\uAC00 \uD560 \uC218 \uC5C6\uB2E4."
1756
+ },
1757
+ cli: { kind: "built", commands: ["naro functions new"] },
1758
+ mcp: {
1759
+ kind: "excluded",
1760
+ reason: "agent-native",
1761
+ note: "naro/functions/<name>/index.ts\uB97C \uC4F4\uB2E4. 2026-11-03 \uACB0\uC815."
1762
+ }
1763
+ },
1764
+ {
1765
+ id: "function-get",
1766
+ domain: "functions",
1767
+ title: "\uD568\uC218 \uC0C1\uC138",
1768
+ routes: ["GET projects/[projectId]/backend/functions/[name]"],
1769
+ ui: { kind: "built" },
1770
+ cli: { kind: "built", commands: ["naro functions versions"] },
1771
+ mcp: { kind: "built", tools: ["get_function"] }
1772
+ },
1773
+ {
1774
+ id: "function-update",
1775
+ domain: "functions",
1776
+ title: "\uD568\uC218 verify_jwt \uC124\uC815",
1777
+ routes: ["PATCH projects/[projectId]/backend/functions/[name]"],
1778
+ ui: { kind: "built" },
1779
+ cli: { kind: "built", commands: ["naro functions update"] },
1780
+ mcp: { kind: "built", tools: ["update_function"] }
1781
+ },
1782
+ {
1783
+ id: "function-delete",
1784
+ domain: "functions",
1785
+ title: "\uD568\uC218 \uC0AD\uC81C",
1786
+ routes: ["DELETE projects/[projectId]/backend/functions/[name]"],
1787
+ ui: { kind: "built" },
1788
+ cli: { kind: "built", commands: ["naro functions delete"] },
1789
+ mcp: { kind: "built", tools: ["delete_function"] }
1790
+ },
1791
+ {
1792
+ id: "function-version-upload",
1793
+ domain: "functions",
1794
+ title: "\uBC88\uB4E4\uD55C \uCF54\uB4DC\uB97C \uC0C8 \uBC84\uC804\uC73C\uB85C \uC62C\uB9AC\uAE30",
1795
+ routes: ["POST projects/[projectId]/backend/functions/[name]/versions"],
1796
+ ui: {
1797
+ kind: "excluded",
1798
+ reason: "local-filesystem",
1799
+ note: "\uB85C\uCEEC\uC5D0\uC11C \uBC88\uB4E4\uD558\uACE0 workerd\uB85C \uBD80\uD305 \uD655\uC778\uD55C \uCF54\uB4DC\uB97C \uC62C\uB9B0\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uB294 \uBC88\uB4E4\uD560 \uC18C\uC2A4\uAC00 \uC5C6\uB2E4."
1800
+ },
1801
+ cli: { kind: "built", commands: ["naro functions deploy"] },
1802
+ mcp: { kind: "built", tools: ["deploy_function"] }
1803
+ },
1804
+ {
1805
+ id: "function-version-deploy",
1806
+ domain: "functions",
1807
+ title: "\uC800\uC7A5\uB41C \uBC84\uC804 \uBC30\uD3EC",
1808
+ routes: [
1809
+ "POST projects/[projectId]/backend/functions/[name]/versions/[versionId]/deploy"
1810
+ ],
1811
+ ui: { kind: "built" },
1812
+ // naro functions deploy가 올린 버전을 배포한다(client.functions.deploy).
1813
+ cli: { kind: "built" },
1814
+ // deploy_function이 naro-cli의 deployFunctions로 올린 버전을 배포한다.
1815
+ mcp: { kind: "built" }
1816
+ },
1817
+ {
1818
+ id: "function-rollback",
1819
+ domain: "functions",
1820
+ title: "\uC774\uC804 \uBC84\uC804\uC73C\uB85C \uB864\uBC31",
1821
+ routes: ["POST projects/[projectId]/backend/functions/[name]/rollback"],
1822
+ ui: { kind: "built" },
1823
+ cli: { kind: "built", commands: ["naro functions rollback"] },
1824
+ mcp: { kind: "built", tools: ["rollback_function"] }
1825
+ },
1826
+ {
1827
+ id: "function-logs",
1828
+ domain: "functions",
1829
+ title: "\uD568\uC218 \uB85C\uADF8",
1830
+ routes: ["GET projects/[projectId]/backend/functions/[name]/logs"],
1831
+ ui: { kind: "built" },
1832
+ cli: { kind: "built", commands: ["naro functions logs"] },
1833
+ mcp: { kind: "built", tools: ["get_function_logs"] }
1834
+ },
1835
+ {
1836
+ id: "function-invoke",
1837
+ domain: "functions",
1838
+ title: "\uD568\uC218 \uD638\uCD9C",
1839
+ routes: ["POST projects/[projectId]/backend/functions/[name]/invoke"],
1840
+ ui: { kind: "built" },
1841
+ cli: { kind: "built", commands: ["naro functions invoke"] },
1842
+ mcp: { kind: "built", tools: ["invoke_function"] }
1843
+ },
1844
+ {
1845
+ id: "function-secret-list",
1846
+ domain: "functions",
1847
+ title: "\uD568\uC218 \uC2DC\uD06C\uB9BF \uBAA9\uB85D",
1848
+ routes: ["GET projects/[projectId]/backend/functions-secrets"],
1849
+ ui: { kind: "built" },
1850
+ cli: { kind: "built", commands: ["naro functions secrets"] },
1851
+ mcp: { kind: "built", tools: ["list_function_secrets"] }
1852
+ },
1853
+ {
1854
+ id: "function-secret-set",
1855
+ domain: "functions",
1856
+ title: "\uD568\uC218 \uC2DC\uD06C\uB9BF \uC124\uC815",
1857
+ routes: ["PUT projects/[projectId]/backend/functions-secrets"],
1858
+ dashboard: "/[teamSlug]/projects/[projectSlug]/functions/secrets",
1859
+ ui: { kind: "built" },
1860
+ cli: {
1861
+ kind: "built",
1862
+ commands: ["naro functions secrets set", "naro functions secrets import"]
1863
+ },
1864
+ mcp: {
1865
+ kind: "excluded",
1866
+ reason: "secret-value",
1867
+ note: "apps/mcp/src/function-tools.ts\uC758 NO_SECRET_VALUES \u2014 \uAC12\uC774 \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4. \uC0AC\uC6A9\uC790\uAC00 naro functions secrets set\uC744 \uB3CC\uB9AC\uAC8C \uD55C\uB2E4."
1868
+ }
1869
+ },
1870
+ // 라우트는 function-secret-set과 같은 PUT functions-secrets다. 한 라우트는
1871
+ // 한 기능에만 걸리므로 여기에는 적지 않는다.
1872
+ {
1873
+ id: "function-secret-unset",
1874
+ domain: "functions",
1875
+ title: "\uD568\uC218 \uC2DC\uD06C\uB9BF \uD574\uC81C",
1876
+ ui: { kind: "built" },
1877
+ cli: { kind: "built", commands: ["naro functions secrets unset"] },
1878
+ mcp: { kind: "built", tools: ["unset_function_secret"] }
1879
+ },
1880
+ {
1881
+ id: "function-sync",
1882
+ domain: "functions",
1883
+ title: "\uD568\uC218 \uC7AC\uB3D9\uAE30\uD654",
1884
+ routes: ["POST projects/[projectId]/backend/functions-sync"],
1885
+ ui: { kind: "built" },
1886
+ cli: { kind: "built", commands: ["naro functions secrets sync"] },
1887
+ mcp: { kind: "built", tools: ["sync_functions"] }
1888
+ },
1889
+ {
1890
+ id: "backend-info",
1891
+ domain: "backend",
1892
+ title: "Backend \uC0C1\uD0DC\xB7URL\xB7anon \uD0A4",
1893
+ routes: ["GET projects/[projectId]/backend"],
1894
+ ui: { kind: "built" },
1895
+ cli: { kind: "built", commands: ["naro backend"] },
1896
+ mcp: { kind: "built", tools: ["get_backend"] }
1897
+ },
1898
+ {
1899
+ id: "backend-enable",
1900
+ domain: "backend",
1901
+ title: "Backend \uCF1C\uAE30",
1902
+ routes: ["POST projects/[projectId]/backend/enable"],
1903
+ ui: { kind: "built" },
1904
+ cli: { kind: "built", commands: ["naro backend enable"] },
1905
+ mcp: { kind: "built", tools: ["enable_backend"] }
1906
+ },
1907
+ {
1908
+ id: "backend-provision",
1909
+ domain: "backend",
1910
+ title: "Backend \uD504\uB85C\uBE44\uC800\uB2DD \uC774\uC5B4\uAC00\uAE30",
1911
+ routes: ["POST projects/[projectId]/backend/provision"],
1912
+ ui: { kind: "built" },
1913
+ // naro backend enable이 멈춘 프로비저닝을 이어 간다.
1914
+ cli: { kind: "built" },
1915
+ // enable_backend가 같은 호출이다(client.backend.enable — 라우트 provision은 그 옛 이름).
1916
+ mcp: { kind: "built" }
1917
+ },
1918
+ {
1919
+ id: "backend-disable",
1920
+ domain: "backend",
1921
+ title: "Backend \uB044\uAE30",
1922
+ routes: ["POST projects/[projectId]/backend/disable"],
1923
+ ui: { kind: "built" },
1924
+ cli: { kind: "built", commands: ["naro backend disable"] },
1925
+ mcp: { kind: "built", tools: ["disable_backend"] }
1926
+ },
1927
+ {
1928
+ id: "backend-keys",
1929
+ domain: "backend",
1930
+ title: "API \uD0A4 \uC870\uD68C(service \uD0A4 \uD3EC\uD568)",
1931
+ routes: ["GET projects/[projectId]/backend/keys"],
1932
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
1933
+ ui: { kind: "built" },
1934
+ cli: { kind: "built", commands: ["naro backend keys"] },
1935
+ mcp: {
1936
+ kind: "excluded",
1937
+ reason: "secret-value",
1938
+ note: "service \uD0A4\uAC00 \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1939
+ }
1940
+ },
1941
+ {
1942
+ id: "backend-keys-rotate",
1943
+ domain: "backend",
1944
+ title: "API \uD0A4 \uAD50\uCCB4",
1945
+ routes: ["POST projects/[projectId]/backend/keys/rotate"],
1946
+ // 2026-11-04 전에는 화면이 없었는데 built로 적혀 있었다.
1947
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
1948
+ ui: { kind: "built" },
1949
+ cli: { kind: "built", commands: ["naro backend keys rotate"] },
1950
+ mcp: {
1951
+ kind: "excluded",
1952
+ reason: "irreversible",
1953
+ note: "\uC61B \uD0A4\uAC00 \uBC14\uB85C \uC8FD\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1954
+ }
1955
+ },
1956
+ {
1957
+ id: "backend-signing-key-rotate",
1958
+ domain: "backend",
1959
+ title: "\uC11C\uBA85 \uD0A4 \uAD50\uCCB4",
1960
+ routes: ["POST projects/[projectId]/backend/signing-key/rotate"],
1961
+ dashboard: "/[teamSlug]/projects/[projectSlug]/settings",
1962
+ ui: { kind: "built" },
1963
+ cli: { kind: "built", commands: ["naro backend signing-key rotate"] },
1964
+ mcp: {
1965
+ kind: "excluded",
1966
+ reason: "irreversible",
1967
+ note: "\uBC1C\uAE09\uD55C \uD1A0\uD070\uC774 15\uBD84 \uC548\uC5D0 \uBAA8\uB450 \uC8FD\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
1968
+ }
1969
+ },
1970
+ {
1971
+ id: "backend-auth-get",
1972
+ domain: "backend",
1973
+ title: "\uB85C\uADF8\uC778 \uC124\uC815 \uC870\uD68C",
1974
+ routes: ["GET projects/[projectId]/backend/auth"],
1975
+ ui: { kind: "built" },
1976
+ cli: {
1977
+ kind: "built",
1978
+ commands: ["naro backend auth", "naro backend auth redirects"]
1979
+ },
1980
+ mcp: { kind: "built", tools: ["get_backend_auth"] }
1981
+ },
1982
+ {
1983
+ id: "backend-auth-update",
1984
+ domain: "backend",
1985
+ title: "\uB85C\uADF8\uC778 \uC124\uC815 \uBCC0\uACBD",
1986
+ routes: ["PUT projects/[projectId]/backend/auth"],
1987
+ ui: { kind: "built" },
1988
+ cli: {
1989
+ kind: "built",
1990
+ commands: [
1991
+ "naro backend auth signups",
1992
+ "naro backend auth password",
1993
+ "naro backend auth email set",
1994
+ "naro backend auth oauth set",
1995
+ "naro backend auth redirects add",
1996
+ "naro backend auth redirects remove",
1997
+ "naro backend auth require-verification"
1998
+ ]
1999
+ },
2000
+ // 리다이렉트 제거·가입·확인 메일·비밀번호 길이만. 메일 API 키와
2001
+ // OAuth client secret은 대화에 남고(secret-value), 리다이렉트 추가는 재설정 토큰이
2002
+ // 그 origin으로 가므로(PR #434 리뷰) 대시보드와 CLI만 한다.
2003
+ mcp: { kind: "built", tools: ["update_backend_auth"] }
2004
+ },
2005
+ {
2006
+ id: "backend-auth-remove",
2007
+ domain: "backend",
2008
+ title: "\uB85C\uADF8\uC778 \uACF5\uAE09\uC790\xB7\uBA54\uC77C \uC124\uC815 \uC81C\uAC70",
2009
+ routes: ["DELETE projects/[projectId]/backend/auth"],
2010
+ ui: { kind: "built" },
2011
+ cli: {
2012
+ kind: "built",
2013
+ commands: [
2014
+ "naro backend auth email remove",
2015
+ "naro backend auth oauth remove"
2016
+ ]
2017
+ },
2018
+ mcp: { kind: "built", tools: ["remove_backend_auth"] }
2019
+ },
2020
+ {
2021
+ id: "backend-user-list",
2022
+ domain: "backend",
2023
+ title: "\uC720\uC800 \uBAA9\uB85D",
2024
+ routes: ["GET projects/[projectId]/backend/users"],
2025
+ ui: { kind: "built" },
2026
+ cli: { kind: "built", commands: ["naro backend users"] },
2027
+ mcp: { kind: "built", tools: ["list_backend_users"] }
2028
+ },
2029
+ {
2030
+ id: "backend-user-create",
2031
+ domain: "backend",
2032
+ title: "\uC720\uC800 \uB9CC\uB4E4\uAE30",
2033
+ routes: ["POST projects/[projectId]/backend/users"],
2034
+ dashboard: "/[teamSlug]/projects/[projectSlug]/auth",
2035
+ ui: { kind: "built" },
2036
+ cli: { kind: "built", commands: ["naro backend users create"] },
2037
+ mcp: {
2038
+ kind: "excluded",
2039
+ reason: "secret-value",
2040
+ note: "\uBE44\uBC00\uBC88\uD638\uAC00 \uB300\uD654\uC5D0 \uB0A8\uB294\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
2041
+ }
2042
+ },
2043
+ {
2044
+ id: "backend-user-delete",
2045
+ domain: "backend",
2046
+ title: "\uC720\uC800 \uC0AD\uC81C",
2047
+ routes: ["DELETE projects/[projectId]/backend/users/[userId]"],
2048
+ dashboard: "/[teamSlug]/projects/[projectSlug]/auth",
2049
+ ui: { kind: "built" },
2050
+ cli: { kind: "built", commands: ["naro backend users delete"] },
2051
+ mcp: {
2052
+ kind: "excluded",
2053
+ reason: "irreversible",
2054
+ note: "\uC720\uC800\uC640 \uC138\uC158\uC774 \uC0AC\uB77C\uC9C4\uB2E4. 2026-11-03 \uACB0\uC815(\uAD8C\uACE0\uB300\uB85C)."
2055
+ }
2056
+ },
2057
+ {
2058
+ id: "logs-runtime",
2059
+ domain: "logs",
2060
+ title: "\uB7F0\uD0C0\uC784 \uB85C\uADF8",
2061
+ routes: ["GET projects/[projectId]/logs"],
2062
+ ui: { kind: "built" },
2063
+ cli: { kind: "built", commands: ["naro logs"] },
2064
+ mcp: { kind: "built", tools: ["get_runtime_logs"] }
2065
+ },
2066
+ {
2067
+ id: "analytics",
2068
+ domain: "measurement",
2069
+ title: "Web Analytics",
2070
+ routes: ["GET projects/[projectId]/analytics"],
2071
+ ui: { kind: "built" },
2072
+ cli: { kind: "built", commands: ["naro analytics"] },
2073
+ mcp: { kind: "built", tools: ["get_analytics"] }
2074
+ },
2075
+ {
2076
+ id: "insights",
2077
+ domain: "measurement",
2078
+ title: "Speed Insights",
2079
+ routes: ["GET projects/[projectId]/speed-insights"],
2080
+ ui: { kind: "built" },
2081
+ cli: { kind: "built", commands: ["naro insights"] },
2082
+ mcp: { kind: "built", tools: ["get_speed_insights"] }
2083
+ },
2084
+ {
2085
+ id: "insights-toggle",
2086
+ domain: "measurement",
2087
+ title: "Speed Insights \uCE21\uC815 \uCF1C\uAE30\xB7\uB044\uAE30",
2088
+ routes: ["PUT projects/[projectId]/speed-insights"],
2089
+ ui: { kind: "built" },
2090
+ cli: {
2091
+ kind: "built",
2092
+ commands: ["naro insights enable", "naro insights disable"]
2093
+ },
2094
+ mcp: { kind: "built", tools: ["set_speed_insights"] }
2095
+ },
2096
+ {
2097
+ id: "insights-deployments",
2098
+ domain: "measurement",
2099
+ title: "Speed Insights \uBC30\uD3EC\uBCC4 \uBE44\uAD50",
2100
+ routes: ["GET projects/[projectId]/speed-insights/deployments"],
2101
+ ui: { kind: "built" },
2102
+ // naro insights가 배포별 표를 같이 읽는다(client.insights.overview).
2103
+ cli: { kind: "built" },
2104
+ // get_speed_insights가 route 없이 배포별 표를 같이 돌려준다(client.insights.overview).
2105
+ mcp: { kind: "built" }
2106
+ },
2107
+ {
2108
+ id: "insights-paths",
2109
+ domain: "measurement",
2110
+ title: "Speed Insights \uB77C\uC6B0\uD2B8\uBCC4 \uACBD\uB85C",
2111
+ routes: ["GET projects/[projectId]/speed-insights/paths"],
2112
+ ui: { kind: "built" },
2113
+ // naro insights --route가 읽는다(client.insights.paths).
2114
+ cli: { kind: "built" },
2115
+ // get_speed_insights에 route를 주면 경로를 돌려준다(client.insights.paths).
2116
+ mcp: { kind: "built" }
2117
+ },
2118
+ {
2119
+ id: "observability",
2120
+ domain: "measurement",
2121
+ title: "Observability",
2122
+ routes: ["GET projects/[projectId]/observability"],
2123
+ ui: { kind: "built" },
2124
+ cli: { kind: "built", commands: ["naro observability"] },
2125
+ mcp: { kind: "built", tools: ["get_observability"] }
2126
+ },
2127
+ {
2128
+ id: "firewall-list",
2129
+ domain: "firewall",
2130
+ title: "\uBC29\uD654\uBCBD \uADDC\uCE59 \uBAA9\uB85D",
2131
+ routes: ["GET projects/[projectId]/firewall"],
2132
+ ui: { kind: "built" },
2133
+ cli: { kind: "built", commands: ["naro firewall"] },
2134
+ mcp: { kind: "built", tools: ["list_firewall_rules"] }
2135
+ },
2136
+ {
2137
+ id: "firewall-add",
2138
+ domain: "firewall",
2139
+ title: "\uBC29\uD654\uBCBD \uADDC\uCE59 \uCD94\uAC00",
2140
+ routes: ["POST projects/[projectId]/firewall"],
2141
+ ui: { kind: "built" },
2142
+ cli: { kind: "built", commands: ["naro firewall add"] },
2143
+ mcp: { kind: "built", tools: ["add_firewall_rule"] }
2144
+ },
2145
+ {
2146
+ id: "firewall-update",
2147
+ domain: "firewall",
2148
+ title: "\uBC29\uD654\uBCBD \uADDC\uCE59 \uC218\uC815",
2149
+ routes: ["PATCH projects/[projectId]/firewall/[ruleId]"],
2150
+ ui: { kind: "built" },
2151
+ cli: {
2152
+ kind: "built",
2153
+ commands: ["naro firewall enable", "naro firewall disable"]
2154
+ },
2155
+ mcp: { kind: "built", tools: ["set_firewall_rule_enabled"] }
2156
+ },
2157
+ {
2158
+ id: "firewall-remove",
2159
+ domain: "firewall",
2160
+ title: "\uBC29\uD654\uBCBD \uADDC\uCE59 \uC0AD\uC81C",
2161
+ routes: ["DELETE projects/[projectId]/firewall/[ruleId]"],
2162
+ ui: { kind: "built" },
2163
+ cli: { kind: "built", commands: ["naro firewall remove"] },
2164
+ mcp: { kind: "built", tools: ["remove_firewall_rule"] }
2165
+ },
2166
+ {
2167
+ id: "flag-list",
2168
+ domain: "flags",
2169
+ title: "\uAE30\uB2A5 \uD50C\uB798\uADF8 \uBAA9\uB85D",
2170
+ routes: ["GET projects/[projectId]/flags"],
2171
+ ui: { kind: "built" },
2172
+ cli: { kind: "built", commands: ["naro flags"] },
2173
+ mcp: { kind: "built", tools: ["list_flags"] }
2174
+ },
2175
+ {
2176
+ id: "flag-add",
2177
+ domain: "flags",
2178
+ title: "\uAE30\uB2A5 \uD50C\uB798\uADF8 \uCD94\uAC00",
2179
+ routes: ["POST projects/[projectId]/flags"],
2180
+ ui: { kind: "built" },
2181
+ cli: { kind: "built", commands: ["naro flags add"] },
2182
+ mcp: { kind: "built", tools: ["add_flag"] }
2183
+ },
2184
+ {
2185
+ id: "flag-update",
2186
+ domain: "flags",
2187
+ title: "\uAE30\uB2A5 \uD50C\uB798\uADF8 \uC218\uC815",
2188
+ routes: ["PATCH projects/[projectId]/flags/[flagId]"],
2189
+ ui: { kind: "built" },
2190
+ cli: { kind: "built", commands: ["naro flags on", "naro flags off"] },
2191
+ mcp: { kind: "built", tools: ["set_flag"] }
2192
+ },
2193
+ {
2194
+ id: "flag-remove",
2195
+ domain: "flags",
2196
+ title: "\uAE30\uB2A5 \uD50C\uB798\uADF8 \uC0AD\uC81C",
2197
+ routes: ["DELETE projects/[projectId]/flags/[flagId]"],
2198
+ ui: { kind: "built" },
2199
+ cli: { kind: "built", commands: ["naro flags remove"] },
2200
+ mcp: { kind: "built", tools: ["remove_flag"] }
2201
+ },
2202
+ {
2203
+ id: "dashboard-link",
2204
+ domain: "guidance",
2205
+ title: "CLI\xB7MCP\uB85C \uBABB \uD558\uB294 \uAE30\uB2A5\uC758 \uB300\uC2DC\uBCF4\uB4DC \uB9C1\uD06C",
2206
+ ui: {
2207
+ kind: "excluded",
2208
+ reason: "dashboard-itself",
2209
+ note: "\uC0AC\uC6A9\uC790\uAC00 \uC774\uBBF8 \uB300\uC2DC\uBCF4\uB4DC\uC5D0 \uC788\uB2E4."
2210
+ },
2211
+ cli: { kind: "built", commands: ["naro dashboard"] },
2212
+ mcp: { kind: "built", tools: ["dashboard_link"] }
2213
+ }
2214
+ ];
2215
+ function dashboardNeeds(path) {
2216
+ return {
2217
+ team: path.includes("[teamSlug]"),
2218
+ project: path.includes("[projectSlug]")
2219
+ };
2220
+ }
2221
+ function dashboardUrl(baseUrl, path, slugs) {
2222
+ const filled2 = path.replace("[teamSlug]", () => {
2223
+ if (!slugs.teamSlug) throw new Error(`${path} needs a team slug`);
2224
+ return encodeURIComponent(slugs.teamSlug);
2225
+ }).replace("[projectSlug]", () => {
2226
+ if (!slugs.projectSlug) throw new Error(`${path} needs a project slug`);
2227
+ return encodeURIComponent(slugs.projectSlug);
2228
+ });
2229
+ return `${baseUrl.replace(/\/+$/, "")}${filled2}`;
2230
+ }
2231
+
313
2232
  // ../../packages/core/src/deployment.ts
314
2233
  var PHASES = [
315
2234
  "QUEUED",
@@ -453,7 +2372,7 @@ function quoted(span, slug, switchContext) {
453
2372
  }
454
2373
  }
455
2374
  var QUOTED = /`(naro [^`\n]+)`/g;
456
- var BARE = /(?<![`\w/-])naro ([a-z]+)((?: [a-z][a-z-]*)*)/g;
2375
+ var BARE = /(?<![`\w/-])naro ([a-z]+)((?: (?!naro\b)[a-z][a-z-]*)*)/g;
457
2376
  function bare(match, command, rest, after, slug, switchContext) {
458
2377
  const vocabulary = SUBCOMMANDS[command];
459
2378
  if (vocabulary === void 0) return match;
@@ -463,7 +2382,7 @@ function bare(match, command, rest, after, slug, switchContext) {
463
2382
  if (count2 === 0) return match;
464
2383
  const head = ["naro", command, ...words.slice(0, count2)];
465
2384
  const tail = words.slice(count2);
466
- const following = after.split(/\n|[.;,)](?:\s|$)/, 1)[0] ?? "";
2385
+ const following = after.split(/\n|[.;,)](?:\s|$)|\uE000/, 1)[0] ?? "";
467
2386
  if (/(?:^|\s)--env[\s=]/.test(following) || after.startsWith(` \u2014 ${PRODUCTION_SWITCH_NOTE}`))
468
2387
  return match;
469
2388
  const shown = head.join(" ");
@@ -633,7 +2552,13 @@ var DEPLOYMENT_ERROR_CODES = [
633
2552
  /** A deployment failure: the environment was deleted before its deployment finished. */
634
2553
  "ENVIRONMENT_DELETED",
635
2554
  /** The edge refused the environment's protection change; the database change is rolled back. */
636
- "ENVIRONMENT_PROTECTION_PUBLISH_FAILED"
2555
+ "ENVIRONMENT_PROTECTION_PUBLISH_FAILED",
2556
+ /** The variable came from the Supabase connection; only its card changes it (spec 2026-11-04 §6.3). */
2557
+ "ENV_MANAGED",
2558
+ /** The 10-minute Supabase sign-in cookie is gone or expired (spec 2026-11-04 §6.4). */
2559
+ "SUPABASE_SESSION_EXPIRED",
2560
+ /** The Supabase Management API refused or did not answer (spec 2026-11-04 §6.4). */
2561
+ "SUPABASE_API_FAILED"
637
2562
  ];
638
2563
  var DeploymentError = class extends Error {
639
2564
  code;
@@ -650,6 +2575,15 @@ var DeploymentError = class extends Error {
650
2575
  // ../../packages/core/src/functions.ts
651
2576
  var FUNCTION_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/;
652
2577
  var FUNCTION_WORKER_NAME_PREFIX = "naro-fn-";
2578
+ var FUNCTION_INVOKE_METHODS = [
2579
+ "GET",
2580
+ "POST",
2581
+ "PUT",
2582
+ "PATCH",
2583
+ "DELETE"
2584
+ ];
2585
+ var FUNCTION_INVOKE_MAX_RESPONSE_BYTES = 65536;
2586
+ var FUNCTION_INVOKE_TIMEOUT_MS = 12e4;
653
2587
  function isFunctionName(name) {
654
2588
  return FUNCTION_NAME_RE.test(name);
655
2589
  }
@@ -710,6 +2644,38 @@ function projectPath(teamSlug, projectSlug) {
710
2644
  // ../../packages/core/src/platform-mail.ts
711
2645
  var encoder = new TextEncoder();
712
2646
 
2647
+ // ../../packages/core/src/read-capped.ts
2648
+ async function readCapped(response, limit, signal) {
2649
+ const reader = response.body?.getReader();
2650
+ if (!reader) return { text: "", truncated: false };
2651
+ signal?.addEventListener(
2652
+ "abort",
2653
+ () => void reader.cancel().catch(() => void 0),
2654
+ { once: true }
2655
+ );
2656
+ const chunks = [];
2657
+ let size = 0;
2658
+ while (size <= limit) {
2659
+ const { done, value } = await reader.read();
2660
+ if (done) break;
2661
+ chunks.push(value);
2662
+ size += value.byteLength;
2663
+ }
2664
+ if (size > limit) await reader.cancel().catch(() => void 0);
2665
+ const all = new Uint8Array(Math.min(size, limit));
2666
+ let offset = 0;
2667
+ for (const chunk of chunks) {
2668
+ const part = chunk.subarray(
2669
+ 0,
2670
+ Math.min(chunk.byteLength, all.byteLength - offset)
2671
+ );
2672
+ all.set(part, offset);
2673
+ offset += part.byteLength;
2674
+ if (offset >= all.byteLength) break;
2675
+ }
2676
+ return { text: new TextDecoder().decode(all), truncated: size > limit };
2677
+ }
2678
+
713
2679
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
714
2680
  var external_exports = {};
715
2681
  __export(external_exports, {
@@ -15614,7 +17580,7 @@ var REQUESTS = {
15614
17580
  blocked: "Sites on this team answer 503 until the month ends (UTC).",
15615
17581
  running: "Sites keep running for now.",
15616
17582
  button: "View usage",
15617
- footer: (team) => `You're receiving this because you're an owner or billing member of ${team} on naro.`
17583
+ footer: (team) => `You're receiving this because you're an owner or billing member of ${team} on Naro.`
15618
17584
  },
15619
17585
  ko: {
15620
17586
  subject80: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uC694\uCCAD \uD55C\uB3C4\uC758 80%\uB97C \uC37C\uC2B5\uB2C8\uB2E4`,
@@ -15624,7 +17590,7 @@ var REQUESTS = {
15624
17590
  blocked: "\uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC774 \uD300\uC758 \uC0AC\uC774\uD2B8\uB294 503\uC73C\uB85C \uC751\uB2F5\uD569\uB2C8\uB2E4.",
15625
17591
  running: "\uC9C0\uAE08\uC740 \uC0AC\uC774\uD2B8\uAC00 \uACC4\uC18D \uB3D9\uC791\uD569\uB2C8\uB2E4.",
15626
17592
  button: "\uC0AC\uC6A9\uB7C9 \uBCF4\uAE30",
15627
- footer: (team) => `naro\uC5D0\uC11C ${team} \uD300\uC758 owner \uB610\uB294 billing \uC5ED\uD560\uC774\uB77C \uC774 \uBA54\uC77C\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`
17593
+ footer: (team) => `Naro\uC5D0\uC11C ${team} \uD300\uC758 owner \uB610\uB294 billing \uC5ED\uD560\uC774\uB77C \uC774 \uBA54\uC77C\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`
15628
17594
  },
15629
17595
  ja: {
15630
17596
  subject80: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u4E0A\u9650\u306E80%\u3092\u4F7F\u7528\u3057\u307E\u3057\u305F`,
@@ -15634,7 +17600,7 @@ var REQUESTS = {
15634
17600
  blocked: "\u6708\u672B(UTC)\u307E\u3067\u3001\u3053\u306E\u30C1\u30FC\u30E0\u306E\u30B5\u30A4\u30C8\u306F503\u3092\u8FD4\u3057\u307E\u3059\u3002",
15635
17601
  running: "\u73FE\u5728\u3001\u30B5\u30A4\u30C8\u306F\u5F15\u304D\u7D9A\u304D\u52D5\u4F5C\u3057\u3066\u3044\u307E\u3059\u3002",
15636
17602
  button: "\u4F7F\u7528\u91CF\u3092\u898B\u308B",
15637
- footer: (team) => `naro \u3067 ${team} \u30C1\u30FC\u30E0\u306E owner \u307E\u305F\u306F billing \u30ED\u30FC\u30EB\u306E\u305F\u3081\u3001\u3053\u306E\u30E1\u30FC\u30EB\u3092\u304A\u9001\u308A\u3057\u3066\u3044\u307E\u3059\u3002`
17603
+ footer: (team) => `Naro \u3067 ${team} \u30C1\u30FC\u30E0\u306E owner \u307E\u305F\u306F billing \u30ED\u30FC\u30EB\u306E\u305F\u3081\u3001\u3053\u306E\u30E1\u30FC\u30EB\u3092\u304A\u9001\u308A\u3057\u3066\u3044\u307E\u3059\u3002`
15638
17604
  }
15639
17605
  };
15640
17606
  var BUILDS = {
@@ -16315,10 +18281,13 @@ export default {
16315
18281
  },
16316
18282
  };
16317
18283
  `;
16318
- var OUTPUT_DIR = "dist";
18284
+ var VINEXT_OUTPUT_DIR = "dist";
16319
18285
  var SERVER_DIR = "server";
16320
18286
  var CLIENT_DIR = "client";
16321
18287
  var WRANGLER_OUTPUT_NAME = "wrangler.json";
18288
+ function vinextWranglerConfigPath(appRoot) {
18289
+ return join6(appRoot, VINEXT_OUTPUT_DIR, SERVER_DIR, WRANGLER_OUTPUT_NAME);
18290
+ }
16322
18291
  async function exists3(path) {
16323
18292
  try {
16324
18293
  await access5(path);
@@ -16342,7 +18311,7 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
16342
18311
  if (await exists3(workerEntryPath)) {
16343
18312
  throw new DeploymentError(
16344
18313
  "ARTIFACT_INVALID",
16345
- `The project already has a ${GENERATED_WORKER_ENTRY}. naro writes its vinext Worker entry under that name and will not overwrite a file of the project's.`
18314
+ `The project already has a ${GENERATED_WORKER_ENTRY}. Naro writes its vinext Worker entry under that name and will not overwrite a file of the project's.`
16346
18315
  );
16347
18316
  }
16348
18317
  const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
@@ -16374,7 +18343,7 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
16374
18343
  compatibility_flags: ["nodejs_compat"],
16375
18344
  main: `./${GENERATED_WORKER_ENTRY}`,
16376
18345
  assets: {
16377
- directory: `${OUTPUT_DIR}/${CLIENT_DIR}`,
18346
+ directory: `${VINEXT_OUTPUT_DIR}/${CLIENT_DIR}`,
16378
18347
  binding: "ASSETS"
16379
18348
  }
16380
18349
  },
@@ -16395,7 +18364,7 @@ function parseVinextCheckReport(output) {
16395
18364
  const plain = output.replace(ANSI_ESCAPE, "");
16396
18365
  const match = SUMMARY_LINE.exec(plain);
16397
18366
  if (!match) return null;
16398
- const issueLines = [
18367
+ const issueLines2 = [
16399
18368
  ...new Set(
16400
18369
  plain.split("\n").map((line) => ISSUE_LINE.exec(line)?.[1]?.trim()).filter((line) => Boolean(line))
16401
18370
  )
@@ -16410,7 +18379,7 @@ function parseVinextCheckReport(output) {
16410
18379
  supported: Number(match[2]),
16411
18380
  partial: Number(match[3]),
16412
18381
  issues: Number(match[4]),
16413
- issueLines,
18382
+ issueLines: issueLines2,
16414
18383
  partialLines
16415
18384
  };
16416
18385
  }
@@ -16473,14 +18442,14 @@ function moduleMatchers(rules) {
16473
18442
  }
16474
18443
  async function readVinextArtifact(appRoot, identity) {
16475
18444
  try {
16476
- const outputRoot = resolve2(appRoot, OUTPUT_DIR);
18445
+ const outputRoot = resolve2(appRoot, VINEXT_OUTPUT_DIR);
16477
18446
  const serverRoot = join6(outputRoot, SERVER_DIR);
16478
18447
  const clientRoot = join6(outputRoot, CLIENT_DIR);
16479
18448
  const configPath = join6(serverRoot, WRANGLER_OUTPUT_NAME);
16480
18449
  if (!await exists3(configPath)) {
16481
18450
  throw new DeploymentError(
16482
18451
  "ARTIFACT_INVALID",
16483
- `vinext build produced no ${OUTPUT_DIR}/${SERVER_DIR}/${WRANGLER_OUTPUT_NAME}.`
18452
+ `vinext build produced no ${VINEXT_OUTPUT_DIR}/${SERVER_DIR}/${WRANGLER_OUTPUT_NAME}.`
16484
18453
  );
16485
18454
  }
16486
18455
  const metadata = JSON.parse(
@@ -16762,10 +18731,10 @@ async function angularOutput(root) {
16762
18731
  } catch {
16763
18732
  workspace = null;
16764
18733
  }
16765
- const project = Object.values(workspace?.projects ?? {}).find(
18734
+ const project2 = Object.values(workspace?.projects ?? {}).find(
16766
18735
  (entry) => entry.projectType === "application"
16767
18736
  );
16768
- const build = project?.architect?.build;
18737
+ const build = project2?.architect?.build;
16769
18738
  const outputPath = build?.options?.outputPath;
16770
18739
  const legacy = build?.builder?.endsWith(":browser") ?? false;
16771
18740
  const clean = (path) => path.replace(/^\.?\/+/, "").replace(/\/+$/, "");
@@ -16777,7 +18746,7 @@ async function angularOutput(root) {
16777
18746
  ""
16778
18747
  );
16779
18748
  const name = Object.entries(workspace?.projects ?? {}).find(
16780
- ([, entry]) => entry === project
18749
+ ([, entry]) => entry === project2
16781
18750
  )?.[0];
16782
18751
  if (name) return `dist/${name}/browser`;
16783
18752
  throw new DeploymentError(
@@ -18552,13 +20521,8 @@ function createAwsMiddlewareCloudflareAppStrategy() {
18552
20521
  }
18553
20522
 
18554
20523
  // ../../packages/build/src/strategy/strategies/vinext-cloudflare.ts
18555
- import { join as join16 } from "path";
18556
20524
  var VINEXT_MIN_NEXT_MAJOR = 16;
18557
20525
  var VINEXT_BUILD_SCRIPTS = ["esbuild", "workerd"];
18558
- var VINEXT_OUTPUT = {
18559
- outputRoot: "dist",
18560
- bundleDir: "dist/.naro-bundle"
18561
- };
18562
20526
  async function runCheck(context, observer) {
18563
20527
  if (!context.toolchain.vinext) return null;
18564
20528
  const held = [];
@@ -18708,13 +20672,8 @@ function createVinextCloudflareStrategy(options) {
18708
20672
  context,
18709
20673
  {
18710
20674
  appRoot: fork.appRoot,
18711
- wranglerConfigPath: join16(
18712
- fork.appRoot,
18713
- "dist",
18714
- "server",
18715
- "wrangler.json"
18716
- ),
18717
- ...VINEXT_OUTPUT
20675
+ wranglerConfigPath: vinextWranglerConfigPath(fork.appRoot),
20676
+ outputRoot: VINEXT_OUTPUT_DIR
18718
20677
  },
18719
20678
  observer
18720
20679
  );
@@ -18733,7 +20692,7 @@ function createVinextCloudflareStrategy(options) {
18733
20692
  // ../../packages/build/src/strategy/workspace.ts
18734
20693
  import { execFile } from "child_process";
18735
20694
  import { cp, mkdir as mkdir2, readdir as readdir4, rm as rm2 } from "fs/promises";
18736
- import { basename as basename2, dirname as dirname3, join as join17, relative as relative5, resolve as resolve6 } from "path";
20695
+ import { basename as basename2, dirname as dirname3, join as join16, relative as relative5, resolve as resolve6 } from "path";
18737
20696
  import { promisify } from "util";
18738
20697
  var execFileAsync = promisify(execFile);
18739
20698
  var SKIPPED_ANYWHERE = /* @__PURE__ */ new Set(["node_modules"]);
@@ -18756,14 +20715,14 @@ async function copyInstalledPackages(source, target2) {
18756
20715
  const walk2 = async (dir) => {
18757
20716
  for (const entry of await readdir4(dir, { withFileTypes: true })) {
18758
20717
  if (!entry.isDirectory() || entry.name === ".git") continue;
18759
- const path = join17(dir, entry.name);
20718
+ const path = join16(dir, entry.name);
18760
20719
  if (entry.name === "node_modules") installs.push(path);
18761
20720
  else await walk2(path);
18762
20721
  }
18763
20722
  };
18764
20723
  await walk2(origin);
18765
20724
  for (const install of installs)
18766
- await copyTree(install, join17(target2, relative5(origin, install)));
20725
+ await copyTree(install, join16(target2, relative5(origin, install)));
18767
20726
  }
18768
20727
  async function copyTree(from, to) {
18769
20728
  await mkdir2(dirname3(to), { recursive: true });
@@ -18778,7 +20737,7 @@ async function copyTree(from, to) {
18778
20737
  await cp(from, to, { recursive: true, verbatimSymlinks: true });
18779
20738
  }
18780
20739
  function forkedAppRoot(checkout, appRoot, forkCheckout) {
18781
- return join17(forkCheckout, relative5(checkout, appRoot));
20740
+ return join16(forkCheckout, relative5(checkout, appRoot));
18782
20741
  }
18783
20742
 
18784
20743
  // ../../packages/build/src/local-build-executor.ts
@@ -18801,7 +20760,7 @@ async function withRouteList(artifact, list, appRoot, observer) {
18801
20760
  }
18802
20761
  }
18803
20762
  var defaultLocalBuildDependencies = {
18804
- createWorkspace: () => mkdtemp(join18(tmpdir(), "next-deploy-build-")),
20763
+ createWorkspace: () => mkdtemp(join17(tmpdir(), "next-deploy-build-")),
18805
20764
  removeWorkspace: (path) => rm3(path, { recursive: true, force: true }),
18806
20765
  runCommand,
18807
20766
  detectPackageManager,
@@ -18836,7 +20795,7 @@ async function readInstalledNextVersion(appRoot, checkout) {
18836
20795
  for (; ; ) {
18837
20796
  try {
18838
20797
  const source = await readFile13(
18839
- join18(dir, "node_modules", "next", "package.json"),
20798
+ join17(dir, "node_modules", "next", "package.json"),
18840
20799
  "utf8"
18841
20800
  );
18842
20801
  const version3 = JSON.parse(source).version;
@@ -18991,8 +20950,8 @@ var LocalBuildExecutor = class {
18991
20950
  }
18992
20951
  async build(input, observer, cache) {
18993
20952
  const workspace = await this.dependencies.createWorkspace();
18994
- const checkout = join18(workspace, "checkout");
18995
- const pmCacheDir = join18(workspace, ".pm-store");
20953
+ const checkout = join17(workspace, "checkout");
20954
+ const pmCacheDir = join17(workspace, ".pm-store");
18996
20955
  let cacheSeq = 0;
18997
20956
  const commandAvailable = this.dependencies.commandAvailable ?? hasCommand;
18998
20957
  const compressor = "zstd";
@@ -19001,7 +20960,7 @@ var LocalBuildExecutor = class {
19001
20960
  transport: cache,
19002
20961
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
19003
20962
  runCommand: this.dependencies.runCommand,
19004
- tmpFile: (suffix) => join18(workspace, `cache-${String(cacheSeq++)}${suffix}`),
20963
+ tmpFile: (suffix) => join17(workspace, `cache-${String(cacheSeq++)}${suffix}`),
19005
20964
  // One latch for the whole build so a disabled cache warns once.
19006
20965
  report: { unavailable: false }
19007
20966
  } : null;
@@ -19209,7 +21168,7 @@ var LocalBuildExecutor = class {
19209
21168
  }
19210
21169
  }
19211
21170
  await notifyStatus(observer, "BUILDING");
19212
- const buildCacheDir = resolved.preset.cachePath ? join18(appRoot, resolved.preset.cachePath) : null;
21171
+ const buildCacheDir = resolved.preset.cachePath ? join17(appRoot, resolved.preset.cachePath) : null;
19213
21172
  const buildCacheKey = sanitizeCacheKey(input.branch);
19214
21173
  if (cachePorts && buildCacheDir) {
19215
21174
  await tryRestore(
@@ -19233,13 +21192,13 @@ var LocalBuildExecutor = class {
19233
21192
  entries: fingerprintEntries,
19234
21193
  nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown"
19235
21194
  });
19236
- const pristine = join18(workspace, "pristine");
21195
+ const pristine = join17(workspace, "pristine");
19237
21196
  const fork = this.dependencies.forkWorkspace ?? defaultFork;
19238
21197
  await fork(checkout, pristine);
19239
21198
  let forks = 0;
19240
21199
  const resolverConcurrency = this.dependencies.resolverConcurrency ?? 1;
19241
21200
  const forkWorkspace = async (label2) => {
19242
- const target2 = join18(
21201
+ const target2 = join17(
19243
21202
  workspace,
19244
21203
  "forks",
19245
21204
  `${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
@@ -19535,12 +21494,12 @@ async function errorFromResponse(response, context) {
19535
21494
  // ../../packages/sdk/src/session.ts
19536
21495
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
19537
21496
  import { homedir } from "os";
19538
- import { join as join19 } from "path";
21497
+ import { join as join18 } from "path";
19539
21498
  function credentialsDir(home = homedir()) {
19540
- return join19(home, ".naro");
21499
+ return join18(home, ".naro");
19541
21500
  }
19542
21501
  function credentialsFile(home = homedir()) {
19543
- return join19(credentialsDir(home), "credentials.json");
21502
+ return join18(credentialsDir(home), "credentials.json");
19544
21503
  }
19545
21504
  function saveSession(session, home = homedir()) {
19546
21505
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -19681,13 +21640,13 @@ function page(options) {
19681
21640
  <head>
19682
21641
  <meta charset="utf-8">
19683
21642
  <meta name="viewport" content="width=device-width,initial-scale=1">
19684
- <title>${title} \xB7 naro</title>
21643
+ <title>${title} \xB7 Naro</title>
19685
21644
  <link rel="icon" href="${FAVICON}">
19686
21645
  <style>:root{--tone:${tone.color};--tone-soft:${tone.soft};--tone-edge:${tone.edge};--glow:${tone.glow}}${STYLES}</style>
19687
21646
  </head>
19688
21647
  <body>
19689
21648
  <main class="card">
19690
- <span class="brand"><span class="brand-mark">n</span>naro</span>
21649
+ <span class="brand"><span class="brand-mark">n</span>Naro</span>
19691
21650
  <div class="glyph">${tone.icon}</div>
19692
21651
  <h1>${title}</h1>
19693
21652
  <p>${escapeHtml(options.message)}</p>
@@ -19976,6 +21935,233 @@ function openBrowser(url2) {
19976
21935
  child.unref();
19977
21936
  }
19978
21937
 
21938
+ // ../../packages/sdk/src/dns.ts
21939
+ var DnsApi = class {
21940
+ constructor(client) {
21941
+ this.client = client;
21942
+ }
21943
+ base(teamSlug, suffix = "") {
21944
+ return `/api/teams/${encodeURIComponent(teamSlug)}/dns-zones${suffix}`;
21945
+ }
21946
+ zone(teamSlug, zoneId, suffix = "") {
21947
+ return this.base(teamSlug, `/${encodeURIComponent(zoneId)}${suffix}`);
21948
+ }
21949
+ /** The team's zones and the project domains each one could carry. */
21950
+ async state(teamSlug) {
21951
+ return this.client.request(this.base(teamSlug), {
21952
+ context: "Read naro DNS"
21953
+ });
21954
+ }
21955
+ /** Starts moving a domain onto naro DNS; the zone answers its name servers. */
21956
+ async addZone(teamSlug, name) {
21957
+ const body = await this.client.request(
21958
+ this.base(teamSlug),
21959
+ { method: "POST", body: { name }, context: "Start a DNS move" }
21960
+ );
21961
+ return body.zone;
21962
+ }
21963
+ /** Cancels a move, or takes an active zone off naro DNS. */
21964
+ async removeZone(teamSlug, zoneId) {
21965
+ await this.client.request(this.zone(teamSlug, zoneId), {
21966
+ method: "DELETE",
21967
+ context: "Remove a DNS zone"
21968
+ });
21969
+ }
21970
+ /** Reads the zone's status and asks Cloudflare for an activation check. */
21971
+ async check(teamSlug, zoneId) {
21972
+ const body = await this.client.request(
21973
+ this.zone(teamSlug, zoneId, "/check"),
21974
+ { method: "POST", context: "Check a DNS zone" }
21975
+ );
21976
+ return body.zone;
21977
+ }
21978
+ /** The mail records (MX, SPF, DMARC, DKIM) to keep before the move. */
21979
+ async mailCheck(teamSlug, zoneId) {
21980
+ return this.client.request(
21981
+ this.zone(teamSlug, zoneId, "/precheck"),
21982
+ { context: "Check mail records" }
21983
+ );
21984
+ }
21985
+ /** The records Cloudflare's scan of the current DNS found. */
21986
+ async scan(teamSlug, zoneId) {
21987
+ const body = await this.client.request(
21988
+ this.zone(teamSlug, zoneId, "/scan"),
21989
+ { context: "Read the DNS scan" }
21990
+ );
21991
+ return body.records;
21992
+ }
21993
+ /** Adds every scanned record to the zone. */
21994
+ async importScan(teamSlug, zoneId) {
21995
+ return this.client.request(
21996
+ this.zone(teamSlug, zoneId, "/scan"),
21997
+ { method: "POST", context: "Import the DNS scan" }
21998
+ );
21999
+ }
22000
+ async records(teamSlug, zoneId) {
22001
+ return this.client.request(
22002
+ this.zone(teamSlug, zoneId, "/records"),
22003
+ { context: "List DNS records" }
22004
+ );
22005
+ }
22006
+ /** Adds a record, always DNS only. */
22007
+ async addRecord(teamSlug, zoneId, input) {
22008
+ const body = await this.client.request(
22009
+ this.zone(teamSlug, zoneId, "/records"),
22010
+ { method: "POST", body: input, context: "Add a DNS record" }
22011
+ );
22012
+ return body.record;
22013
+ }
22014
+ /** Overwrites the whole record; its type cannot change. */
22015
+ async replaceRecord(teamSlug, zoneId, recordId, input) {
22016
+ const body = await this.client.request(
22017
+ this.zone(teamSlug, zoneId, `/records/${encodeURIComponent(recordId)}`),
22018
+ { method: "PATCH", body: input, context: "Change a DNS record" }
22019
+ );
22020
+ return body.record;
22021
+ }
22022
+ async removeRecord(teamSlug, zoneId, recordId) {
22023
+ await this.client.request(
22024
+ this.zone(teamSlug, zoneId, `/records/${encodeURIComponent(recordId)}`),
22025
+ { method: "DELETE", context: "Remove a DNS record" }
22026
+ );
22027
+ }
22028
+ };
22029
+
22030
+ // ../../packages/sdk/src/edge-config.ts
22031
+ var project = (projectId, suffix) => `/api/projects/${encodeURIComponent(projectId)}${suffix}`;
22032
+ var FirewallApi = class {
22033
+ constructor(client) {
22034
+ this.client = client;
22035
+ }
22036
+ async list(projectId) {
22037
+ const body = await this.client.request(
22038
+ project(projectId, "/firewall"),
22039
+ { context: "List firewall rules" }
22040
+ );
22041
+ return body.rules;
22042
+ }
22043
+ /** `VALIDATION_FAILED` for a bad kind or value, `RULE_LIMIT` past the cap. */
22044
+ async add(projectId, input) {
22045
+ const body = {
22046
+ kind: input.kind,
22047
+ value: input.value
22048
+ };
22049
+ if (input.rateLimit !== void 0) body.rate_limit = input.rateLimit;
22050
+ if (input.periodSeconds !== void 0)
22051
+ body.period_seconds = input.periodSeconds;
22052
+ if (input.enabled !== void 0) body.enabled = input.enabled;
22053
+ const answer = await this.client.request(
22054
+ project(projectId, "/firewall"),
22055
+ { method: "POST", body, context: "Add firewall rule" }
22056
+ );
22057
+ return answer.rule;
22058
+ }
22059
+ async setEnabled(projectId, ruleId, enabled) {
22060
+ const answer = await this.client.request(
22061
+ project(projectId, `/firewall/${encodeURIComponent(ruleId)}`),
22062
+ { method: "PATCH", body: { enabled }, context: "Change firewall rule" }
22063
+ );
22064
+ return answer.rule;
22065
+ }
22066
+ async remove(projectId, ruleId) {
22067
+ await this.client.request(
22068
+ project(projectId, `/firewall/${encodeURIComponent(ruleId)}`),
22069
+ { method: "DELETE", context: "Remove firewall rule" }
22070
+ );
22071
+ }
22072
+ };
22073
+ var FlagsApi = class {
22074
+ constructor(client) {
22075
+ this.client = client;
22076
+ }
22077
+ async list(projectId) {
22078
+ const body = await this.client.request(
22079
+ project(projectId, "/flags"),
22080
+ { context: "List flags" }
22081
+ );
22082
+ return body.flags;
22083
+ }
22084
+ /** Off unless `enabled`. `FLAG_LIMIT` past 32. */
22085
+ async add(projectId, name, enabled = false) {
22086
+ const answer = await this.client.request(
22087
+ project(projectId, "/flags"),
22088
+ { method: "POST", body: { name, enabled }, context: "Add flag" }
22089
+ );
22090
+ return answer.flag;
22091
+ }
22092
+ async setEnabled(projectId, flagId, enabled) {
22093
+ const answer = await this.client.request(
22094
+ project(projectId, `/flags/${encodeURIComponent(flagId)}`),
22095
+ { method: "PATCH", body: { enabled }, context: "Change flag" }
22096
+ );
22097
+ return answer.flag;
22098
+ }
22099
+ async remove(projectId, flagId) {
22100
+ await this.client.request(
22101
+ project(projectId, `/flags/${encodeURIComponent(flagId)}`),
22102
+ { method: "DELETE", context: "Remove flag" }
22103
+ );
22104
+ }
22105
+ };
22106
+
22107
+ // ../../packages/sdk/src/ops.ts
22108
+ var BillingApi = class {
22109
+ constructor(client) {
22110
+ this.client = client;
22111
+ }
22112
+ /** `usage.read`: `403 FORBIDDEN "Your role cannot read this team's billing."` otherwise. */
22113
+ async get(teamSlug) {
22114
+ const body = await this.client.request(`/api/teams/${encodeURIComponent(teamSlug)}/billing`, {
22115
+ context: "Read billing"
22116
+ });
22117
+ return {
22118
+ team: body.team,
22119
+ subscription: body.subscription,
22120
+ canManage: body.canManage,
22121
+ overage: body.overage
22122
+ };
22123
+ }
22124
+ };
22125
+ var ObservabilityApi = class {
22126
+ constructor(client) {
22127
+ this.client = client;
22128
+ }
22129
+ /** 7d unless given; `400 VALIDATION_FAILED "range must be 24h, 7d or 30d."` otherwise. */
22130
+ async report(projectId, options = {}) {
22131
+ const query = options.range === void 0 ? "" : `?range=${encodeURIComponent(options.range)}`;
22132
+ return this.client.request(
22133
+ `/api/projects/${encodeURIComponent(projectId)}/observability${query}`,
22134
+ { context: "Read observability" }
22135
+ );
22136
+ }
22137
+ };
22138
+
22139
+ // ../../packages/sdk/src/supabase-links.ts
22140
+ var SupabaseApi = class {
22141
+ constructor(client) {
22142
+ this.client = client;
22143
+ }
22144
+ async links(projectId) {
22145
+ const body = await this.client.request(
22146
+ `/api/projects/${encodeURIComponent(projectId)}/supabase`,
22147
+ { context: "Read Supabase links" }
22148
+ );
22149
+ return body.links;
22150
+ }
22151
+ /**
22152
+ * Removes the link of one target (production, preview or an environment's
22153
+ * slug), or every link the role may write when `target` is left out, with
22154
+ * the env rows they hold. Answers how many went.
22155
+ */
22156
+ async disconnect(projectId, options = {}) {
22157
+ const query = options.target === void 0 ? "" : `?target=${encodeURIComponent(options.target)}`;
22158
+ return this.client.request(
22159
+ `/api/projects/${encodeURIComponent(projectId)}/supabase${query}`,
22160
+ { method: "DELETE", context: "Disconnect Supabase" }
22161
+ );
22162
+ }
22163
+ };
22164
+
19979
22165
  // ../../packages/sdk/src/environments.ts
19980
22166
  function isReservedEnvTarget(value) {
19981
22167
  return RESERVED_ENVIRONMENT_SLUGS.includes(value);
@@ -20110,6 +22296,12 @@ var NaroClient = class _NaroClient {
20110
22296
  deployments;
20111
22297
  env;
20112
22298
  domains;
22299
+ dns;
22300
+ firewall;
22301
+ flags;
22302
+ billing;
22303
+ observability;
22304
+ supabase;
20113
22305
  logs;
20114
22306
  db;
20115
22307
  backend;
@@ -20133,6 +22325,12 @@ var NaroClient = class _NaroClient {
20133
22325
  this.deployments = new DeploymentsApi(this);
20134
22326
  this.env = new EnvApi(this);
20135
22327
  this.domains = new DomainsApi(this);
22328
+ this.dns = new DnsApi(this);
22329
+ this.firewall = new FirewallApi(this);
22330
+ this.flags = new FlagsApi(this);
22331
+ this.billing = new BillingApi(this);
22332
+ this.observability = new ObservabilityApi(this);
22333
+ this.supabase = new SupabaseApi(this);
20136
22334
  this.logs = new LogsApi(this);
20137
22335
  this.db = new DbApi(this);
20138
22336
  this.backend = new BackendApi(this);
@@ -20217,7 +22415,93 @@ var TeamsApi = class {
20217
22415
  { context: "Read team usage" }
20218
22416
  );
20219
22417
  }
22418
+ path(teamSlug, suffix = "") {
22419
+ return `/api/teams/${encodeURIComponent(teamSlug)}${suffix}`;
22420
+ }
22421
+ /** A new team, owned by the caller. `TEAM_SLUG_TAKEN` (409) for a slug in use. */
22422
+ async create(slug, name) {
22423
+ const body = await this.client.request("/api/teams", {
22424
+ method: "POST",
22425
+ body: { slug, name },
22426
+ context: "Create team"
22427
+ });
22428
+ return teamSummary(body.team);
22429
+ }
22430
+ /** Renames the team or changes its slug (`team.update`); only the fields given. */
22431
+ async update(teamSlug, patch) {
22432
+ const body = await this.client.request(
22433
+ this.path(teamSlug),
22434
+ { method: "PATCH", body: patch, context: "Update team" }
22435
+ );
22436
+ return teamSummary(body.team);
22437
+ }
22438
+ /**
22439
+ * Deletes the team with its projects and their data. Owner only;
22440
+ * `PERSONAL_TEAM` and `SUBSCRIPTION_ACTIVE` (cancel the subscription first)
22441
+ * refuse it. The route tears every project down before it answers, so this
22442
+ * waits two minutes, like `projects.delete`.
22443
+ */
22444
+ async delete(teamSlug) {
22445
+ await this.client.request(this.path(teamSlug), {
22446
+ method: "DELETE",
22447
+ context: "Delete team",
22448
+ timeoutMs: PROVISION_TIMEOUT_MS
22449
+ });
22450
+ }
22451
+ /** The members the caller may see: all of them with `team.read`, else only themselves. */
22452
+ async members(teamSlug) {
22453
+ const body = await this.client.request(
22454
+ this.path(teamSlug, "/members"),
22455
+ { context: "List team members" }
22456
+ );
22457
+ return body.members;
22458
+ }
22459
+ /** `team.members.manage`; `LAST_OWNER` (409) keeps the team's last owner. */
22460
+ async setMemberRole(teamSlug, userId, role) {
22461
+ await this.client.request(
22462
+ this.path(teamSlug, `/members/${encodeURIComponent(userId)}`),
22463
+ { method: "PATCH", body: { role }, context: "Change member role" }
22464
+ );
22465
+ }
22466
+ /** Removes a member, or leaves the team when it is the caller. */
22467
+ async removeMember(teamSlug, userId) {
22468
+ await this.client.request(
22469
+ this.path(teamSlug, `/members/${encodeURIComponent(userId)}`),
22470
+ { method: "DELETE", context: "Remove member" }
22471
+ );
22472
+ }
22473
+ async invites(teamSlug) {
22474
+ const body = await this.client.request(
22475
+ this.path(teamSlug, "/invites"),
22476
+ { context: "List invites" }
22477
+ );
22478
+ return body.invites;
22479
+ }
22480
+ /**
22481
+ * A new invite link; with `email` the control plane also mails it.
22482
+ * `PERSONAL_TEAM` refuses a personal team, `INVALID_EMAIL` a bad address.
22483
+ */
22484
+ async createInvite(teamSlug, options = {}) {
22485
+ return this.client.request(
22486
+ this.path(teamSlug, "/invites"),
22487
+ { method: "POST", body: options, context: "Create invite" }
22488
+ );
22489
+ }
22490
+ async revokeInvite(teamSlug, inviteId) {
22491
+ await this.client.request(
22492
+ this.path(teamSlug, `/invites/${encodeURIComponent(inviteId)}`),
22493
+ { method: "DELETE", context: "Revoke invite" }
22494
+ );
22495
+ }
20220
22496
  };
22497
+ function teamSummary(row) {
22498
+ return {
22499
+ id: row.id,
22500
+ slug: row.slug,
22501
+ name: row.name,
22502
+ isPersonal: row.is_personal
22503
+ };
22504
+ }
20221
22505
  var ProjectsApi = class {
20222
22506
  constructor(client) {
20223
22507
  this.client = client;
@@ -20236,6 +22520,28 @@ var ProjectsApi = class {
20236
22520
  );
20237
22521
  return projectInfo(body.project);
20238
22522
  }
22523
+ /**
22524
+ * Deletes the project and everything under it: its workers, its D1 and
22525
+ * Storage, its domains, environments and deployment history. It cannot be
22526
+ * undone. `project.delete` (owner and admin); a suspended project is `403
22527
+ * FORBIDDEN`, one that is gone or out of sight `404 NOT_FOUND`. Only the
22528
+ * CLI calls it, behind typing the name — naro-mcp has no tool for it
22529
+ * (project_cli_mcp_backlog #6).
22530
+ *
22531
+ * Two minutes, like `backend.enable`: the route answers only after it has
22532
+ * torn down the workers, D1s, backends and environments one after another
22533
+ * (PR #420 review). A TIMEOUT does not mean the project is still there.
22534
+ */
22535
+ async delete(projectId) {
22536
+ await this.client.request(
22537
+ `/api/projects/${encodeURIComponent(projectId)}`,
22538
+ {
22539
+ method: "DELETE",
22540
+ context: "Delete project",
22541
+ timeoutMs: PROVISION_TIMEOUT_MS
22542
+ }
22543
+ );
22544
+ }
20239
22545
  /**
20240
22546
  * Connects a project to a GitHub repository, turning `git push` into a
20241
22547
  * deployment.
@@ -20269,6 +22575,52 @@ var ProjectsApi = class {
20269
22575
  );
20270
22576
  return projectInfo(body.project);
20271
22577
  }
22578
+ /**
22579
+ * The dashboard's General settings: only the fields given are sent, and the
22580
+ * control plane checks them (`400 VALIDATION_FAILED "Check the project
22581
+ * settings."`, `SUBDOMAIN_INVALID`, `SUBDOMAIN_RESERVED`, `409
22582
+ * SUBDOMAIN_TAKEN`). A new subdomain moves the project's URL at once: the
22583
+ * old `{subdomain}.naro.sh` stops answering. Build settings need
22584
+ * `project.update`.
22585
+ */
22586
+ async update(projectId, patch) {
22587
+ const body = await this.client.request(
22588
+ `/api/projects/${encodeURIComponent(projectId)}`,
22589
+ { method: "PATCH", body: patch, context: "Update project" }
22590
+ );
22591
+ return projectInfo(body.project);
22592
+ }
22593
+ /** Whether a subdomain is free: `reason` is `taken`, `reserved` or the format problem. */
22594
+ async subdomainAvailability(name) {
22595
+ return this.client.request(
22596
+ `/api/subdomains/availability?name=${encodeURIComponent(name)}`,
22597
+ { context: "Check subdomain" }
22598
+ );
22599
+ }
22600
+ /** The project roles given to the team's contributors. */
22601
+ async access(projectId) {
22602
+ const body = await this.client.request(`/api/projects/${encodeURIComponent(projectId)}/access`, {
22603
+ context: "List project access"
22604
+ });
22605
+ return body.assignments;
22606
+ }
22607
+ /** Gives a contributor a project role (`project.update`); a member of another role is refused. */
22608
+ async grantAccess(projectId, userId, role) {
22609
+ await this.client.request(
22610
+ `/api/projects/${encodeURIComponent(projectId)}/access`,
22611
+ {
22612
+ method: "PUT",
22613
+ body: { userId, role },
22614
+ context: "Grant project access"
22615
+ }
22616
+ );
22617
+ }
22618
+ async revokeAccess(projectId, userId) {
22619
+ await this.client.request(
22620
+ `/api/projects/${encodeURIComponent(projectId)}/access?userId=${encodeURIComponent(userId)}`,
22621
+ { method: "DELETE", context: "Revoke project access" }
22622
+ );
22623
+ }
20272
22624
  };
20273
22625
  var GithubApi = class {
20274
22626
  constructor(client) {
@@ -20500,9 +22852,10 @@ var EnvApi = class {
20500
22852
  /**
20501
22853
  * The names the env route refuses as a key — `409 VALIDATION_FAILED`
20502
22854
  * `<KEY> is already used by a storage binding.` — read the way it reads
20503
- * them: `ASSETS` (the static files), every binding of the project's storage
20504
- * and, on a project using the managed database (`database_provider` is
20505
- * `d1`), `DB`, which may not be a storage row yet. For a caller that sends
22855
+ * them: `ASSETS` (the static files) and, on a project using the managed
22856
+ * database (`database_provider` is `d1`), `DB`. The bindings a user
22857
+ * declared (R2·KV·D1) are gone since 2026-11-04, and so is the route that
22858
+ * listed them. For a caller that sends
20506
22859
  * many entries in several requests (`naro env import`) and has to refuse
20507
22860
  * before the first one is saved. A control plane whose project row has no
20508
22861
  * `database_provider` reads as having no managed database, so `DB` is only
@@ -20510,21 +22863,9 @@ var EnvApi = class {
20510
22863
  */
20511
22864
  async takenBindingNames(projectId) {
20512
22865
  const base2 = `/api/projects/${encodeURIComponent(projectId)}`;
20513
- const [storage, project] = await Promise.all([
20514
- this.client.request(
20515
- `${base2}/storage`,
20516
- { context: "List storage" }
20517
- ),
20518
- this.client.request(
20519
- base2,
20520
- { context: "Read project" }
20521
- )
20522
- ]);
20523
- const names = /* @__PURE__ */ new Set([
20524
- STATIC_ASSETS_BINDING,
20525
- ...storage.storage.map((row) => row.bindingName)
20526
- ]);
20527
- if (project.project.database_provider === "d1")
22866
+ const project2 = await this.client.request(base2, { context: "Read project" });
22867
+ const names = /* @__PURE__ */ new Set([STATIC_ASSETS_BINDING]);
22868
+ if (project2.project.database_provider === "d1")
20528
22869
  names.add(MANAGED_DATABASE_BINDING);
20529
22870
  return [...names];
20530
22871
  }
@@ -21816,6 +24157,41 @@ var StorageApi = class {
21816
24157
  );
21817
24158
  return body.objects;
21818
24159
  }
24160
+ /**
24161
+ * One object under another name in its bucket (Storage 3-1i), moved by the
24162
+ * project's Naro worker with the stored service key. It never replaces an
24163
+ * object (`409 OBJECT_EXISTS`); both names are held to the name rules
24164
+ * (`INVALID_PATH`). An old worker answers `NOT_FOUND` until
24165
+ * `naro backend enable` updates it.
24166
+ */
24167
+ async moveObject(projectId, bucket, from, to) {
24168
+ return this.client.request(
24169
+ this.bucketPath(projectId, bucket, "/objects/move"),
24170
+ { method: "POST", body: { from, to }, context: "Move an object" }
24171
+ );
24172
+ }
24173
+ /**
24174
+ * A token that lets whoever holds it upload `path` once, for two hours,
24175
+ * without a key (Storage 3-1e §4.5) — the bytes go straight to the
24176
+ * project's worker, never through the control plane. `upsert` lets it
24177
+ * replace a file that is there (`409 OBJECT_EXISTS` otherwise, at upload).
24178
+ * `signedUrl` is built the worker's way: each path segment encoded.
24179
+ */
24180
+ async createUploadUrl(projectId, bucket, path, options = {}) {
24181
+ const body = await this.client.request(
24182
+ this.bucketPath(projectId, bucket, "/objects/upload-url"),
24183
+ {
24184
+ method: "POST",
24185
+ body: { path, ...options.upsert === true && { upsert: true } },
24186
+ context: "Create an upload URL"
24187
+ }
24188
+ );
24189
+ const encoded = body.path.split("/").map(encodeURIComponent).join("/");
24190
+ return {
24191
+ ...body,
24192
+ signedUrl: `${body.url}/_naro/v1/storage/object/upload/sign/${encodeURIComponent(bucket)}/${encoded}?token=${encodeURIComponent(body.token)}`
24193
+ };
24194
+ }
21819
24195
  /**
21820
24196
  * A link that downloads one object without a key, for `expiresIn` seconds
21821
24197
  * (1–3600 through the control plane; `VALIDATION_FAILED` past it), on the
@@ -22164,6 +24540,54 @@ var FunctionsApi = class {
22164
24540
  }
22165
24541
  };
22166
24542
 
24543
+ // ../../packages/sdk/src/dashboard.ts
24544
+ function dashboardCapability(id) {
24545
+ const found = CAPABILITIES.find((capability) => capability.id === id);
24546
+ if (!found || typeof found.dashboard !== "string")
24547
+ throw new NaroError(
24548
+ "VALIDATION_FAILED",
24549
+ `No dashboard page for "${id}". Run without a capability to list the ones that have one.`
24550
+ );
24551
+ return found;
24552
+ }
24553
+ async function dashboardLink(client, capability, target2, options) {
24554
+ const needs = dashboardNeeds(capability.dashboard);
24555
+ let teamSlug = target2.teamSlug;
24556
+ let projectSlug;
24557
+ if (needs.project) {
24558
+ if (!target2.projectId)
24559
+ throw new NaroError(
24560
+ "PROJECT_NOT_LINKED",
24561
+ "This page belongs to a project: pass its id, or link the directory first."
24562
+ );
24563
+ const [project2, teams] = await Promise.all([
24564
+ client.projects.get(target2.projectId),
24565
+ client.teams.list()
24566
+ ]);
24567
+ const team = teams.find((candidate) => candidate.id === project2.teamId);
24568
+ if (!team)
24569
+ throw new NaroError(
24570
+ "NOT_FOUND",
24571
+ `The team of project ${project2.slug} is not one of yours.`
24572
+ );
24573
+ teamSlug = team.slug;
24574
+ projectSlug = project2.slug;
24575
+ } else if (needs.team && !teamSlug) {
24576
+ throw new NaroError("TEAM_REQUIRED", "This page belongs to a team.");
24577
+ }
24578
+ const support = capability[options.client];
24579
+ return {
24580
+ capability: capability.id,
24581
+ title: capability.title,
24582
+ url: dashboardUrl(options.baseUrl, capability.dashboard, {
24583
+ teamSlug,
24584
+ projectSlug
24585
+ }),
24586
+ reason: support.kind === "excluded" ? { code: support.reason, why: REASON_CODES[support.reason].why } : null,
24587
+ cli: capability.cli.kind === "built" ? capability.cli.commands ?? [] : []
24588
+ };
24589
+ }
24590
+
22167
24591
  // ../../packages/sdk/src/database.ts
22168
24592
  var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
22169
24593
  var BOOKMARK_FORMAT_MESSAGE = "bookmark must look like 00000001-00000008-000050ed-272153b8520cfb93b49834d93a1a1ab3 (four hyphen-separated hex groups, 59 characters).";
@@ -22214,14 +24638,14 @@ function summarizeStatement(statement2) {
22214
24638
 
22215
24639
  // ../../packages/sdk/src/dotenv.ts
22216
24640
  import { existsSync, readFileSync as readFileSync2 } from "fs";
22217
- import { join as join20 } from "path";
24641
+ import { join as join19 } from "path";
22218
24642
  var QUOTES2 = /* @__PURE__ */ new Set(["'", '"', "`"]);
22219
24643
  function parseDotenv(text3) {
22220
24644
  return parseDotenvDetailed(text3).vars;
22221
24645
  }
22222
24646
  function readProjectEnv(dir) {
22223
24647
  for (const name of [".env.local", ".env.production", ".env"]) {
22224
- const path = join20(dir, name);
24648
+ const path = join19(dir, name);
22225
24649
  if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
22226
24650
  }
22227
24651
  return {};
@@ -22275,7 +24699,7 @@ function serializeDotenv(vars) {
22275
24699
 
22276
24700
  // ../../packages/sdk/src/env-scan.ts
22277
24701
  import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
22278
- import { join as join21 } from "path";
24702
+ import { join as join20 } from "path";
22279
24703
  var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([
22280
24704
  ".ts",
22281
24705
  ".tsx",
@@ -22330,7 +24754,7 @@ function collect(dir, out) {
22330
24754
  for (const entry of entries) {
22331
24755
  if (out.length >= MAX_FILES) return;
22332
24756
  if (entry.isSymbolicLink()) continue;
22333
- const path = join21(dir, entry.name);
24757
+ const path = join20(dir, entry.name);
22334
24758
  if (entry.isDirectory()) {
22335
24759
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
22336
24760
  continue;
@@ -22362,7 +24786,7 @@ function scanEnvReferences(dir) {
22362
24786
  }
22363
24787
  }
22364
24788
  for (const name of EXAMPLE_FILES) {
22365
- const path = join21(dir, name);
24789
+ const path = join20(dir, name);
22366
24790
  if (!existsSync2(path)) continue;
22367
24791
  let parsed;
22368
24792
  try {
@@ -22380,11 +24804,11 @@ function scanEnvReferences(dir) {
22380
24804
 
22381
24805
  // ../../packages/sdk/src/framework.ts
22382
24806
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
22383
- import { join as join22 } from "path";
24807
+ import { join as join21 } from "path";
22384
24808
  function readPackageJson2(dir) {
22385
24809
  try {
22386
24810
  return JSON.parse(
22387
- readFileSync4(join22(dir, "package.json"), "utf8")
24811
+ readFileSync4(join21(dir, "package.json"), "utf8")
22388
24812
  );
22389
24813
  } catch {
22390
24814
  return null;
@@ -22395,7 +24819,7 @@ function hasDependency(pkg, name) {
22395
24819
  }
22396
24820
  function hasConfig(dir, base2, evidence) {
22397
24821
  for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
22398
- if (existsSync3(join22(dir, `${base2}.${ext}`))) {
24822
+ if (existsSync3(join21(dir, `${base2}.${ext}`))) {
22399
24823
  evidence.push(`${base2}.${ext}`);
22400
24824
  return true;
22401
24825
  }
@@ -22503,8 +24927,8 @@ function detectFramework2(dir) {
22503
24927
  if (!byDependency && !byConfig) continue;
22504
24928
  if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
22505
24929
  if (detector.framework === "nextjs") {
22506
- if (existsSync3(join22(dir, "app"))) evidence.push("app/");
22507
- if (existsSync3(join22(dir, "pages"))) evidence.push("pages/");
24930
+ if (existsSync3(join21(dir, "app"))) evidence.push("app/");
24931
+ if (existsSync3(join21(dir, "pages"))) evidence.push("pages/");
22508
24932
  }
22509
24933
  return {
22510
24934
  framework: detector.framework,
@@ -22515,7 +24939,7 @@ function detectFramework2(dir) {
22515
24939
  deployable: true
22516
24940
  };
22517
24941
  }
22518
- if (!pkg && existsSync3(join22(dir, "index.html"))) {
24942
+ if (!pkg && existsSync3(join21(dir, "index.html"))) {
22519
24943
  evidence.push("index.html (no package.json)");
22520
24944
  return {
22521
24945
  framework: "static",
@@ -23400,10 +25824,10 @@ var REALTIME_COMMAND_PROBLEM = "realtime.messages takes SELECT, INSERT and ALL p
23400
25824
  var REALTIME_COLUMNS_PROBLEM = "realtime.messages policies are checked once per join, so they can read topic, extension and private only";
23401
25825
  var REALTIME_TOPIC_PROBLEM = "realtime.topic() is available only in policies of realtime.messages";
23402
25826
  function quotedRealtimeMessagesProblem(written) {
23403
- return `a table named "${written}" cannot be used in policies, as naro keeps the name for Realtime: write realtime.messages, without quotes, for Realtime's messages`;
25827
+ return `a table named "${written}" cannot be used in policies, as Naro keeps the name for Realtime: write realtime.messages, without quotes, for Realtime's messages`;
23404
25828
  }
23405
25829
  function quotedRealtimeAliasProblem(written) {
23406
- return `an alias named "${written}" cannot be used in policies, as naro keeps the name for Realtime: give the subquery another alias`;
25830
+ return `an alias named "${written}" cannot be used in policies, as Naro keeps the name for Realtime: give the subquery another alias`;
23407
25831
  }
23408
25832
  function realtimeFunctionProblem(name) {
23409
25833
  return `function realtime.${name} is not available in policies: use realtime.topic()`;
@@ -23888,7 +26312,7 @@ function storageHelperColumnProblem(fn) {
23888
26312
  return `storage.${fn}() takes the name column of storage.objects`;
23889
26313
  }
23890
26314
  function quotedStorageObjectsProblem(written) {
23891
- return `a table named "${written}" cannot be used in policies, as naro keeps the name for Storage: write storage.objects, without quotes, for Storage's objects`;
26315
+ return `a table named "${written}" cannot be used in policies, as Naro keeps the name for Storage: write storage.objects, without quotes, for Storage's objects`;
23892
26316
  }
23893
26317
  function count(state, token) {
23894
26318
  if (++state.nodes > MAX_EXPRESSION_NODES)
@@ -24346,7 +26770,7 @@ function readTableName(stream) {
24346
26770
  if (stream.acceptOperator(".")) {
24347
26771
  if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
24348
26772
  throw new PolicySyntaxError(
24349
- `schema ${name} does not exist: naro's tables are in public`,
26773
+ `schema ${name} does not exist: Naro's tables are in public`,
24350
26774
  first.position
24351
26775
  );
24352
26776
  token = stream.peek();
@@ -24457,7 +26881,7 @@ function readJwtAccess(stream) {
24457
26881
  const next = stream.peek();
24458
26882
  if (isOperator(next, "->") || isOperator(next, "->>"))
24459
26883
  throw new PolicySyntaxError(
24460
- "naro's auth.jwt() claims are not nested",
26884
+ "Naro's auth.jwt() claims are not nested",
24461
26885
  next.position
24462
26886
  );
24463
26887
  return {
@@ -24556,7 +26980,7 @@ function readPrimary(state) {
24556
26980
  stream.next();
24557
26981
  if (isOperator(stream.peek(), "(") || isOperator(stream.peek(), "."))
24558
26982
  throw new PolicySyntaxError(
24559
- `${token.value}.${name.value} is not a column: naro reads column or table.column`,
26983
+ `${token.value}.${name.value} is not a column: Naro reads column or table.column`,
24560
26984
  token.position
24561
26985
  );
24562
26986
  return { kind: "column", qualifier: token.value, name: name.value };
@@ -24609,7 +27033,7 @@ function readRealtimeColumn(state) {
24609
27033
  const next = stream.peek();
24610
27034
  if (isOperator(next, "(") || isOperator(next, "."))
24611
27035
  throw new PolicySyntaxError(
24612
- `realtime.messages.${name} is not a column: naro reads realtime.messages.column`,
27036
+ `realtime.messages.${name} is not a column: Naro reads realtime.messages.column`,
24613
27037
  next.position
24614
27038
  );
24615
27039
  return { kind: "column", qualifier: REALTIME_MESSAGES, name };
@@ -26531,7 +28955,7 @@ function textTruthProblem(snapshot, table, expr, names = [asciiLower(table.name)
26531
28955
  function selectCheckRefusal(path, column2, problem) {
26532
28956
  const reason = {
26533
28957
  generated: `is not available: the select policy reads the generated column ${column2}, which a new row cannot be checked against`,
26534
- collation: `is not available: the select policy reads ${column2}, whose collation naro cannot read, so a new row cannot be checked against it`,
28958
+ collation: `is not available: the select policy reads ${column2}, whose collation Naro cannot read, so a new row cannot be checked against it`,
26535
28959
  pattern: `is not available: the select policy compares the number column ${column2} with like or ilike, which a new row cannot be checked against`,
26536
28960
  textual: `is not available: the select policy turns the number column ${column2} into text, which a new row cannot be checked against`,
26537
28961
  readBack: `is not available: the select policy reads ${column2}, a number in a column that declares no number type, as text, which a new row cannot be checked against`,
@@ -26929,7 +29353,7 @@ function checkProblemReason(column2, problem) {
26929
29353
  case "generated":
26930
29354
  return `${column2} is a generated column, which a WITH CHECK cannot read (an update policy without WITH CHECK checks its USING)`;
26931
29355
  case "collation":
26932
- return `${column2} has a collation naro cannot read, which a WITH CHECK needs to compare it`;
29356
+ return `${column2} has a collation Naro cannot read, which a WITH CHECK needs to compare it`;
26933
29357
  case "pattern":
26934
29358
  return `${column2} is a number column, which a WITH CHECK cannot compare with LIKE or ILIKE`;
26935
29359
  case "textual":
@@ -26945,7 +29369,7 @@ function usingProblemReason(name, problem) {
26945
29369
  case "generated":
26946
29370
  return `${name} is a generated column, which a USING judged on a row's values cannot read`;
26947
29371
  case "collation":
26948
- return `${name} has a collation naro cannot read, which a USING judged on a row's values needs to compare it`;
29372
+ return `${name} has a collation Naro cannot read, which a USING judged on a row's values needs to compare it`;
26949
29373
  case "pattern":
26950
29374
  return `${name} is a number column, which a USING judged on a row's values cannot compare with LIKE or ILIKE`;
26951
29375
  case "textual":
@@ -27343,7 +29767,7 @@ function checkCase(table, terms, guard, pass, action, nonce) {
27343
29767
  fail(
27344
29768
  invalidQuery(
27345
29769
  action === "insert" ? "returning" : "values",
27346
- `is not available: naro cannot read the schema of ${table.name}, so a new row cannot be checked against its select policy`
29770
+ `is not available: Naro cannot read the schema of ${table.name}, so a new row cannot be checked against its select policy`
27347
29771
  )
27348
29772
  );
27349
29773
  fail(policyInvalid(first.policy, action));
@@ -27505,7 +29929,7 @@ function checkFragment(target2, req, snapshot, command, row, nonce, stored, with
27505
29929
  const first = terms.find((term) => term.policy !== void 0)?.policy;
27506
29930
  return fail(
27507
29931
  first ? policyInvalid(first, command) : dataError("INTERNAL_ERROR"),
27508
- `naro cannot read the schema of ${target2.name}, so a check of the stored row cannot be guarded`
29932
+ `Naro cannot read the schema of ${target2.name}, so a check of the stored row cannot be guarded`
27509
29933
  );
27510
29934
  }
27511
29935
  return [schemaGuard(name, sql, command, checked), ...guards, ...terms];
@@ -28686,7 +31110,7 @@ var Reader = class {
28686
31110
  }
28687
31111
  if (this.takeWord("WHERE")) top.resolved.push(this.where());
28688
31112
  this.returning(top.resolved);
28689
- } else throw new Unreadable("not a statement naro compiles");
31113
+ } else throw new Unreadable("not a statement Naro compiles");
28690
31114
  return top;
28691
31115
  }
28692
31116
  };
@@ -30826,9 +33250,9 @@ var SYSTEM_MIGRATIONS = [
30826
33250
  ];
30827
33251
 
30828
33252
  // ../../packages/data-core/src/policy-migration.ts
30829
- var GRANT_REFUSAL = "naro has no GRANT or REVOKE: every table is behind row-level security, so write a policy instead (CREATE POLICY \u2026).";
33253
+ var GRANT_REFUSAL = "Naro has no GRANT or REVOKE: every table is behind row-level security, so write a policy instead (CREATE POLICY \u2026).";
30830
33254
  function disableRefusal(table) {
30831
- return `naro keeps row-level security on for every table. To let everyone through, create a policy: CREATE POLICY "open" ON ${policyTableSql(table)} USING (true) WITH CHECK (true);`;
33255
+ return `Naro keeps row-level security on for every table. To let everyone through, create a policy: CREATE POLICY "open" ON ${policyTableSql(table)} USING (true) WITH CHECK (true);`;
30832
33256
  }
30833
33257
  function isStorageObjects(table) {
30834
33258
  return asciiLower(table) === STORAGE_OBJECTS;
@@ -31325,12 +33749,12 @@ import {
31325
33749
  rmSync as rmSync2,
31326
33750
  writeFileSync as writeFileSync2
31327
33751
  } from "fs";
31328
- import { join as join23 } from "path";
33752
+ import { join as join22 } from "path";
31329
33753
  function linkDir(dir) {
31330
- return join23(dir, ".naro");
33754
+ return join22(dir, ".naro");
31331
33755
  }
31332
33756
  function linkFile(dir) {
31333
- return join23(linkDir(dir), "project.json");
33757
+ return join22(linkDir(dir), "project.json");
31334
33758
  }
31335
33759
  function readProjectLink(dir) {
31336
33760
  try {
@@ -31352,7 +33776,7 @@ function writeProjectLink(dir, link) {
31352
33776
  writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
31353
33777
  `);
31354
33778
  writeFileSync2(
31355
- join23(linkDir(dir), "README.txt"),
33779
+ join22(linkDir(dir), "README.txt"),
31356
33780
  "This directory links the project to Naro and should not be committed.\n"
31357
33781
  );
31358
33782
  }
@@ -31362,9 +33786,9 @@ function removeProjectLink(dir) {
31362
33786
  return true;
31363
33787
  }
31364
33788
  function ensureGitignoreHasNaro(dir) {
31365
- const gitignorePath = join23(dir, ".gitignore");
33789
+ const gitignorePath = join22(dir, ".gitignore");
31366
33790
  const hasGitignore = existsSync4(gitignorePath);
31367
- if (!hasGitignore && !existsSync4(join23(dir, ".git"))) return false;
33791
+ if (!hasGitignore && !existsSync4(join22(dir, ".git"))) return false;
31368
33792
  const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
31369
33793
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
31370
33794
  if (covered) return false;
@@ -31549,6 +33973,159 @@ function lintSchema(input) {
31549
33973
  ];
31550
33974
  }
31551
33975
 
33976
+ // ../../packages/sdk/src/lint-database.ts
33977
+ function hasCode(error51, code) {
33978
+ return typeof error51 === "object" && error51 !== null && error51.code === code;
33979
+ }
33980
+ var DIAGNOSTICS_SQL = [
33981
+ `SELECT name FROM sqlite_master WHERE type = 'trigger' AND ${systemTablePredicate("name")} ORDER BY name;`,
33982
+ "SELECT name, type FROM pragma_table_list WHERE schema = 'main' AND type IN ('virtual', 'shadow') ORDER BY name;",
33983
+ "SELECT * FROM pragma_quick_check;",
33984
+ "SELECT * FROM pragma_foreign_key_check;"
33985
+ ].join("\n");
33986
+ var POLICIES_TRUNCATED_NOTICE = "\u26A0 The policies were cut at 10,000 rows, so they were not checked.";
33987
+ var POLICY_TABLE_PROBE = `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '${NAMED_POLICY_TABLE}'`;
33988
+ var POLICY_READ_SQL = [NAMED_POLICY_SQL.list, INDEX_SQL, ...SCHEMA_SQL].map((sql) => `${sql};`).join("\n");
33989
+ function objectsOf(statement2) {
33990
+ return statement2.rows.map(
33991
+ (row) => Object.fromEntries(
33992
+ statement2.columns.map((column2, index) => [column2, row[index]])
33993
+ )
33994
+ );
33995
+ }
33996
+ async function readPolicies(client, projectId, warn2) {
33997
+ let backend;
33998
+ try {
33999
+ backend = await client.backend.info(projectId);
34000
+ } catch (error51) {
34001
+ if (hasCode(error51, "BACKEND_DISABLED")) return void 0;
34002
+ throw error51;
34003
+ }
34004
+ if (backend.status === "none") return void 0;
34005
+ const probe = await client.db.query(projectId, {
34006
+ sql: POLICY_TABLE_PROBE,
34007
+ readOnly: true
34008
+ });
34009
+ if ((probe.statements[0]?.rows.length ?? 0) === 0) return void 0;
34010
+ const answer = await client.db.query(projectId, { sql: POLICY_READ_SQL });
34011
+ const [stored, indexes, ...schema] = answer.statements;
34012
+ const expected = SCHEMA_SQL.length + 2;
34013
+ if (stored === void 0 || indexes === void 0 || answer.statements.length !== expected)
34014
+ throw new NaroError(
34015
+ "DATABASE_QUERY_FAILED",
34016
+ `The policies could not be read: the database answered with ${String(answer.statements.length)} result sets instead of ${String(expected)}.`
34017
+ );
34018
+ if (answer.truncated) {
34019
+ warn2(POLICIES_TRUNCATED_NOTICE);
34020
+ return void 0;
34021
+ }
34022
+ const policies = [];
34023
+ const unreadable = [];
34024
+ for (const row of objectsOf(stored)) {
34025
+ const policy = loadPolicy(row);
34026
+ if (policy === void 0) continue;
34027
+ if (policy.invalid !== null)
34028
+ unreadable.push({
34029
+ table: policy.table,
34030
+ name: policy.name,
34031
+ reason: policy.invalid
34032
+ });
34033
+ else
34034
+ policies.push({
34035
+ table: policy.table,
34036
+ name: policy.name,
34037
+ permissive: policy.permissive,
34038
+ command: policy.command,
34039
+ roles: policy.roles,
34040
+ using: policy.using,
34041
+ check: policy.check,
34042
+ owner: policy.owner
34043
+ });
34044
+ }
34045
+ const snapshot = snapshotFromRows(schema.flatMap(objectsOf));
34046
+ return {
34047
+ tables: tableNames(snapshot),
34048
+ policies,
34049
+ unreadable,
34050
+ snapshot,
34051
+ indexes: indexesFromRows(objectsOf(indexes))
34052
+ };
34053
+ }
34054
+ function firstCell(row) {
34055
+ const value = row[0];
34056
+ return value === null || value === void 0 ? "" : String(value);
34057
+ }
34058
+ function rowidOf(text3) {
34059
+ if (text3 === "") return null;
34060
+ const value = Number(text3);
34061
+ return Number.isFinite(value) ? value : null;
34062
+ }
34063
+ async function lintDatabase(client, projectId, warn2) {
34064
+ const tables = await client.db.tables(projectId);
34065
+ const answer = await client.db.query(projectId, { sql: DIAGNOSTICS_SQL });
34066
+ const [triggers, objects, quick, broken] = answer.statements;
34067
+ if (triggers === void 0 || objects === void 0 || quick === void 0 || broken === void 0)
34068
+ throw new NaroError(
34069
+ "DATABASE_QUERY_FAILED",
34070
+ `The diagnostics could not be read: the database answered with ${String(answer.statements.length)} result sets instead of 4.`
34071
+ );
34072
+ const virtualTables = [];
34073
+ const shadow = /* @__PURE__ */ new Set();
34074
+ for (const row of objects.rows) {
34075
+ const name = cellText(objects, row, "name");
34076
+ if (cellText(objects, row, "type") === "shadow") shadow.add(name);
34077
+ else virtualTables.push(name);
34078
+ }
34079
+ const foreignKeyCheck = broken.rows.map((row) => ({
34080
+ table: cellText(broken, row, "table"),
34081
+ rowid: rowidOf(cellText(broken, row, "rowid")),
34082
+ parent: cellText(broken, row, "parent")
34083
+ }));
34084
+ const policies = await readPolicies(client, projectId, warn2);
34085
+ const input = {
34086
+ tables: tables.filter((table) => !shadow.has(table.name)),
34087
+ foreignKeyCheck,
34088
+ quickCheck: quick.rows.map(firstCell),
34089
+ triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
34090
+ virtualTables,
34091
+ ...policies === void 0 ? {} : { policies }
34092
+ };
34093
+ const issues = lintSchema(input);
34094
+ const errors = issues.filter((issue2) => issue2.severity === "error").length;
34095
+ return {
34096
+ issues,
34097
+ errors,
34098
+ warnings: issues.length - errors,
34099
+ tables: countLintedTables(input),
34100
+ truncated: answer.truncated
34101
+ };
34102
+ }
34103
+
34104
+ // ../../packages/sdk/src/migration-repair.ts
34105
+ var MIGRATION_NAME_RE = /^[A-Za-z0-9_.-]{1,200}\.sql$/;
34106
+ var LEDGER_DDL = "CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
34107
+ var LEDGER_INSERT = "INSERT INTO d1_migrations (name) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM d1_migrations WHERE name = ?)";
34108
+ var LEDGER_DELETE = "DELETE FROM d1_migrations WHERE name IN (?, ?)";
34109
+ function withoutSuffix(key) {
34110
+ return key.endsWith(".sql") ? key.slice(0, -".sql".length) : key;
34111
+ }
34112
+ var MIGRATION_NAME_MESSAGE = "name must end with .sql and contain only letters, digits, _ . -";
34113
+ function repairIsNoop(ledger, key, status) {
34114
+ const recorded = ledger.some(
34115
+ (row) => row.name === key || row.name === withoutSuffix(key)
34116
+ );
34117
+ return status === "applied" ? recorded : !recorded;
34118
+ }
34119
+ async function repairLedger(client, projectId, key, status, ledgerIsEmpty) {
34120
+ if (status === "applied" && ledgerIsEmpty)
34121
+ await client.db.query(projectId, { sql: LEDGER_DDL });
34122
+ const answer = await client.db.query(projectId, {
34123
+ sql: status === "applied" ? LEDGER_INSERT : LEDGER_DELETE,
34124
+ params: status === "applied" ? [key, key] : [key, withoutSuffix(key)]
34125
+ });
34126
+ return (answer.statements[0]?.meta.changes ?? 0) > 0;
34127
+ }
34128
+
31552
34129
  // ../../packages/sdk/src/migrations.ts
31553
34130
  import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
31554
34131
  import { resolve as resolve9 } from "path";
@@ -31630,7 +34207,7 @@ function migrationsPendingError(pending) {
31630
34207
  );
31631
34208
  }
31632
34209
  function migrationNameMismatch(sent, recorded) {
31633
- return `The control plane recorded the migration as ${recorded}, but naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
34210
+ return `The control plane recorded the migration as ${recorded}, but Naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
31634
34211
  }
31635
34212
 
31636
34213
  // ../../packages/sdk/src/reset-plan.ts
@@ -31996,7 +34573,7 @@ function usageStatus(report, currentMonth, requests) {
31996
34573
 
31997
34574
  // src/env-consent.ts
31998
34575
  import { existsSync as existsSync6 } from "fs";
31999
- import { join as join24 } from "path";
34576
+ import { join as join23 } from "path";
32000
34577
  var DOTENV_FILES = [".env.local", ".env.production", ".env"];
32001
34578
  var PUBLIC_PREFIX = "NEXT_PUBLIC_";
32002
34579
  function maskSecretValue(value) {
@@ -32008,7 +34585,7 @@ function maskEnvValue(key, value) {
32008
34585
  return maskSecretValue(value);
32009
34586
  }
32010
34587
  function findDotenvFile(dir) {
32011
- for (const name of DOTENV_FILES) if (existsSync6(join24(dir, name))) return name;
34588
+ for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
32012
34589
  return null;
32013
34590
  }
32014
34591
  function envAddGuidance(keys, environment) {
@@ -32272,13 +34849,13 @@ async function uploadArtifact(artifact, opts) {
32272
34849
 
32273
34850
  // src/version.ts
32274
34851
  import { readFileSync as readFileSync6 } from "fs";
32275
- import { dirname as dirname5, join as join25 } from "path";
34852
+ import { dirname as dirname5, join as join24 } from "path";
32276
34853
  import { fileURLToPath } from "url";
32277
34854
  function version2() {
32278
34855
  try {
32279
34856
  const pkg = JSON.parse(
32280
34857
  readFileSync6(
32281
- join25(dirname5(fileURLToPath(import.meta.url)), "..", "package.json"),
34858
+ join24(dirname5(fileURLToPath(import.meta.url)), "..", "package.json"),
32282
34859
  "utf8"
32283
34860
  )
32284
34861
  );
@@ -32307,7 +34884,7 @@ import {
32307
34884
  basename as basename3,
32308
34885
  dirname as dirname6,
32309
34886
  isAbsolute as isAbsolute4,
32310
- join as join26,
34887
+ join as join25,
32311
34888
  relative as relative6,
32312
34889
  resolve as resolve10,
32313
34890
  sep as sep5
@@ -32434,12 +35011,12 @@ async function copyWorkingTree(source, checkout) {
32434
35011
  keptLinks,
32435
35012
  async ({ path, text: text3 }) => {
32436
35013
  try {
32437
- return inside(await realpath3(join26(checkout, path)), root);
35014
+ return inside(await realpath3(join25(checkout, path)), root);
32438
35015
  } catch (error51) {
32439
35016
  return error51.code === "ENOENT" && climbsOnlyFirst(text3);
32440
35017
  }
32441
35018
  },
32442
- ({ path }) => unlink(join26(checkout, path))
35019
+ ({ path }) => unlink(join25(checkout, path))
32443
35020
  );
32444
35021
  linksElsewhere.push(...strays.map(({ path }) => path));
32445
35022
  const committed = await committedEnvFiles(sourceDir);
@@ -32453,7 +35030,7 @@ async function copyWorkingTree(source, checkout) {
32453
35030
  outsideProject.push(path);
32454
35031
  continue;
32455
35032
  }
32456
- const target2 = join26(checkout, path);
35033
+ const target2 = join25(checkout, path);
32457
35034
  if (!inside(await realAncestor(dirname6(target2)), root)) {
32458
35035
  outsideProject.push(path);
32459
35036
  continue;
@@ -32478,16 +35055,16 @@ async function copyWorkingTree(source, checkout) {
32478
35055
  continue;
32479
35056
  }
32480
35057
  await writeFile7(target2, content, { flag: "wx" });
32481
- const local = await readLocal(join26(sourceDir, path));
35058
+ const local = await readLocal(join25(sourceDir, path));
32482
35059
  if (!local || !sameText(local, content)) committedVersion.push(path);
32483
35060
  }
32484
35061
  const envStrays = await keptOnly(
32485
35062
  envLinks,
32486
35063
  async (path) => {
32487
- const real = await realpath3(join26(checkout, path)).catch(() => null);
35064
+ const real = await realpath3(join25(checkout, path)).catch(() => null);
32488
35065
  return real !== null && inside(real, root);
32489
35066
  },
32490
- (path) => unlink(join26(checkout, path))
35067
+ (path) => unlink(join25(checkout, path))
32491
35068
  );
32492
35069
  outsideProject.push(...envStrays);
32493
35070
  const inCommit = new Set(
@@ -32507,6 +35084,427 @@ async function copyWorkingTree(source, checkout) {
32507
35084
  };
32508
35085
  }
32509
35086
 
35087
+ // src/data-environment.ts
35088
+ var DATA_ENV_MESSAGES = {
35089
+ name: "--env must be production or an environment name.",
35090
+ preview: "--env preview has no data of its own: preview deployments use production's. Drop --env, or name an environment.",
35091
+ projectWide: (command) => `--env is not an option of ${command}: turning the backend on or off applies to every environment.`
35092
+ };
35093
+ function dataEnvironment(flags) {
35094
+ const value = flags.env;
35095
+ if (value === void 0 || value === "production") return null;
35096
+ if (value === "preview")
35097
+ throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.preview);
35098
+ if (environmentSlugProblem(value) !== null)
35099
+ throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.name);
35100
+ return value;
35101
+ }
35102
+ async function dataClient(context) {
35103
+ return (await context.client()).inEnvironment(dataEnvironment(context.flags));
35104
+ }
35105
+ function databaseName(environment) {
35106
+ return environment === null ? "the project database" : `the ${environment} environment's database`;
35107
+ }
35108
+ function enableBackendCommand(environment) {
35109
+ return environment === null ? "naro backend enable" : `naro environments provision ${environment}`;
35110
+ }
35111
+ function envFlag(environment) {
35112
+ return environment === null ? "" : ` --env ${environment}`;
35113
+ }
35114
+ function forEnvironment(text3, environment) {
35115
+ return environmentGuidance(text3, environment);
35116
+ }
35117
+
35118
+ // src/commands/migration.ts
35119
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
35120
+ import { join as join26, relative as relative7 } from "path";
35121
+
35122
+ // src/prompt.ts
35123
+ import { createInterface as createInterface2 } from "readline";
35124
+ import { Writable } from "stream";
35125
+ function confirmHint(defaultYes) {
35126
+ return defaultYes ? "(Y/n)" : "(y/N)";
35127
+ }
35128
+ function readConfirmAnswer(answer, defaultYes) {
35129
+ const normalized2 = answer.trim().toLowerCase();
35130
+ if (normalized2 === "") return defaultYes;
35131
+ if (normalized2 === "y" || normalized2 === "yes") return true;
35132
+ if (!defaultYes) return false;
35133
+ if (normalized2 === "n" || normalized2 === "no") return false;
35134
+ return null;
35135
+ }
35136
+ function isInteractive() {
35137
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
35138
+ }
35139
+ async function readStdin() {
35140
+ let data = "";
35141
+ for await (const chunk of process.stdin) data += String(chunk);
35142
+ return data.replace(/\r?\n$/, "");
35143
+ }
35144
+ var TtyPrompter = class {
35145
+ async ask(question) {
35146
+ const rl = createInterface2({
35147
+ input: process.stdin,
35148
+ output: process.stdout
35149
+ });
35150
+ try {
35151
+ return await new Promise(
35152
+ (resolve15) => rl.question(question, resolve15)
35153
+ );
35154
+ } finally {
35155
+ rl.close();
35156
+ }
35157
+ }
35158
+ async askHidden(question) {
35159
+ let muted = false;
35160
+ const output = new Writable({
35161
+ write(chunk, _enc, done) {
35162
+ if (!muted) process.stdout.write(chunk);
35163
+ done();
35164
+ }
35165
+ });
35166
+ const rl = createInterface2({
35167
+ input: process.stdin,
35168
+ output,
35169
+ terminal: true
35170
+ });
35171
+ try {
35172
+ const answer = new Promise(
35173
+ (resolve15) => rl.question(question, resolve15)
35174
+ );
35175
+ muted = true;
35176
+ const value = await answer;
35177
+ process.stdout.write("\n");
35178
+ return value;
35179
+ } finally {
35180
+ rl.close();
35181
+ }
35182
+ }
35183
+ async confirm(question, options = {}) {
35184
+ const defaultYes = options.defaultYes ?? false;
35185
+ for (; ; ) {
35186
+ const read = readConfirmAnswer(
35187
+ await this.ask(`${question} ${confirmHint(defaultYes)} `),
35188
+ defaultYes
35189
+ );
35190
+ if (read !== null) return read;
35191
+ }
35192
+ }
35193
+ async select(question, options) {
35194
+ process.stdout.write(`${question}
35195
+ `);
35196
+ options.forEach((option, index) => {
35197
+ process.stdout.write(` ${String(index + 1)}. ${option}
35198
+ `);
35199
+ });
35200
+ for (; ; ) {
35201
+ const answer = Number.parseInt(
35202
+ (await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
35203
+ 10
35204
+ );
35205
+ if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
35206
+ return answer - 1;
35207
+ }
35208
+ }
35209
+ };
35210
+ var NonInteractivePrompter = class {
35211
+ constructor(assumeYes) {
35212
+ this.assumeYes = assumeYes;
35213
+ }
35214
+ refuse(question) {
35215
+ throw new NaroError(
35216
+ "VALIDATION_FAILED",
35217
+ `Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
35218
+ );
35219
+ }
35220
+ async ask(question) {
35221
+ this.refuse(question);
35222
+ }
35223
+ async askHidden(question) {
35224
+ this.refuse(question);
35225
+ }
35226
+ async confirm() {
35227
+ return this.assumeYes;
35228
+ }
35229
+ async select(question) {
35230
+ this.refuse(question);
35231
+ }
35232
+ };
35233
+ function createPrompter(options) {
35234
+ if (options.yes || options.json || !isInteractive())
35235
+ return new NonInteractivePrompter(options.yes);
35236
+ return new TtyPrompter();
35237
+ }
35238
+
35239
+ // src/commands/confirm.ts
35240
+ function cannotAsk(context) {
35241
+ return context.flags.json || !isInteractive();
35242
+ }
35243
+ async function confirmOrAbort(context, question) {
35244
+ if (context.flags.yes) return;
35245
+ if (cannotAsk(context)) {
35246
+ throw new NaroError(
35247
+ "CONFIRMATION_REQUIRED",
35248
+ `${question} Re-run with --yes to confirm.`
35249
+ );
35250
+ }
35251
+ const confirmed = await context.prompter.confirm(question);
35252
+ if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
35253
+ }
35254
+
35255
+ // src/commands/migration.ts
35256
+ var processStdin = {
35257
+ stdinIsTty: () => process.stdin.isTTY === true,
35258
+ readStdin
35259
+ };
35260
+ function migrationsDir(context) {
35261
+ return resolveMigrationsDir({
35262
+ baseDir: context.cwd,
35263
+ rootDirectory: context.flags.root,
35264
+ dir: context.flags.dir
35265
+ });
35266
+ }
35267
+ function relativeToCwd(context, path) {
35268
+ const rel = relative7(context.cwd, path);
35269
+ return rel === "" ? "." : rel;
35270
+ }
35271
+ async function appliedMigrations(client, projectId) {
35272
+ try {
35273
+ return await client.db.migrations(projectId);
35274
+ } catch (error51) {
35275
+ if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY") return [];
35276
+ throw error51;
35277
+ }
35278
+ }
35279
+ var UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
35280
+ function printable(text3) {
35281
+ return text3.replace(UNPRINTABLE, "?");
35282
+ }
35283
+ function isPrintable(text3) {
35284
+ return printable(text3) === text3;
35285
+ }
35286
+ var UNSHOWN_WARNING_LINE = "(a line of this warning has characters the terminal cannot show; run with --json to see it exactly)";
35287
+ function warningLines(message) {
35288
+ return message.split("\n").map(
35289
+ (line, index) => index === 0 ? `\u26A0 ${printable(line)}` : ` ${isPrintable(line) ? line : UNSHOWN_WARNING_LINE}`
35290
+ );
35291
+ }
35292
+ function policyProblems(dir, names) {
35293
+ return names.flatMap((name) => {
35294
+ const split2 = splitStatements(readFileSync7(join26(dir, name), "utf8"));
35295
+ if (!split2.ok) return [];
35296
+ const planned = planPolicyMigration(
35297
+ split2.statements.map((statement2) => statement2.text)
35298
+ );
35299
+ return planned.ok ? [] : [{ migration: name, message: planned.message }];
35300
+ });
35301
+ }
35302
+ function warningLine(warning) {
35303
+ if (warning === "out-of-order")
35304
+ return " \u26A0 older than an applied migration \u2014 db push will still apply it";
35305
+ if (warning === "ledger-name-missing-extension")
35306
+ return " \u26A0 recorded without .sql in the ledger";
35307
+ return null;
35308
+ }
35309
+ async function runMigrationNew(context, deps = processStdin) {
35310
+ const [name] = context.positionalOrThrow(
35311
+ 1,
35312
+ "Usage: naro migration new <name>"
35313
+ );
35314
+ const dir = migrationsDir(context);
35315
+ const fileName = nextMigrationFileName(
35316
+ listMigrationFiles(dir),
35317
+ name
35318
+ );
35319
+ mkdirSync3(dir, { recursive: true });
35320
+ const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
35321
+ const fromStdin = piped.length > 0;
35322
+ const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
35323
+ ` : `-- Migration: ${fileName}
35324
+ `;
35325
+ const path = join26(dir, fileName);
35326
+ writeFileSync3(path, body, { flag: "wx" });
35327
+ const relPath = relativeToCwd(context, path);
35328
+ context.output.result({ file: fileName, path: relPath, fromStdin }, [
35329
+ `\u2705 Created ${relPath}`
35330
+ ]);
35331
+ }
35332
+ async function runMigrationList(context) {
35333
+ const client = await dataClient(context);
35334
+ const projectId = await context.resolveProjectId();
35335
+ const dir = migrationsDir(context);
35336
+ const local = listMigrationFiles(dir);
35337
+ const ledger = await appliedMigrations(client, projectId);
35338
+ const migrations = compareMigrations(local, ledger);
35339
+ const relDir = relativeToCwd(context, dir);
35340
+ const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
35341
+ `Migrations in ${relDir} (${String(migrations.length)}):`,
35342
+ ...migrations.flatMap((row) => {
35343
+ const line = ` ${row.status.padEnd(12)}${printable(row.name)}${row.appliedAt ? ` ${printable(row.appliedAt)}` : ""}`;
35344
+ const warning = warningLine(row.warning);
35345
+ return warning ? [line, warning] : [line];
35346
+ })
35347
+ ];
35348
+ context.output.result({ projectId, dir: relDir, migrations }, lines);
35349
+ }
35350
+ async function applyMigrationFiles(context, client, projectId, dir, names) {
35351
+ const applied = [];
35352
+ const warnings = [];
35353
+ for (const name of names) {
35354
+ const sql = readFileSync7(join26(dir, name), "utf8");
35355
+ let answer;
35356
+ try {
35357
+ answer = await client.db.applyMigration(projectId, { name, sql });
35358
+ } catch (error51) {
35359
+ if (!isNaroError(error51)) throw error51;
35360
+ for (const message of serverWarnings(error51.details))
35361
+ for (const line of warningLines(message)) context.output.warn(line);
35362
+ const shown = context.output.json ? name : printable(name);
35363
+ throw new NaroError(error51.code, `${shown}: ${error51.message}`, {
35364
+ ...error51.status === void 0 ? {} : { status: error51.status },
35365
+ details: {
35366
+ ...error51.details,
35367
+ migration: name,
35368
+ applied,
35369
+ appliedWarnings: warnings
35370
+ },
35371
+ cause: error51
35372
+ });
35373
+ }
35374
+ context.output.log(`\u2713 ${printable(name)}`);
35375
+ applied.push(name);
35376
+ for (const message of answer.warnings) {
35377
+ warnings.push({ migration: name, message });
35378
+ for (const line of warningLines(message)) context.output.warn(line);
35379
+ }
35380
+ }
35381
+ return { applied, warnings };
35382
+ }
35383
+ function serverWarnings(details) {
35384
+ const warnings = details?.warnings;
35385
+ return Array.isArray(warnings) ? warnings.filter(
35386
+ (warning) => typeof warning === "string"
35387
+ ) : [];
35388
+ }
35389
+ async function runMigrationRepair(context) {
35390
+ const [name] = context.positionalOrThrow(
35391
+ 1,
35392
+ "Usage: naro migration repair <name> --status applied|reverted"
35393
+ );
35394
+ const key = name;
35395
+ if (!MIGRATION_NAME_RE.test(key))
35396
+ throw new NaroError("VALIDATION_FAILED", MIGRATION_NAME_MESSAGE);
35397
+ const status = context.flags.status;
35398
+ if (status !== "applied" && status !== "reverted")
35399
+ throw new NaroError(
35400
+ "VALIDATION_FAILED",
35401
+ "Pass --status applied or --status reverted."
35402
+ );
35403
+ const client = await dataClient(context);
35404
+ const projectId = await context.resolveProjectId();
35405
+ const ledger = await appliedMigrations(client, projectId);
35406
+ const nothingToDo = status === "applied" ? `${key} was already applied; nothing to do.` : `${key} was not in the ledger; nothing to do.`;
35407
+ if (repairIsNoop(ledger, key, status)) {
35408
+ context.output.warn(nothingToDo);
35409
+ context.output.result({ name: key, status, changed: false });
35410
+ return;
35411
+ }
35412
+ await confirmOrAbort(
35413
+ context,
35414
+ status === "applied" ? `Mark ${key} as applied in ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not run.` : `Remove ${key} from ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not rolled back.`
35415
+ );
35416
+ const changed = await repairLedger(
35417
+ client,
35418
+ projectId,
35419
+ key,
35420
+ status,
35421
+ ledger.length === 0
35422
+ );
35423
+ if (!changed) {
35424
+ context.output.warn(nothingToDo);
35425
+ context.output.result({ name: key, status, changed: false });
35426
+ return;
35427
+ }
35428
+ context.output.result({ name: key, status, changed: true }, [
35429
+ status === "applied" ? `\u2713 Marked ${key} as applied.` : `\u2713 Removed ${key} from the ledger.`
35430
+ ]);
35431
+ }
35432
+ async function runMigrationUp(context) {
35433
+ const client = await dataClient(context);
35434
+ const projectId = await context.resolveProjectId();
35435
+ const dir = migrationsDir(context);
35436
+ const local = listMigrationFiles(dir);
35437
+ const ledger = await appliedMigrations(client, projectId);
35438
+ const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
35439
+ if (pending.length === 0) {
35440
+ context.output.result({ projectId, applied: [], pending: [] }, [
35441
+ "No pending migrations."
35442
+ ]);
35443
+ return;
35444
+ }
35445
+ if (context.flags.dryRun) {
35446
+ const problems2 = policyProblems(dir, pending);
35447
+ context.output.result({ projectId, dryRun: true, pending, problems: problems2 }, [
35448
+ `Pending migrations (${String(pending.length)}):`,
35449
+ ...pending.map((name) => ` ${printable(name)}`),
35450
+ ...problems2.map(
35451
+ (problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
35452
+ )
35453
+ ]);
35454
+ if (problems2.length > 0) process.exitCode = 1;
35455
+ return;
35456
+ }
35457
+ context.output.log(`Pending migrations (${String(pending.length)}):`);
35458
+ for (const name of pending) context.output.log(` ${printable(name)}`);
35459
+ await confirmOrAbort(
35460
+ context,
35461
+ `Apply ${String(pending.length)} migration(s) to ${databaseName(dataEnvironment(context.flags))}? Each file is applied all-or-nothing; a failure stops the run.`
35462
+ );
35463
+ const { applied, warnings } = await applyMigrationFiles(
35464
+ context,
35465
+ client,
35466
+ projectId,
35467
+ dir,
35468
+ pending
35469
+ );
35470
+ context.output.result({ projectId, applied, pending: [], warnings }, [
35471
+ `\u2705 Applied ${String(applied.length)} migration(s).`
35472
+ ]);
35473
+ }
35474
+
35475
+ // src/commands/db-lint.ts
35476
+ var TRUNCATED_NOTICE = "\u26A0 The diagnostics were cut at 10,000 rows \u2014 there may be more broken rows than these.";
35477
+ function issueLines(issues, tables) {
35478
+ const errors = issues.filter((issue2) => issue2.severity === "error").length;
35479
+ if (issues.length === 0)
35480
+ return [`\u2713 No issues found in ${String(tables)} table(s).`];
35481
+ return [
35482
+ ...issues.map(
35483
+ (issue2) => printable(
35484
+ `${issue2.severity.padEnd(5)} ${issue2.rule} ${issue2.object}: ${issue2.message}`
35485
+ )
35486
+ ),
35487
+ `${String(errors)} error(s), ${String(issues.length - errors)} warning(s) in ${String(tables)} table(s).`
35488
+ ];
35489
+ }
35490
+ async function runDbLint(context) {
35491
+ const client = await dataClient(context);
35492
+ const projectId = await context.resolveProjectId();
35493
+ const linted = await lintDatabase(
35494
+ client,
35495
+ projectId,
35496
+ (message) => context.output.warn(message)
35497
+ );
35498
+ const { issues, errors, warnings, tables: counted } = linted;
35499
+ if (linted.truncated) context.output.warn(TRUNCATED_NOTICE);
35500
+ context.output.result(
35501
+ { issues, errors, warnings, tables: counted },
35502
+ issueLines(issues, counted)
35503
+ );
35504
+ if (errors > 0 || context.flags.strict === true && warnings > 0)
35505
+ process.exitCode = 1;
35506
+ }
35507
+
32510
35508
  // src/functions/deploy.ts
32511
35509
  import { relative as relative11, resolve as resolve13, sep as sep10 } from "path";
32512
35510
 
@@ -32564,8 +35562,8 @@ var BUNDLE_MESSAGES = {
32564
35562
  };
32565
35563
 
32566
35564
  // ../../packages/functions-bundler/src/discover.ts
32567
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
32568
- import { join as join27, relative as relative7, resolve as resolve11, sep as sep6 } from "path";
35565
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
35566
+ import { join as join27, relative as relative8, resolve as resolve11, sep as sep6 } from "path";
32569
35567
 
32570
35568
  // ../../packages/functions-bundler/src/limits.ts
32571
35569
  var FUNCTION_BUNDLE_MAX_BYTES = 67108864;
@@ -32590,7 +35588,7 @@ function functionsFolder(options) {
32590
35588
  const root = resolve11(options.cwd, options.root ?? ".");
32591
35589
  const explicit = options.dir !== void 0;
32592
35590
  const dir = explicit ? resolve11(options.cwd, options.dir) : join27(root, "naro", "functions");
32593
- const label2 = relative7(options.cwd, dir).split(sep6).join("/") || ".";
35591
+ const label2 = relative8(options.cwd, dir).split(sep6).join("/") || ".";
32594
35592
  return { dir, label: label2, root, explicit };
32595
35593
  }
32596
35594
  function listFunctionNames(folder) {
@@ -32623,7 +35621,7 @@ function readFunctionJson(dir, name) {
32623
35621
  if (!existsSync7(file2)) return { verifyJwt: void 0, entrypoint: void 0 };
32624
35622
  let parsed;
32625
35623
  try {
32626
- parsed = JSON.parse(readFileSync7(file2, "utf8"));
35624
+ parsed = JSON.parse(readFileSync8(file2, "utf8"));
32627
35625
  } catch {
32628
35626
  throw new FunctionBundleError(
32629
35627
  "VALIDATION_FAILED",
@@ -32699,7 +35697,7 @@ function configTomlWarning(folder) {
32699
35697
  if (!folder.dir.endsWith(join27("supabase", "functions"))) return null;
32700
35698
  const file2 = join27(folder.dir, "..", "config.toml");
32701
35699
  if (!existsSync7(file2)) return null;
32702
- return /^\s*verify_jwt\s*=/m.test(readFileSync7(file2, "utf8")) ? BUNDLE_MESSAGES.supabaseVerifyJwt(folder.label) : null;
35700
+ return /^\s*verify_jwt\s*=/m.test(readFileSync8(file2, "utf8")) ? BUNDLE_MESSAGES.supabaseVerifyJwt(folder.label) : null;
32703
35701
  }
32704
35702
 
32705
35703
  // ../../packages/functions-bundler/src/jsonc.ts
@@ -32902,22 +35900,22 @@ ${injects.map(
32902
35900
  }
32903
35901
 
32904
35902
  // ../../packages/functions-bundler/src/deno-imports.ts
32905
- import { existsSync as existsSync9, readFileSync as readFileSync9, realpathSync as realpathSync2 } from "fs";
32906
- import { dirname as dirname9, isAbsolute as isAbsolute5, join as join29, relative as relative8, sep as sep8 } from "path";
35903
+ import { existsSync as existsSync9, readFileSync as readFileSync10, realpathSync as realpathSync2 } from "fs";
35904
+ import { dirname as dirname9, isAbsolute as isAbsolute5, join as join29, relative as relative9, sep as sep8 } from "path";
32907
35905
 
32908
35906
  // ../../packages/functions-bundler/src/import-map.ts
32909
- import { existsSync as existsSync8, readFileSync as readFileSync8, realpathSync } from "fs";
35907
+ import { existsSync as existsSync8, readFileSync as readFileSync9, realpathSync } from "fs";
32910
35908
  import { basename as basename4, dirname as dirname8, join as join28, resolve as resolve12, sep as sep7 } from "path";
32911
35909
  import { fileURLToPath as fileURLToPath3 } from "url";
32912
35910
 
32913
35911
  // ../../packages/functions-bundler/src/printable.ts
32914
- var UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
32915
- function printable(text3) {
32916
- return text3.replace(UNPRINTABLE, "?");
35912
+ var UNPRINTABLE2 = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
35913
+ function printable2(text3) {
35914
+ return text3.replace(UNPRINTABLE2, "?");
32917
35915
  }
32918
35916
  function causeLine(error51) {
32919
35917
  const text3 = error51 instanceof Error ? error51.message : String(error51);
32920
- return printable(
35918
+ return printable2(
32921
35919
  text3.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "unknown error"
32922
35920
  );
32923
35921
  }
@@ -32941,7 +35939,7 @@ function expandBareKeys(imports) {
32941
35939
  function load(file2, label2) {
32942
35940
  let parsed;
32943
35941
  try {
32944
- parsed = parseJsonc2(readFileSync8(file2, "utf8"));
35942
+ parsed = parseJsonc2(readFileSync9(file2, "utf8"));
32945
35943
  } catch {
32946
35944
  throw new FunctionBundleError(
32947
35945
  "VALIDATION_FAILED",
@@ -32961,7 +35959,7 @@ function load(file2, label2) {
32961
35959
  if (key.endsWith("/") && !value.endsWith("/"))
32962
35960
  throw new FunctionBundleError(
32963
35961
  "VALIDATION_FAILED",
32964
- printable(BUNDLE_MESSAGES.importMapPrefix(label2, key, value))
35962
+ printable2(BUNDLE_MESSAGES.importMapPrefix(label2, key, value))
32965
35963
  );
32966
35964
  return {
32967
35965
  imports: DENO_CONFIGS.has(basename4(file2)) ? expandBareKeys(written) : written,
@@ -33104,7 +36102,7 @@ function parseNpm(text3) {
33104
36102
  }
33105
36103
  var afterScheme = (specifier) => specifier.slice(4).replace(/^\//, "");
33106
36104
  var isPackageName = (pkg) => pkg.startsWith("@") ? /^@[^/]+\/[^/]+$/.test(pkg) : /^[^/]+$/.test(pkg);
33107
- var refuse2 = (text3) => ({ errors: [{ text: printable(text3) }] });
36105
+ var refuse2 = (text3) => ({ errors: [{ text: printable2(text3) }] });
33108
36106
  var installOf = (pkg, range) => range ? `${pkg}@${range}` : pkg;
33109
36107
  function realPath2(path) {
33110
36108
  try {
@@ -33113,9 +36111,9 @@ function realPath2(path) {
33113
36111
  return path;
33114
36112
  }
33115
36113
  }
33116
- var fromRoot = (projectRoot, path) => relative8(projectRoot, path).split(sep8).join("/");
36114
+ var fromRoot = (projectRoot, path) => relative9(projectRoot, path).split(sep8).join("/");
33117
36115
  function isInside(path, projectRoot) {
33118
- const rel = relative8(projectRoot, path);
36116
+ const rel = relative9(projectRoot, path);
33119
36117
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep8}`) && !isAbsolute5(rel);
33120
36118
  }
33121
36119
  function packageDir(from, projectRoot) {
@@ -33147,7 +36145,7 @@ function installedVersion(resolved, pkg) {
33147
36145
  const file2 = join29(current, "package.json");
33148
36146
  if (existsSync9(file2)) {
33149
36147
  try {
33150
- const manifest2 = JSON.parse(readFileSync9(file2, "utf8"));
36148
+ const manifest2 = JSON.parse(readFileSync10(file2, "utf8"));
33151
36149
  if (manifest2.name === pkg && typeof manifest2.version === "string")
33152
36150
  return manifest2.version;
33153
36151
  } catch {
@@ -33160,7 +36158,7 @@ function installedVersion(resolved, pkg) {
33160
36158
  }
33161
36159
  var byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
33162
36160
  function missingPackagesMessage(unsorted) {
33163
- return printable(installSentence(unsorted));
36161
+ return printable2(installSentence(unsorted));
33164
36162
  }
33165
36163
  function installSentence(unsorted) {
33166
36164
  const missing = [...unsorted].sort(
@@ -33241,7 +36239,7 @@ function denoImportsPlugin(options) {
33241
36239
  if (spec.range) {
33242
36240
  const version3 = installedVersion(result.path, pkgName);
33243
36241
  if (version3 !== null && inRange(version3, spec.range) === false) {
33244
- const warning = printable(
36242
+ const warning = printable2(
33245
36243
  BUNDLE_MESSAGES.outOfRange(
33246
36244
  pkgName,
33247
36245
  version3,
@@ -33328,7 +36326,7 @@ function denoImportsPlugin(options) {
33328
36326
 
33329
36327
  // ../../packages/functions-bundler/src/bundle.ts
33330
36328
  import { createHash as createHash4 } from "crypto";
33331
- import { join as join30, relative as relative9, sep as sep9 } from "path";
36329
+ import { join as join30, relative as relative10, sep as sep9 } from "path";
33332
36330
  import { gzipSync } from "zlib";
33333
36331
 
33334
36332
  // ../../packages/functions-runtime/dist/index.js
@@ -33349,7 +36347,7 @@ function runtimeCompressedBytes() {
33349
36347
  return runtimeGzip;
33350
36348
  }
33351
36349
  function relativeLabel(root, file2) {
33352
- return relative9(root, file2).split(sep9).join("/");
36350
+ return relative10(root, file2).split(sep9).join("/");
33353
36351
  }
33354
36352
  function location(message) {
33355
36353
  return message.location ? `${message.location.file}:${String(message.location.line)}` : null;
@@ -33496,7 +36494,7 @@ function bootErrorLine(logs, error51) {
33496
36494
  const logged = logs.find((line2) => line2.includes("Uncaught")) ?? logs.find((line2) => line2.includes("Error"));
33497
36495
  const raw = logged ?? (error51 instanceof Error ? error51.message : String(error51));
33498
36496
  const line = raw.split("\n").map((part) => part.trim()).find((part) => part.length > 0) ?? "unknown error";
33499
- return printable(line.replace(WORKERD_SERVICE_HEAD, ""));
36497
+ return printable2(line.replace(WORKERD_SERVICE_HEAD, ""));
33500
36498
  }
33501
36499
  async function bootCheck(name, code, options = {}) {
33502
36500
  const timeoutMs = options.timeoutMs ?? BOOT_TIMEOUT_MS;
@@ -33577,407 +36575,6 @@ async function bootCheck(name, code, options = {}) {
33577
36575
  }
33578
36576
  }
33579
36577
 
33580
- // src/commands/migration.ts
33581
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
33582
- import { join as join31, relative as relative10 } from "path";
33583
-
33584
- // src/data-environment.ts
33585
- var DATA_ENV_MESSAGES = {
33586
- name: "--env must be production or an environment name.",
33587
- preview: "--env preview has no data of its own: preview deployments use production's. Drop --env, or name an environment.",
33588
- projectWide: (command) => `--env is not an option of ${command}: turning the backend on or off applies to every environment.`
33589
- };
33590
- function dataEnvironment(flags) {
33591
- const value = flags.env;
33592
- if (value === void 0 || value === "production") return null;
33593
- if (value === "preview")
33594
- throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.preview);
33595
- if (environmentSlugProblem(value) !== null)
33596
- throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.name);
33597
- return value;
33598
- }
33599
- async function dataClient(context) {
33600
- return (await context.client()).inEnvironment(dataEnvironment(context.flags));
33601
- }
33602
- function databaseName(environment) {
33603
- return environment === null ? "the project database" : `the ${environment} environment's database`;
33604
- }
33605
- function enableBackendCommand(environment) {
33606
- return environment === null ? "naro backend enable" : `naro environments provision ${environment}`;
33607
- }
33608
- function envFlag(environment) {
33609
- return environment === null ? "" : ` --env ${environment}`;
33610
- }
33611
- function forEnvironment(text3, environment) {
33612
- return environmentGuidance(text3, environment);
33613
- }
33614
-
33615
- // src/prompt.ts
33616
- import { createInterface as createInterface2 } from "readline";
33617
- import { Writable } from "stream";
33618
- function confirmHint(defaultYes) {
33619
- return defaultYes ? "(Y/n)" : "(y/N)";
33620
- }
33621
- function readConfirmAnswer(answer, defaultYes) {
33622
- const normalized2 = answer.trim().toLowerCase();
33623
- if (normalized2 === "") return defaultYes;
33624
- if (normalized2 === "y" || normalized2 === "yes") return true;
33625
- if (!defaultYes) return false;
33626
- if (normalized2 === "n" || normalized2 === "no") return false;
33627
- return null;
33628
- }
33629
- function isInteractive() {
33630
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
33631
- }
33632
- async function readStdin() {
33633
- let data = "";
33634
- for await (const chunk of process.stdin) data += String(chunk);
33635
- return data.replace(/\r?\n$/, "");
33636
- }
33637
- var TtyPrompter = class {
33638
- async ask(question) {
33639
- const rl = createInterface2({
33640
- input: process.stdin,
33641
- output: process.stdout
33642
- });
33643
- try {
33644
- return await new Promise(
33645
- (resolve15) => rl.question(question, resolve15)
33646
- );
33647
- } finally {
33648
- rl.close();
33649
- }
33650
- }
33651
- async askHidden(question) {
33652
- let muted = false;
33653
- const output = new Writable({
33654
- write(chunk, _enc, done) {
33655
- if (!muted) process.stdout.write(chunk);
33656
- done();
33657
- }
33658
- });
33659
- const rl = createInterface2({
33660
- input: process.stdin,
33661
- output,
33662
- terminal: true
33663
- });
33664
- try {
33665
- const answer = new Promise(
33666
- (resolve15) => rl.question(question, resolve15)
33667
- );
33668
- muted = true;
33669
- const value = await answer;
33670
- process.stdout.write("\n");
33671
- return value;
33672
- } finally {
33673
- rl.close();
33674
- }
33675
- }
33676
- async confirm(question, options = {}) {
33677
- const defaultYes = options.defaultYes ?? false;
33678
- for (; ; ) {
33679
- const read = readConfirmAnswer(
33680
- await this.ask(`${question} ${confirmHint(defaultYes)} `),
33681
- defaultYes
33682
- );
33683
- if (read !== null) return read;
33684
- }
33685
- }
33686
- async select(question, options) {
33687
- process.stdout.write(`${question}
33688
- `);
33689
- options.forEach((option, index) => {
33690
- process.stdout.write(` ${String(index + 1)}. ${option}
33691
- `);
33692
- });
33693
- for (; ; ) {
33694
- const answer = Number.parseInt(
33695
- (await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
33696
- 10
33697
- );
33698
- if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
33699
- return answer - 1;
33700
- }
33701
- }
33702
- };
33703
- var NonInteractivePrompter = class {
33704
- constructor(assumeYes) {
33705
- this.assumeYes = assumeYes;
33706
- }
33707
- refuse(question) {
33708
- throw new NaroError(
33709
- "VALIDATION_FAILED",
33710
- `Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
33711
- );
33712
- }
33713
- async ask(question) {
33714
- this.refuse(question);
33715
- }
33716
- async askHidden(question) {
33717
- this.refuse(question);
33718
- }
33719
- async confirm() {
33720
- return this.assumeYes;
33721
- }
33722
- async select(question) {
33723
- this.refuse(question);
33724
- }
33725
- };
33726
- function createPrompter(options) {
33727
- if (options.yes || options.json || !isInteractive())
33728
- return new NonInteractivePrompter(options.yes);
33729
- return new TtyPrompter();
33730
- }
33731
-
33732
- // src/commands/confirm.ts
33733
- function cannotAsk(context) {
33734
- return context.flags.json || !isInteractive();
33735
- }
33736
- async function confirmOrAbort(context, question) {
33737
- if (context.flags.yes) return;
33738
- if (cannotAsk(context)) {
33739
- throw new NaroError(
33740
- "CONFIRMATION_REQUIRED",
33741
- `${question} Re-run with --yes to confirm.`
33742
- );
33743
- }
33744
- const confirmed = await context.prompter.confirm(question);
33745
- if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
33746
- }
33747
-
33748
- // src/commands/migration.ts
33749
- var processStdin = {
33750
- stdinIsTty: () => process.stdin.isTTY === true,
33751
- readStdin
33752
- };
33753
- function migrationsDir(context) {
33754
- return resolveMigrationsDir({
33755
- baseDir: context.cwd,
33756
- rootDirectory: context.flags.root,
33757
- dir: context.flags.dir
33758
- });
33759
- }
33760
- function relativeToCwd(context, path) {
33761
- const rel = relative10(context.cwd, path);
33762
- return rel === "" ? "." : rel;
33763
- }
33764
- async function appliedMigrations(client, projectId) {
33765
- try {
33766
- return await client.db.migrations(projectId);
33767
- } catch (error51) {
33768
- if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY") return [];
33769
- throw error51;
33770
- }
33771
- }
33772
- var UNPRINTABLE2 = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
33773
- function printable2(text3) {
33774
- return text3.replace(UNPRINTABLE2, "?");
33775
- }
33776
- function isPrintable(text3) {
33777
- return printable2(text3) === text3;
33778
- }
33779
- var UNSHOWN_WARNING_LINE = "(a line of this warning has characters the terminal cannot show; run with --json to see it exactly)";
33780
- function warningLines(message) {
33781
- return message.split("\n").map(
33782
- (line, index) => index === 0 ? `\u26A0 ${printable2(line)}` : ` ${isPrintable(line) ? line : UNSHOWN_WARNING_LINE}`
33783
- );
33784
- }
33785
- function policyProblems(dir, names) {
33786
- return names.flatMap((name) => {
33787
- const split2 = splitStatements(readFileSync10(join31(dir, name), "utf8"));
33788
- if (!split2.ok) return [];
33789
- const planned = planPolicyMigration(
33790
- split2.statements.map((statement2) => statement2.text)
33791
- );
33792
- return planned.ok ? [] : [{ migration: name, message: planned.message }];
33793
- });
33794
- }
33795
- function warningLine(warning) {
33796
- if (warning === "out-of-order")
33797
- return " \u26A0 older than an applied migration \u2014 db push will still apply it";
33798
- if (warning === "ledger-name-missing-extension")
33799
- return " \u26A0 recorded without .sql in the ledger";
33800
- return null;
33801
- }
33802
- async function runMigrationNew(context, deps = processStdin) {
33803
- const [name] = context.positionalOrThrow(
33804
- 1,
33805
- "Usage: naro migration new <name>"
33806
- );
33807
- const dir = migrationsDir(context);
33808
- const fileName = nextMigrationFileName(
33809
- listMigrationFiles(dir),
33810
- name
33811
- );
33812
- mkdirSync3(dir, { recursive: true });
33813
- const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
33814
- const fromStdin = piped.length > 0;
33815
- const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
33816
- ` : `-- Migration: ${fileName}
33817
- `;
33818
- const path = join31(dir, fileName);
33819
- writeFileSync3(path, body, { flag: "wx" });
33820
- const relPath = relativeToCwd(context, path);
33821
- context.output.result({ file: fileName, path: relPath, fromStdin }, [
33822
- `\u2705 Created ${relPath}`
33823
- ]);
33824
- }
33825
- async function runMigrationList(context) {
33826
- const client = await dataClient(context);
33827
- const projectId = await context.resolveProjectId();
33828
- const dir = migrationsDir(context);
33829
- const local = listMigrationFiles(dir);
33830
- const ledger = await appliedMigrations(client, projectId);
33831
- const migrations = compareMigrations(local, ledger);
33832
- const relDir = relativeToCwd(context, dir);
33833
- const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
33834
- `Migrations in ${relDir} (${String(migrations.length)}):`,
33835
- ...migrations.flatMap((row) => {
33836
- const line = ` ${row.status.padEnd(12)}${printable2(row.name)}${row.appliedAt ? ` ${printable2(row.appliedAt)}` : ""}`;
33837
- const warning = warningLine(row.warning);
33838
- return warning ? [line, warning] : [line];
33839
- })
33840
- ];
33841
- context.output.result({ projectId, dir: relDir, migrations }, lines);
33842
- }
33843
- async function applyMigrationFiles(context, client, projectId, dir, names) {
33844
- const applied = [];
33845
- const warnings = [];
33846
- for (const name of names) {
33847
- const sql = readFileSync10(join31(dir, name), "utf8");
33848
- let answer;
33849
- try {
33850
- answer = await client.db.applyMigration(projectId, { name, sql });
33851
- } catch (error51) {
33852
- if (!isNaroError(error51)) throw error51;
33853
- for (const message of serverWarnings(error51.details))
33854
- for (const line of warningLines(message)) context.output.warn(line);
33855
- const shown = context.output.json ? name : printable2(name);
33856
- throw new NaroError(error51.code, `${shown}: ${error51.message}`, {
33857
- ...error51.status === void 0 ? {} : { status: error51.status },
33858
- details: {
33859
- ...error51.details,
33860
- migration: name,
33861
- applied,
33862
- appliedWarnings: warnings
33863
- },
33864
- cause: error51
33865
- });
33866
- }
33867
- context.output.log(`\u2713 ${printable2(name)}`);
33868
- applied.push(name);
33869
- for (const message of answer.warnings) {
33870
- warnings.push({ migration: name, message });
33871
- for (const line of warningLines(message)) context.output.warn(line);
33872
- }
33873
- }
33874
- return { applied, warnings };
33875
- }
33876
- function serverWarnings(details) {
33877
- const warnings = details?.warnings;
33878
- return Array.isArray(warnings) ? warnings.filter(
33879
- (warning) => typeof warning === "string"
33880
- ) : [];
33881
- }
33882
- var MIGRATION_NAME_RE = /^[A-Za-z0-9_.-]{1,200}\.sql$/;
33883
- var LEDGER_DDL = "CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
33884
- var LEDGER_INSERT = "INSERT INTO d1_migrations (name) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM d1_migrations WHERE name = ?)";
33885
- var LEDGER_DELETE = "DELETE FROM d1_migrations WHERE name IN (?, ?)";
33886
- function withoutSuffix(key) {
33887
- return key.slice(0, -".sql".length);
33888
- }
33889
- async function runMigrationRepair(context) {
33890
- const [name] = context.positionalOrThrow(
33891
- 1,
33892
- "Usage: naro migration repair <name> --status applied|reverted"
33893
- );
33894
- const key = name;
33895
- if (!MIGRATION_NAME_RE.test(key))
33896
- throw new NaroError(
33897
- "VALIDATION_FAILED",
33898
- "name must end with .sql and contain only letters, digits, _ . -"
33899
- );
33900
- const status = context.flags.status;
33901
- if (status !== "applied" && status !== "reverted")
33902
- throw new NaroError(
33903
- "VALIDATION_FAILED",
33904
- "Pass --status applied or --status reverted."
33905
- );
33906
- const client = await dataClient(context);
33907
- const projectId = await context.resolveProjectId();
33908
- const ledger = await appliedMigrations(client, projectId);
33909
- const recorded = ledger.some(
33910
- (row) => row.name === key || row.name === withoutSuffix(key)
33911
- );
33912
- const nothingToDo = status === "applied" ? `${key} was already applied; nothing to do.` : `${key} was not in the ledger; nothing to do.`;
33913
- if (status === "applied" ? recorded : !recorded) {
33914
- context.output.warn(nothingToDo);
33915
- context.output.result({ name: key, status, changed: false });
33916
- return;
33917
- }
33918
- await confirmOrAbort(
33919
- context,
33920
- status === "applied" ? `Mark ${key} as applied in ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not run.` : `Remove ${key} from ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not rolled back.`
33921
- );
33922
- if (status === "applied" && ledger.length === 0)
33923
- await client.db.query(projectId, { sql: LEDGER_DDL });
33924
- const answer = await client.db.query(projectId, {
33925
- sql: status === "applied" ? LEDGER_INSERT : LEDGER_DELETE,
33926
- params: status === "applied" ? [key, key] : [key, withoutSuffix(key)]
33927
- });
33928
- const changed = (answer.statements[0]?.meta.changes ?? 0) > 0;
33929
- if (!changed) {
33930
- context.output.warn(nothingToDo);
33931
- context.output.result({ name: key, status, changed: false });
33932
- return;
33933
- }
33934
- context.output.result({ name: key, status, changed: true }, [
33935
- status === "applied" ? `\u2713 Marked ${key} as applied.` : `\u2713 Removed ${key} from the ledger.`
33936
- ]);
33937
- }
33938
- async function runMigrationUp(context) {
33939
- const client = await dataClient(context);
33940
- const projectId = await context.resolveProjectId();
33941
- const dir = migrationsDir(context);
33942
- const local = listMigrationFiles(dir);
33943
- const ledger = await appliedMigrations(client, projectId);
33944
- const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
33945
- if (pending.length === 0) {
33946
- context.output.result({ projectId, applied: [], pending: [] }, [
33947
- "No pending migrations."
33948
- ]);
33949
- return;
33950
- }
33951
- if (context.flags.dryRun) {
33952
- const problems2 = policyProblems(dir, pending);
33953
- context.output.result({ projectId, dryRun: true, pending, problems: problems2 }, [
33954
- `Pending migrations (${String(pending.length)}):`,
33955
- ...pending.map((name) => ` ${printable2(name)}`),
33956
- ...problems2.map(
33957
- (problem) => `${printable2(problem.migration)}: ${printable2(problem.message)}`
33958
- )
33959
- ]);
33960
- if (problems2.length > 0) process.exitCode = 1;
33961
- return;
33962
- }
33963
- context.output.log(`Pending migrations (${String(pending.length)}):`);
33964
- for (const name of pending) context.output.log(` ${printable2(name)}`);
33965
- await confirmOrAbort(
33966
- context,
33967
- `Apply ${String(pending.length)} migration(s) to ${databaseName(dataEnvironment(context.flags))}? Each file is applied all-or-nothing; a failure stops the run.`
33968
- );
33969
- const { applied, warnings } = await applyMigrationFiles(
33970
- context,
33971
- client,
33972
- projectId,
33973
- dir,
33974
- pending
33975
- );
33976
- context.output.result({ projectId, applied, pending: [], warnings }, [
33977
- `\u2705 Applied ${String(applied.length)} migration(s).`
33978
- ]);
33979
- }
33980
-
33981
36578
  // src/functions/retry.ts
33982
36579
  var FUNCTION_RETRIES = 3;
33983
36580
  var FUNCTION_BUSY_WAIT_MS = 5e3;
@@ -34262,7 +36859,7 @@ async function deployFunctions(options) {
34262
36859
  if (!PER_FUNCTION_CODES.has(error51.code))
34263
36860
  throw withProgress(error51, deployed, failures);
34264
36861
  failures.push({ name, code: error51.code, message: error51.message });
34265
- io.log(`\u2717 ${name}: ${printable2(error51.message)}`);
36862
+ io.log(`\u2717 ${name}: ${printable(error51.message)}`);
34266
36863
  }
34267
36864
  }
34268
36865
  if (failures.length > 0)
@@ -34375,7 +36972,7 @@ function slugify2(value) {
34375
36972
  async function projectName(sourceDir) {
34376
36973
  try {
34377
36974
  const pkg = JSON.parse(
34378
- await readFile15(join32(sourceDir, "package.json"), "utf8")
36975
+ await readFile15(join31(sourceDir, "package.json"), "utf8")
34379
36976
  );
34380
36977
  if (pkg.name) return pkg.name;
34381
36978
  } catch {
@@ -34385,7 +36982,7 @@ async function projectName(sourceDir) {
34385
36982
  function resolveBin(name) {
34386
36983
  let dir = dirname10(fileURLToPath4(import.meta.url));
34387
36984
  for (; ; ) {
34388
- const candidate = join32(dir, "node_modules", ".bin", name);
36985
+ const candidate = join31(dir, "node_modules", ".bin", name);
34389
36986
  if (existsSync10(candidate)) return candidate;
34390
36987
  const parent = dirname10(dir);
34391
36988
  if (parent === dir) return name;
@@ -34452,7 +37049,7 @@ function workingTreeNotices(copy) {
34452
37049
  }
34453
37050
  function localDependencies(sourceDir, say = () => void 0) {
34454
37051
  return {
34455
- createWorkspace: () => mkdtemp2(join32(tmpdir2(), "naro-build-")),
37052
+ createWorkspace: () => mkdtemp2(join31(tmpdir2(), "naro-build-")),
34456
37053
  removeWorkspace: (path) => rm5(path, { recursive: true, force: true }),
34457
37054
  async runCommand(spec, observer) {
34458
37055
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -34557,7 +37154,7 @@ async function deployDirectory(sourceDir, options, log2) {
34557
37154
  const set2 = missing.filter((key) => isNaroSystemName(key));
34558
37155
  missing = missing.filter((key) => !isNaroSystemName(key));
34559
37156
  log2(
34560
- `\u25B8 naro sets ${set2.join(", ")} for ${environment} to its own backend's: not asked for`
37157
+ `\u25B8 Naro sets ${set2.join(", ")} for ${environment} to its own backend's: not asked for`
34561
37158
  );
34562
37159
  }
34563
37160
  }
@@ -34707,11 +37304,17 @@ function deploymentUrl(detail, ctx, production) {
34707
37304
  }
34708
37305
 
34709
37306
  export {
37307
+ CAPABILITIES,
37308
+ dashboardNeeds,
34710
37309
  parseDotenvDetailed,
37310
+ FUNCTION_INVOKE_METHODS,
37311
+ FUNCTION_INVOKE_MAX_RESPONSE_BYTES,
37312
+ FUNCTION_INVOKE_TIMEOUT_MS,
34711
37313
  isFunctionName,
34712
37314
  parseGitHubRemote,
34713
37315
  schemaMigrationTitle,
34714
37316
  projectPath,
37317
+ readCapped,
34715
37318
  ANALYTICS_FILTER_KEYS,
34716
37319
  NaroError,
34717
37320
  promoteTargetUnsupported,
@@ -34740,6 +37343,8 @@ export {
34740
37343
  POLICY_RULES,
34741
37344
  POLICY_ACTIONS,
34742
37345
  NaroClient,
37346
+ dashboardCapability,
37347
+ dashboardLink,
34743
37348
  BOOKMARK_FORMAT_MESSAGE,
34744
37349
  isBookmark,
34745
37350
  RESTORE_OUTCOME_UNKNOWN_CODES,
@@ -34753,18 +37358,12 @@ export {
34753
37358
  parseCacheControl,
34754
37359
  STORAGE_OBJECTS_TABLE,
34755
37360
  dataError,
34756
- SCHEMA_SQL,
34757
- tableNames,
34758
- snapshotFromRows,
34759
37361
  quoteIdent,
34760
37362
  commentText,
34761
37363
  quotePolicyName,
34762
37364
  NAMED_POLICY_TABLE,
34763
37365
  NAMED_POLICY_SQL,
34764
- loadPolicy,
34765
37366
  parsePolicyNode,
34766
- INDEX_SQL,
34767
- indexesFromRows,
34768
37367
  parsePolicyStatement,
34769
37368
  splitPolicyStatements,
34770
37369
  STORAGE_SUBQUERIES_SQL,
@@ -34777,8 +37376,7 @@ export {
34777
37376
  writeProjectLink,
34778
37377
  removeProjectLink,
34779
37378
  ensureGitignoreHasNaro,
34780
- countLintedTables,
34781
- lintSchema,
37379
+ lintDatabase,
34782
37380
  listMigrationFiles,
34783
37381
  nextMigrationFileName,
34784
37382
  pendingMigrationNames,
@@ -34800,9 +37398,6 @@ export {
34800
37398
  pullEnv,
34801
37399
  detectGitInfo,
34802
37400
  version2 as version,
34803
- BUNDLE_MESSAGES,
34804
- functionsFolder,
34805
- parseJsonc2 as parseJsonc,
34806
37401
  DATA_ENV_MESSAGES,
34807
37402
  dataEnvironment,
34808
37403
  dataClient,
@@ -34818,7 +37413,7 @@ export {
34818
37413
  migrationsDir,
34819
37414
  relativeToCwd,
34820
37415
  appliedMigrations,
34821
- printable2 as printable,
37416
+ printable,
34822
37417
  isPrintable,
34823
37418
  UNSHOWN_WARNING_LINE,
34824
37419
  warningLines,
@@ -34828,6 +37423,10 @@ export {
34828
37423
  applyMigrationFiles,
34829
37424
  runMigrationRepair,
34830
37425
  runMigrationUp,
37426
+ runDbLint,
37427
+ BUNDLE_MESSAGES,
37428
+ functionsFolder,
37429
+ parseJsonc2 as parseJsonc,
34831
37430
  defaultSleep,
34832
37431
  retryAfterSeconds,
34833
37432
  withFunctionRetries,