naro-cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,9 +69,13 @@ naro logs <deployment-id> Build logs of a deployment
69
69
  naro logs --type build --follow Stream build logs until READY/FAILED
70
70
  naro logs --env preview Preview-surface runtime logs
71
71
 
72
- naro insights Speed Insights: Web Vitals p75 (--metric lcp, --range 7d)
72
+ naro analytics Web Analytics: visitors, page views, top pages, referrers, countries, devices, browsers, operating systems (--range 7d)
73
+ naro analytics --country KR --os ios Narrow it: --path, --referrer, --country, --device, --browser, --os (one value each, all must match)
74
+
75
+ naro insights Speed Insights: Web Vitals p75, and the page load's p75 and the mean of each step (--metric lcp, --range 7d)
73
76
  naro insights --route '/posts/[id]' The paths under one route
74
77
  naro insights --env preview The same for the preview worker
78
+ naro insights --country KR --device mobile Narrow it: --path, --country, --device, --browser, --os (no --referrer: Speed Insights has none)
75
79
  naro insights enable | disable Turn measuring on or off
76
80
 
77
81
  naro protection Preview protection: on or off, and whether a bypass secret is set
@@ -789,6 +793,39 @@ An `--update-check` you give replaces the default: without
789
793
 
790
794
  ## Release notes
791
795
 
796
+ ### 0.14.0
797
+
798
+ - `naro analytics` shows the project's Web Analytics from the terminal: the
799
+ range's visitors and page views (`--range 24h|7d|30d`, 7d by default), then
800
+ the top pages, referrers, countries, devices, browsers and operating systems
801
+ by page views, and `--json` prints the whole report, trend included. The
802
+ same six filters as on the dashboard narrow it, one value each, all of them
803
+ at once: `naro analytics --country KR --os ios`. The first line names the
804
+ filters that applied; filters that leave no visit print one line under it,
805
+ `No visits match these filters. Drop a filter to see more.`, instead of six
806
+ empty tables. The CLI does not check the values; the server answers
807
+ `400 VALIDATION_FAILED` with the rule the value broke (`--country` is an
808
+ uppercase two-letter code like `KR`, `--referrer` a lowercase host like
809
+ `t.co`; `kr` and `T.co` are refused). It reads the
810
+ production worker only, so `--env` is refused with `VALIDATION_FAILED` and
811
+ nothing is sent. An API token (`--token`, `NARO_TOKEN`) can read it.
812
+ - `naro insights` takes the same filters: `--path`, `--country`, `--device`,
813
+ `--browser` and `--os` narrow it to one value each (`--referrer` is refused
814
+ with `VALIDATION_FAILED` and nothing is sent: a vitals row has no referrer).
815
+ The first line names the filters that applied
816
+ (`· filtered by country=KR, os=ios`); filters that leave no measurement print
817
+ `No measurements match these filters. Drop a filter to see more.` instead of
818
+ waiting advice.
819
+ The filters narrow a report only: `naro insights enable | disable` refuses
820
+ them (`Filters only narrow a report; drop --country.`) and sends nothing, and
821
+ an empty value (`--referrer=`) is no filter.
822
+ - `naro insights` prints a `Page load p75 … · mean per step: …` line under the
823
+ metrics table: the p75 of how long visits took to load the page, to the end
824
+ of the load event, then the mean of each step (wait, DNS, connect, server,
825
+ download, processing, onload), so the steps need not add up to the p75. It is
826
+ left out until a visit has sent a page load. The dashboard's Speed Insights
827
+ tab has the same Page load card.
828
+
792
829
  ### 0.13.0
793
830
 
794
831
  - `naro insights` shows the project's Speed Insights from the terminal: each
@@ -18488,6 +18488,16 @@ function createLogSender(ports) {
18488
18488
  };
18489
18489
  }
18490
18490
 
18491
+ // ../../packages/sdk/src/analytics.ts
18492
+ var ANALYTICS_FILTER_KEYS = [
18493
+ "path",
18494
+ "referrer",
18495
+ "country",
18496
+ "device",
18497
+ "browser",
18498
+ "os"
18499
+ ];
18500
+
18491
18501
  // ../../packages/sdk/src/auth.ts
18492
18502
  import { homedir as homedir2 } from "os";
18493
18503
 
@@ -18960,6 +18970,23 @@ function openBrowser(url2) {
18960
18970
  child.unref();
18961
18971
  }
18962
18972
 
18973
+ // ../../packages/sdk/src/insights.ts
18974
+ var SPEED_INSIGHTS_METRICS = [
18975
+ "lcp",
18976
+ "inp",
18977
+ "cls",
18978
+ "fcp",
18979
+ "ttfb"
18980
+ ];
18981
+ var SPEED_INSIGHTS_FILTER_KEYS = [
18982
+ "path",
18983
+ "country",
18984
+ "device",
18985
+ "browser",
18986
+ "os"
18987
+ ];
18988
+ var SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES = 20;
18989
+
18963
18990
  // ../../packages/sdk/src/types.ts
18964
18991
  var TERMINAL_DEPLOYMENT_STATUSES = [
18965
18992
  "READY",
@@ -19052,6 +19079,7 @@ var NaroClient = class {
19052
19079
  backend;
19053
19080
  storage;
19054
19081
  insights;
19082
+ analytics;
19055
19083
  protection;
19056
19084
  constructor(options) {
19057
19085
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
@@ -19069,6 +19097,7 @@ var NaroClient = class {
19069
19097
  this.backend = new BackendApi(this);
19070
19098
  this.storage = new StorageApi(this);
19071
19099
  this.insights = new InsightsApi(this);
19100
+ this.analytics = new AnalyticsApi(this);
19072
19101
  this.protection = new ProtectionApi(this);
19073
19102
  }
19074
19103
  async request(path, options) {
@@ -19477,12 +19506,20 @@ var LogsApi = class {
19477
19506
  return body.entries;
19478
19507
  }
19479
19508
  };
19509
+ function appendFilters(params, keys, filters) {
19510
+ if (!filters) return;
19511
+ for (const key of keys) {
19512
+ const value = filters[key];
19513
+ if (value) params.set(key, value);
19514
+ }
19515
+ }
19480
19516
  function insightsQuery(options) {
19481
19517
  const params = new URLSearchParams();
19482
19518
  if (options.range !== void 0) params.set("range", options.range);
19483
19519
  if (options.metric !== void 0) params.set("metric", options.metric);
19484
19520
  if (options.environment !== void 0) params.set("env", options.environment);
19485
19521
  if (options.route !== void 0) params.set("route", options.route);
19522
+ appendFilters(params, SPEED_INSIGHTS_FILTER_KEYS, options.filters);
19486
19523
  const text2 = params.toString();
19487
19524
  return text2 ? `?${text2}` : "";
19488
19525
  }
@@ -19493,7 +19530,7 @@ var InsightsApi = class {
19493
19530
  base(projectId) {
19494
19531
  return `/api/projects/${encodeURIComponent(projectId)}/speed-insights`;
19495
19532
  }
19496
- /** The five metrics' summary, and the trend, pages, devices and elements of one. */
19533
+ /** The five metrics' summary, and the trend, pages, devices, browsers, operating systems, countries and elements of one. */
19497
19534
  async report(projectId, options = {}) {
19498
19535
  return this.client.request(
19499
19536
  `${this.base(projectId)}${insightsQuery(options)}`,
@@ -19553,6 +19590,22 @@ var InsightsApi = class {
19553
19590
  });
19554
19591
  }
19555
19592
  };
19593
+ var AnalyticsApi = class {
19594
+ constructor(client) {
19595
+ this.client = client;
19596
+ }
19597
+ /** Visitors, page views, the trend, and the pages, referrers, countries, devices, browsers and operating systems behind them. */
19598
+ async report(projectId, options = {}) {
19599
+ const params = new URLSearchParams();
19600
+ if (options.range !== void 0) params.set("range", options.range);
19601
+ appendFilters(params, ANALYTICS_FILTER_KEYS, options.filters);
19602
+ const text2 = params.toString();
19603
+ return this.client.request(
19604
+ `/api/projects/${encodeURIComponent(projectId)}/analytics${text2 ? `?${text2}` : ""}`,
19605
+ { context: "Read Analytics" }
19606
+ );
19607
+ }
19608
+ };
19556
19609
  var ProtectionApi = class {
19557
19610
  constructor(client) {
19558
19611
  this.client = client;
@@ -27333,16 +27386,6 @@ function generateTypes(tables, options = {}) {
27333
27386
  `;
27334
27387
  }
27335
27388
 
27336
- // ../../packages/sdk/src/insights.ts
27337
- var SPEED_INSIGHTS_METRICS = [
27338
- "lcp",
27339
- "inp",
27340
- "cls",
27341
- "fcp",
27342
- "ttfb"
27343
- ];
27344
- var SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES = 20;
27345
-
27346
27389
  // ../../packages/sdk/src/link.ts
27347
27390
  import {
27348
27391
  existsSync as existsSync4,
@@ -28464,6 +28507,7 @@ export {
28464
28507
  schemaMigrationTitle,
28465
28508
  projectPath,
28466
28509
  splitStatements,
28510
+ ANALYTICS_FILTER_KEYS,
28467
28511
  NaroError,
28468
28512
  isNaroError,
28469
28513
  toNaroError,
@@ -28477,6 +28521,9 @@ export {
28477
28521
  login,
28478
28522
  resolveAuth,
28479
28523
  openBrowser,
28524
+ SPEED_INSIGHTS_METRICS,
28525
+ SPEED_INSIGHTS_FILTER_KEYS,
28526
+ SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
28480
28527
  isTerminalStatus,
28481
28528
  BACKEND_STEPS,
28482
28529
  isStalledBackend,
@@ -28515,8 +28562,6 @@ export {
28515
28562
  formatRedirectEntry,
28516
28563
  policiesForTypes,
28517
28564
  generateTypes,
28518
- SPEED_INSIGHTS_METRICS,
28519
- SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
28520
28565
  readProjectLink,
28521
28566
  writeProjectLink,
28522
28567
  removeProjectLink,
package/dist/deploy.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseDotenv,
6
6
  readProjectEnv,
7
7
  slugify
8
- } from "./chunk-O4NJRDJE.js";
8
+ } from "./chunk-U7AZSAFS.js";
9
9
  import "./chunk-K3NQKI34.js";
10
10
  export {
11
11
  deployDirectory,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ ANALYTICS_FILTER_KEYS,
3
4
  BACKEND_STEPS,
4
5
  BOOKMARK_FORMAT_MESSAGE,
5
6
  INDEX_SQL,
@@ -13,6 +14,7 @@ import {
13
14
  RESET_BACKEND_OUTDATED,
14
15
  RESTORE_OUTCOME_UNKNOWN_CODES,
15
16
  SCHEMA_SQL,
17
+ SPEED_INSIGHTS_FILTER_KEYS,
16
18
  SPEED_INSIGHTS_METRICS,
17
19
  SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
18
20
  STORAGE_OBJECTS_TABLE,
@@ -81,7 +83,7 @@ import {
81
83
  toNaroError,
82
84
  unreadablePolicyComment,
83
85
  writeProjectLink
84
- } from "./chunk-O4NJRDJE.js";
86
+ } from "./chunk-U7AZSAFS.js";
85
87
  import "./chunk-K3NQKI34.js";
86
88
 
87
89
  // src/args.ts
@@ -139,6 +141,12 @@ var STRING_FLAGS = {
139
141
  "--metric": "metric",
140
142
  "--range": "range",
141
143
  "--route": "route",
144
+ "--path": "path",
145
+ "--referrer": "referrer",
146
+ "--country": "country",
147
+ "--device": "device",
148
+ "--browser": "browser",
149
+ "--os": "os",
142
150
  "--file": "file",
143
151
  "--output": "output",
144
152
  "--lang": "lang",
@@ -490,86 +498,9 @@ import { readFileSync as readFileSync5 } from "fs";
490
498
  import { dirname as dirname4, join as join7 } from "path";
491
499
  import { fileURLToPath } from "url";
492
500
 
493
- // src/commands/auth.ts
494
- async function runLogin(context) {
495
- const { output } = context;
496
- output.log("Opening your browser to sign in with GitHub...");
497
- const session = await login({
498
- openBrowser: (url) => {
499
- output.log(`If your browser did not open, visit:
500
- ${url}`);
501
- openBrowser(url);
502
- }
503
- });
504
- saveSession(session);
505
- output.result(
506
- {
507
- user: {
508
- id: session.user.id,
509
- email: session.user.email,
510
- github: session.user.github
511
- }
512
- },
513
- [`\u2705 Logged in as ${session.user.email ?? session.user.id}`]
514
- );
515
- }
516
- async function runLogout(context) {
517
- const session = loadStoredSession();
518
- if (session) {
519
- try {
520
- const client = createAuthClient();
521
- await client.auth.setSession({
522
- access_token: session.access_token,
523
- refresh_token: session.refresh_token
524
- });
525
- await client.auth.signOut();
526
- } catch {
527
- }
528
- }
529
- clearSession();
530
- context.output.result({ loggedOut: true }, ["Logged out."]);
531
- }
532
- async function currentSession() {
533
- let session = loadStoredSession();
534
- if (!session)
535
- throw new NaroError("NOT_LOGGED_IN", "Not logged in. Run `naro login`.");
536
- if (isExpired(session)) {
537
- try {
538
- session = await refreshStoredSession(session.refresh_token);
539
- saveSession(session);
540
- } catch {
541
- throw new NaroError(
542
- "NOT_LOGGED_IN",
543
- "Session expired. Run `naro login`."
544
- );
545
- }
546
- }
547
- return session;
548
- }
549
- async function runWhoami(context) {
550
- const session = await currentSession();
551
- const handle = session.user.github ? ` (${session.user.github})` : "";
552
- context.output.result(
553
- {
554
- user: {
555
- id: session.user.id,
556
- email: session.user.email,
557
- github: session.user.github
558
- }
559
- },
560
- [`Logged in as ${session.user.email ?? session.user.id}${handle}`]
561
- );
562
- }
563
- async function runToken(context) {
564
- const session = await currentSession();
565
- process.stderr.write(
566
- "\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
567
- );
568
- context.output.result(
569
- { token: session.access_token, expiresAt: session.expires_at },
570
- [session.access_token]
571
- );
572
- }
501
+ // src/commands/migration.ts
502
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
503
+ import { join, relative } from "path";
573
504
 
574
505
  // src/commands/confirm.ts
575
506
  function cannotAsk(context) {
@@ -588,8 +519,6 @@ async function confirmOrAbort(context, question) {
588
519
  }
589
520
 
590
521
  // src/commands/migration.ts
591
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
592
- import { join, relative } from "path";
593
522
  var processStdin = {
594
523
  stdinIsTty: () => process.stdin.isTTY === true,
595
524
  readStdin
@@ -792,34 +721,409 @@ async function runMigrationUp(context) {
792
721
  ]);
793
722
  return;
794
723
  }
795
- if (context.flags.dryRun) {
796
- const problems = policyProblems(dir, pending);
797
- context.output.result({ projectId, dryRun: true, pending, problems }, [
798
- `Pending migrations (${String(pending.length)}):`,
799
- ...pending.map((name) => ` ${printable(name)}`),
800
- ...problems.map(
801
- (problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
802
- )
803
- ]);
804
- if (problems.length > 0) process.exitCode = 1;
805
- return;
724
+ if (context.flags.dryRun) {
725
+ const problems = policyProblems(dir, pending);
726
+ context.output.result({ projectId, dryRun: true, pending, problems }, [
727
+ `Pending migrations (${String(pending.length)}):`,
728
+ ...pending.map((name) => ` ${printable(name)}`),
729
+ ...problems.map(
730
+ (problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
731
+ )
732
+ ]);
733
+ if (problems.length > 0) process.exitCode = 1;
734
+ return;
735
+ }
736
+ context.output.log(`Pending migrations (${String(pending.length)}):`);
737
+ for (const name of pending) context.output.log(` ${printable(name)}`);
738
+ await confirmOrAbort(
739
+ context,
740
+ `Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
741
+ );
742
+ const { applied, warnings } = await applyMigrationFiles(
743
+ context,
744
+ client,
745
+ projectId,
746
+ dir,
747
+ pending
748
+ );
749
+ context.output.result({ projectId, applied, pending: [], warnings }, [
750
+ `\u2705 Applied ${String(applied.length)} migration(s).`
751
+ ]);
752
+ }
753
+
754
+ // src/commands/filters.ts
755
+ function filtersOf(flags, keys) {
756
+ const filters = {};
757
+ for (const key of keys) {
758
+ const value = flags[key];
759
+ if (value !== void 0) filters[key] = value;
760
+ }
761
+ return filters;
762
+ }
763
+ function givenFilterFlag(flags) {
764
+ return ANALYTICS_FILTER_KEYS.find((key) => {
765
+ const value = flags[key];
766
+ return value !== void 0 && value !== "";
767
+ });
768
+ }
769
+ function filteredBy(filters) {
770
+ const applied = ANALYTICS_FILTER_KEYS.flatMap((key) => {
771
+ const value = filters[key];
772
+ return value ? [`${key}=${printable(value)}`] : [];
773
+ });
774
+ return applied.length === 0 ? "" : ` \xB7 filtered by ${applied.join(", ")}`;
775
+ }
776
+
777
+ // src/commands/insights-format.ts
778
+ var OFF_LINE = "Speed Insights is off for this project. Turn it on: naro insights enable";
779
+ var TURNED_ON = "Speed Insights is on. Measuring starts within about a minute; values arrive when visitors leave a page.";
780
+ var TURNED_OFF = "Speed Insights is off. Measuring stops within about a minute; turning it back on shows past measurements again.";
781
+ var RANGE_TEXT = {
782
+ "24h": "last 24 hours",
783
+ "7d": "last 7 days",
784
+ "30d": "last 30 days"
785
+ };
786
+ var PART_TEXT = {
787
+ ttfb: "TTFB",
788
+ loadDelay: "load delay",
789
+ loadDuration: "load duration",
790
+ renderDelay: "render delay",
791
+ inputDelay: "input delay",
792
+ processing: "processing",
793
+ presentation: "presentation"
794
+ };
795
+ var PAGE_LOAD_PART_TEXT = {
796
+ wait: "wait",
797
+ dns: "DNS",
798
+ connect: "connect",
799
+ server: "server",
800
+ download: "download",
801
+ processing: "processing",
802
+ onload: "onload"
803
+ };
804
+ function rangeText(range) {
805
+ const known = RANGE_TEXT;
806
+ return known[range] ?? printable(range);
807
+ }
808
+ function partText(name) {
809
+ const known = PART_TEXT;
810
+ return known[name] ?? printable(name);
811
+ }
812
+ function pageLoadPartText(name) {
813
+ const known = PAGE_LOAD_PART_TEXT;
814
+ return known[name] ?? printable(name);
815
+ }
816
+ var ATTRIBUTED = /* @__PURE__ */ new Set([
817
+ "lcp",
818
+ "inp",
819
+ "cls"
820
+ ]);
821
+ var DEPLOYMENT_ROWS = 10;
822
+ var SUBJECT_LENGTH = 50;
823
+ function formatMs(ms) {
824
+ const rounded = Math.round(ms);
825
+ return rounded < 1e3 ? `${String(rounded)} ms` : `${(rounded / 1e3).toFixed(2)} s`;
826
+ }
827
+ function formatValue(metric, value) {
828
+ return metric === "cls" ? value.toFixed(2) : formatMs(value);
829
+ }
830
+ function formatChange(metric, change2) {
831
+ const size = formatValue(metric, Math.abs(change2));
832
+ if (Number.parseFloat(size) === 0) return "\xB10";
833
+ return change2 > 0 ? `\u25B2 +${size}` : `\u25BC \u2212${size}`;
834
+ }
835
+ function formatCount(count2) {
836
+ return Math.round(count2).toLocaleString("en-US");
837
+ }
838
+ function percent(part, total) {
839
+ return `${String(Math.round(part / total * 100))}%`;
840
+ }
841
+ function label(metric) {
842
+ return metric.toUpperCase();
843
+ }
844
+ function environmentSuffix(environment) {
845
+ return environment === "preview" ? ", preview" : "";
846
+ }
847
+ function environmentWord(environment) {
848
+ return environment === "preview" ? "preview " : "";
849
+ }
850
+ function table(rows) {
851
+ const widths = [];
852
+ for (const row of rows)
853
+ row.forEach((cell, i) => {
854
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
855
+ });
856
+ return rows.map(
857
+ (row) => ` ${row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ")}`.trimEnd()
858
+ );
859
+ }
860
+ function summaryLines(report) {
861
+ return table([
862
+ ["Metric", "p75", "Good", "Needs improvement", "Poor", "Visits"],
863
+ ...SPEED_INSIGHTS_METRICS.map((metric) => {
864
+ const summary = report.summary[metric];
865
+ if (summary.p75 === null || summary.samples <= 0)
866
+ return [label(metric), "\u2014", "\u2014", "\u2014", "\u2014", "0"];
867
+ return [
868
+ label(metric),
869
+ formatValue(metric, summary.p75),
870
+ percent(summary.good, summary.samples),
871
+ percent(summary.needsImprovement, summary.samples),
872
+ percent(summary.poor, summary.samples),
873
+ formatCount(summary.samples)
874
+ ];
875
+ })
876
+ ]);
877
+ }
878
+ function pageLoadLine(pageLoad) {
879
+ if (!pageLoad || pageLoad.samples <= 0 || pageLoad.p75 === null) return null;
880
+ const head = `Page load p75 ${formatMs(pageLoad.p75)}`;
881
+ if (pageLoad.parts.length === 0) return head;
882
+ const steps = pageLoad.parts.map(
883
+ (part) => `${pageLoadPartText(part.name)} ${formatMs(part.ms)}`
884
+ );
885
+ return `${head} \xB7 mean per step: ${steps.join(" \xB7 ")}`;
886
+ }
887
+ function breakdownTable(heading, metric, rows) {
888
+ if (rows.length === 0) return [" (none)"];
889
+ return table([
890
+ ["p75", "Visits", heading],
891
+ ...rows.map((row) => [
892
+ formatValue(metric, row.p75),
893
+ formatCount(row.samples),
894
+ printable(row.name)
895
+ ])
896
+ ]);
897
+ }
898
+ function elementLines(metric, elements) {
899
+ const lines = ["", `${label(metric)} elements behind slow visits`];
900
+ if (elements.length === 0) return [...lines, " (none)"];
901
+ for (const row of elements) {
902
+ lines.push(
903
+ ` ${printable(row.element)} on ${printable(row.route)}: ${formatCount(row.slow)} slow visits, average ${formatValue(metric, row.average)}`
904
+ );
905
+ if (row.parts && row.parts.length > 0)
906
+ lines.push(
907
+ ` ${row.parts.map((part) => `${partText(part.name)} ${formatMs(part.ms)}`).join(" \xB7 ")}`
908
+ );
909
+ if (row.url) lines.push(` image ${printable(row.url)}`);
910
+ }
911
+ return lines;
912
+ }
913
+ function deploymentName(row) {
914
+ if (row.source === "cli" || !row.commitSha) return row.id.slice(0, 8);
915
+ const sha = printable(row.commitSha.slice(0, 7));
916
+ const subject = [
917
+ ...printable(row.commitMessage?.split("\n")[0]?.trim() ?? "")
918
+ ];
919
+ if (subject.length === 0) return sha;
920
+ return subject.length > SUBJECT_LENGTH ? `${sha} ${subject.slice(0, SUBJECT_LENGTH - 1).join("")}\u2026` : `${sha} ${subject.join("")}`;
921
+ }
922
+ function changeText(metric, row) {
923
+ if (row.samples < SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES)
924
+ return "too few visits";
925
+ if (row.change === null) return "\u2013";
926
+ return formatChange(metric, row.change);
927
+ }
928
+ function deploymentLines(metric, deployments, environment) {
929
+ if (deployments === null) return [];
930
+ const lines = [
931
+ "",
932
+ `${label(metric)} by ${environmentWord(environment)}deployment, newest first`
933
+ ];
934
+ const measured = deployments.filter((row) => row.samples > 0).slice(0, DEPLOYMENT_ROWS);
935
+ if (measured.length === 0) return [...lines, " (none)"];
936
+ return [
937
+ ...lines,
938
+ ...table([
939
+ ["", "p75", "Visits", "Change", "Deployment"],
940
+ ...measured.map((row) => [
941
+ row.liveUntil === null ? "Live" : "",
942
+ row.p75 === null ? "\u2014" : formatValue(metric, row.p75),
943
+ formatCount(row.samples),
944
+ changeText(metric, row),
945
+ deploymentName(row)
946
+ ])
947
+ ])
948
+ ];
949
+ }
950
+ function reportLines(report, deployments, filters = {}) {
951
+ if (!report.enabled) return [OFF_LINE];
952
+ const { metric, range, environment } = report;
953
+ const filtered = filteredBy(filters);
954
+ const head = `Speed Insights \u2014 ${label(metric)}, ${rangeText(range)}${environmentSuffix(environment)}${filtered}`;
955
+ if (SPEED_INSIGHTS_METRICS.every((m) => report.summary[m].samples <= 0))
956
+ return [
957
+ head,
958
+ "",
959
+ // The dashboard's noFilteredSamples: waiting is the wrong advice when a
960
+ // filter is what emptied the report.
961
+ filtered === "" ? `No ${environmentWord(environment)}measurements in the ${rangeText(range)} yet. Values arrive when visitors leave a page.` : "No measurements match these filters. Drop a filter to see more."
962
+ ];
963
+ const pageLoad = pageLoadLine(report.pageLoad);
964
+ return [
965
+ head,
966
+ "",
967
+ ...summaryLines(report),
968
+ ...pageLoad === null ? [] : ["", pageLoad],
969
+ "",
970
+ `${label(metric)} by page`,
971
+ ...breakdownTable("Page", metric, report.paths),
972
+ "",
973
+ `${label(metric)} by device`,
974
+ ...breakdownTable("Device", metric, report.devices),
975
+ ...ATTRIBUTED.has(metric) ? elementLines(metric, report.elements) : [],
976
+ ...deploymentLines(metric, deployments, environment)
977
+ ];
978
+ }
979
+ function routePathsLines(result, filters = {}) {
980
+ return [
981
+ `${label(result.metric)} p75 under ${printable(result.route)}, ${rangeText(result.range)}${environmentSuffix(result.environment)}${filteredBy(filters)}`,
982
+ ...breakdownTable("Path", result.metric, result.paths)
983
+ ];
984
+ }
985
+
986
+ // src/commands/analytics-format.ts
987
+ var TABLES = [
988
+ { key: "pages", title: "Pages", column: "Page" },
989
+ { key: "referrers", title: "Referrers", column: "Referrer" },
990
+ { key: "countries", title: "Countries", column: "Country" },
991
+ { key: "devices", title: "Devices", column: "Device" },
992
+ { key: "browsers", title: "Browsers", column: "Browser" },
993
+ { key: "os", title: "Operating systems", column: "Operating system" }
994
+ ];
995
+ function breakdownLines({ title, column }, rows) {
996
+ if (rows.length === 0) return [title, " No data yet."];
997
+ return [
998
+ title,
999
+ ...table([
1000
+ ["Page views", column],
1001
+ // The name last: free text is never padded against anything after it.
1002
+ ...rows.map((row) => [formatCount(row.views), printable(row.name)])
1003
+ ])
1004
+ ];
1005
+ }
1006
+ function analyticsLines(report, range, filters) {
1007
+ const filtered = filteredBy(filters);
1008
+ const head = `Visitors ${formatCount(report.totals.visitors)} \xB7 Page views ${formatCount(report.totals.views)} \xB7 last ${printable(range)}${filtered}`;
1009
+ if (filtered !== "" && report.totals.views === 0)
1010
+ return [
1011
+ head,
1012
+ "",
1013
+ "No visits match these filters. Drop a filter to see more."
1014
+ ];
1015
+ return [
1016
+ head,
1017
+ ...TABLES.flatMap((spec) => [
1018
+ "",
1019
+ ...breakdownLines(spec, report[spec.key])
1020
+ ])
1021
+ ];
1022
+ }
1023
+
1024
+ // src/commands/analytics.ts
1025
+ function queryOf(flags) {
1026
+ const filters = filtersOf(flags, ANALYTICS_FILTER_KEYS);
1027
+ return {
1028
+ ...flags.range !== void 0 ? { range: flags.range } : {},
1029
+ ...Object.keys(filters).length > 0 ? { filters } : {}
1030
+ };
1031
+ }
1032
+ async function runAnalytics(context) {
1033
+ if (context.flags.env !== void 0)
1034
+ throw new NaroError(
1035
+ "VALIDATION_FAILED",
1036
+ "Analytics reads the production worker only; drop --env."
1037
+ );
1038
+ const client = await context.client();
1039
+ const projectId = await context.resolveProjectId();
1040
+ const query = queryOf(context.flags);
1041
+ const report = await client.analytics.report(projectId, query);
1042
+ context.output.result(
1043
+ { projectId, ...report },
1044
+ analyticsLines(report, report.range, query.filters ?? {})
1045
+ );
1046
+ }
1047
+
1048
+ // src/commands/auth.ts
1049
+ async function runLogin(context) {
1050
+ const { output } = context;
1051
+ output.log("Opening your browser to sign in with GitHub...");
1052
+ const session = await login({
1053
+ openBrowser: (url) => {
1054
+ output.log(`If your browser did not open, visit:
1055
+ ${url}`);
1056
+ openBrowser(url);
1057
+ }
1058
+ });
1059
+ saveSession(session);
1060
+ output.result(
1061
+ {
1062
+ user: {
1063
+ id: session.user.id,
1064
+ email: session.user.email,
1065
+ github: session.user.github
1066
+ }
1067
+ },
1068
+ [`\u2705 Logged in as ${session.user.email ?? session.user.id}`]
1069
+ );
1070
+ }
1071
+ async function runLogout(context) {
1072
+ const session = loadStoredSession();
1073
+ if (session) {
1074
+ try {
1075
+ const client = createAuthClient();
1076
+ await client.auth.setSession({
1077
+ access_token: session.access_token,
1078
+ refresh_token: session.refresh_token
1079
+ });
1080
+ await client.auth.signOut();
1081
+ } catch {
1082
+ }
1083
+ }
1084
+ clearSession();
1085
+ context.output.result({ loggedOut: true }, ["Logged out."]);
1086
+ }
1087
+ async function currentSession() {
1088
+ let session = loadStoredSession();
1089
+ if (!session)
1090
+ throw new NaroError("NOT_LOGGED_IN", "Not logged in. Run `naro login`.");
1091
+ if (isExpired(session)) {
1092
+ try {
1093
+ session = await refreshStoredSession(session.refresh_token);
1094
+ saveSession(session);
1095
+ } catch {
1096
+ throw new NaroError(
1097
+ "NOT_LOGGED_IN",
1098
+ "Session expired. Run `naro login`."
1099
+ );
1100
+ }
806
1101
  }
807
- context.output.log(`Pending migrations (${String(pending.length)}):`);
808
- for (const name of pending) context.output.log(` ${printable(name)}`);
809
- await confirmOrAbort(
810
- context,
811
- `Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
1102
+ return session;
1103
+ }
1104
+ async function runWhoami(context) {
1105
+ const session = await currentSession();
1106
+ const handle = session.user.github ? ` (${session.user.github})` : "";
1107
+ context.output.result(
1108
+ {
1109
+ user: {
1110
+ id: session.user.id,
1111
+ email: session.user.email,
1112
+ github: session.user.github
1113
+ }
1114
+ },
1115
+ [`Logged in as ${session.user.email ?? session.user.id}${handle}`]
812
1116
  );
813
- const { applied, warnings } = await applyMigrationFiles(
814
- context,
815
- client,
816
- projectId,
817
- dir,
818
- pending
1117
+ }
1118
+ async function runToken(context) {
1119
+ const session = await currentSession();
1120
+ process.stderr.write(
1121
+ "\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
1122
+ );
1123
+ context.output.result(
1124
+ { token: session.access_token, expiresAt: session.expires_at },
1125
+ [session.access_token]
819
1126
  );
820
- context.output.result({ projectId, applied, pending: [], warnings }, [
821
- `\u2705 Applied ${String(applied.length)} migration(s).`
822
- ]);
823
1127
  }
824
1128
 
825
1129
  // src/commands/backend.ts
@@ -1455,7 +1759,7 @@ async function allUsers(client, projectId) {
1455
1759
  function orDash(value) {
1456
1760
  return value === null ? "-" : printable(value);
1457
1761
  }
1458
- function label(user) {
1762
+ function label2(user) {
1459
1763
  return printable(user.username ?? user.email ?? user.id);
1460
1764
  }
1461
1765
  function userTable(users) {
@@ -1537,7 +1841,7 @@ async function runBackendUsersCreate(context, input = processStdin3) {
1537
1841
  );
1538
1842
  const signIn = user.username === null ? "naro.auth.signIn({ email, password })" : `naro.auth.signIn({ username: ${JSON.stringify(printable(user.username))}, password })`;
1539
1843
  context.output.result({ projectId, user }, [
1540
- `\u2705 Created user ${label(user)}. It signs in at once with ${signIn}.`,
1844
+ `\u2705 Created user ${label2(user)}. It signs in at once with ${signIn}.`,
1541
1845
  ...userLines(user)
1542
1846
  ]);
1543
1847
  }
@@ -1562,7 +1866,7 @@ async function runBackendUsersDelete(context) {
1562
1866
  "USER_NOT_FOUND",
1563
1867
  `No user has the id, username or email ${printable(given)}. \`naro backend users\` lists them.`
1564
1868
  );
1565
- const named = user.id === label(user) ? label(user) : `${label(user)} (${printable(user.id)})`;
1869
+ const named = user.id === label2(user) ? label2(user) : `${label2(user)} (${printable(user.id)})`;
1566
1870
  await confirmOrAbort(
1567
1871
  context,
1568
1872
  `Delete the user ${named}? Their sessions end at once and the account cannot be brought back.`
@@ -4583,206 +4887,35 @@ async function runGenTypes(context) {
4583
4887
  ]);
4584
4888
  }
4585
4889
 
4586
- // src/commands/insights-format.ts
4587
- var OFF_LINE = "Speed Insights is off for this project. Turn it on: naro insights enable";
4588
- var TURNED_ON = "Speed Insights is on. Measuring starts within about a minute; values arrive when visitors leave a page.";
4589
- var TURNED_OFF = "Speed Insights is off. Measuring stops within about a minute; turning it back on shows past measurements again.";
4590
- var RANGE_TEXT = {
4591
- "24h": "last 24 hours",
4592
- "7d": "last 7 days",
4593
- "30d": "last 30 days"
4594
- };
4595
- var PART_TEXT = {
4596
- ttfb: "TTFB",
4597
- loadDelay: "load delay",
4598
- loadDuration: "load duration",
4599
- renderDelay: "render delay",
4600
- inputDelay: "input delay",
4601
- processing: "processing",
4602
- presentation: "presentation"
4603
- };
4604
- function rangeText(range) {
4605
- const known = RANGE_TEXT;
4606
- return known[range] ?? printable(range);
4607
- }
4608
- function partText(name) {
4609
- const known = PART_TEXT;
4610
- return known[name] ?? printable(name);
4611
- }
4612
- var ATTRIBUTED = /* @__PURE__ */ new Set([
4613
- "lcp",
4614
- "inp",
4615
- "cls"
4616
- ]);
4617
- var DEPLOYMENT_ROWS = 10;
4618
- var SUBJECT_LENGTH = 50;
4619
- function formatMs(ms) {
4620
- const rounded = Math.round(ms);
4621
- return rounded < 1e3 ? `${String(rounded)} ms` : `${(rounded / 1e3).toFixed(2)} s`;
4622
- }
4623
- function formatValue(metric, value) {
4624
- return metric === "cls" ? value.toFixed(2) : formatMs(value);
4625
- }
4626
- function formatChange(metric, change2) {
4627
- const size = formatValue(metric, Math.abs(change2));
4628
- if (Number.parseFloat(size) === 0) return "\xB10";
4629
- return change2 > 0 ? `\u25B2 +${size}` : `\u25BC \u2212${size}`;
4630
- }
4631
- function formatCount(count2) {
4632
- return Math.round(count2).toLocaleString("en-US");
4633
- }
4634
- function percent(part, total) {
4635
- return `${String(Math.round(part / total * 100))}%`;
4636
- }
4637
- function label2(metric) {
4638
- return metric.toUpperCase();
4639
- }
4640
- function environmentSuffix(environment) {
4641
- return environment === "preview" ? ", preview" : "";
4642
- }
4643
- function environmentWord(environment) {
4644
- return environment === "preview" ? "preview " : "";
4645
- }
4646
- function table(rows) {
4647
- const widths = [];
4648
- for (const row of rows)
4649
- row.forEach((cell, i) => {
4650
- widths[i] = Math.max(widths[i] ?? 0, cell.length);
4651
- });
4652
- return rows.map(
4653
- (row) => ` ${row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ")}`.trimEnd()
4654
- );
4655
- }
4656
- function summaryLines(report) {
4657
- return table([
4658
- ["Metric", "p75", "Good", "Needs improvement", "Poor", "Visits"],
4659
- ...SPEED_INSIGHTS_METRICS.map((metric) => {
4660
- const summary = report.summary[metric];
4661
- if (summary.p75 === null || summary.samples <= 0)
4662
- return [label2(metric), "\u2014", "\u2014", "\u2014", "\u2014", "0"];
4663
- return [
4664
- label2(metric),
4665
- formatValue(metric, summary.p75),
4666
- percent(summary.good, summary.samples),
4667
- percent(summary.needsImprovement, summary.samples),
4668
- percent(summary.poor, summary.samples),
4669
- formatCount(summary.samples)
4670
- ];
4671
- })
4672
- ]);
4673
- }
4674
- function breakdownTable(heading, metric, rows) {
4675
- if (rows.length === 0) return [" (none)"];
4676
- return table([
4677
- ["p75", "Visits", heading],
4678
- ...rows.map((row) => [
4679
- formatValue(metric, row.p75),
4680
- formatCount(row.samples),
4681
- printable(row.name)
4682
- ])
4683
- ]);
4684
- }
4685
- function elementLines(metric, elements) {
4686
- const lines = ["", `${label2(metric)} elements behind slow visits`];
4687
- if (elements.length === 0) return [...lines, " (none)"];
4688
- for (const row of elements) {
4689
- lines.push(
4690
- ` ${printable(row.element)} on ${printable(row.route)}: ${formatCount(row.slow)} slow visits, average ${formatValue(metric, row.average)}`
4691
- );
4692
- if (row.parts && row.parts.length > 0)
4693
- lines.push(
4694
- ` ${row.parts.map((part) => `${partText(part.name)} ${formatMs(part.ms)}`).join(" \xB7 ")}`
4695
- );
4696
- if (row.url) lines.push(` image ${printable(row.url)}`);
4697
- }
4698
- return lines;
4699
- }
4700
- function deploymentName(row) {
4701
- if (row.source === "cli" || !row.commitSha) return row.id.slice(0, 8);
4702
- const sha = printable(row.commitSha.slice(0, 7));
4703
- const subject = [
4704
- ...printable(row.commitMessage?.split("\n")[0]?.trim() ?? "")
4705
- ];
4706
- if (subject.length === 0) return sha;
4707
- return subject.length > SUBJECT_LENGTH ? `${sha} ${subject.slice(0, SUBJECT_LENGTH - 1).join("")}\u2026` : `${sha} ${subject.join("")}`;
4708
- }
4709
- function changeText(metric, row) {
4710
- if (row.samples < SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES)
4711
- return "too few visits";
4712
- if (row.change === null) return "\u2013";
4713
- return formatChange(metric, row.change);
4714
- }
4715
- function deploymentLines(metric, deployments, environment) {
4716
- if (deployments === null) return [];
4717
- const lines = [
4718
- "",
4719
- `${label2(metric)} by ${environmentWord(environment)}deployment, newest first`
4720
- ];
4721
- const measured = deployments.filter((row) => row.samples > 0).slice(0, DEPLOYMENT_ROWS);
4722
- if (measured.length === 0) return [...lines, " (none)"];
4723
- return [
4724
- ...lines,
4725
- ...table([
4726
- ["", "p75", "Visits", "Change", "Deployment"],
4727
- ...measured.map((row) => [
4728
- row.liveUntil === null ? "Live" : "",
4729
- row.p75 === null ? "\u2014" : formatValue(metric, row.p75),
4730
- formatCount(row.samples),
4731
- changeText(metric, row),
4732
- deploymentName(row)
4733
- ])
4734
- ])
4735
- ];
4736
- }
4737
- function reportLines(report, deployments) {
4738
- if (!report.enabled) return [OFF_LINE];
4739
- const { metric, range, environment } = report;
4740
- const head = `Speed Insights \u2014 ${label2(metric)}, ${rangeText(range)}${environmentSuffix(environment)}`;
4741
- if (SPEED_INSIGHTS_METRICS.every((m) => report.summary[m].samples <= 0))
4742
- return [
4743
- head,
4744
- "",
4745
- `No ${environmentWord(environment)}measurements in the ${rangeText(range)} yet. Values arrive when visitors leave a page.`
4746
- ];
4747
- return [
4748
- head,
4749
- "",
4750
- ...summaryLines(report),
4751
- "",
4752
- `${label2(metric)} by page`,
4753
- ...breakdownTable("Page", metric, report.paths),
4754
- "",
4755
- `${label2(metric)} by device`,
4756
- ...breakdownTable("Device", metric, report.devices),
4757
- ...ATTRIBUTED.has(metric) ? elementLines(metric, report.elements) : [],
4758
- ...deploymentLines(metric, deployments, environment)
4759
- ];
4760
- }
4761
- function routePathsLines(result) {
4762
- return [
4763
- `${label2(result.metric)} p75 under ${printable(result.route)}, ${rangeText(result.range)}${environmentSuffix(result.environment)}`,
4764
- ...breakdownTable("Path", result.metric, result.paths)
4765
- ];
4766
- }
4767
-
4768
4890
  // src/commands/insights.ts
4769
- function queryOf(flags) {
4891
+ function queryOf2(flags) {
4892
+ const filters = filtersOf(flags, SPEED_INSIGHTS_FILTER_KEYS);
4770
4893
  return {
4771
4894
  ...flags.range !== void 0 ? { range: flags.range } : {},
4772
4895
  ...flags.metric !== void 0 ? { metric: flags.metric } : {},
4773
- ...flags.env !== void 0 ? { environment: flags.env } : {}
4896
+ ...flags.env !== void 0 ? { environment: flags.env } : {},
4897
+ ...Object.keys(filters).length > 0 ? { filters } : {}
4774
4898
  };
4775
4899
  }
4776
4900
  async function runInsights(context) {
4901
+ if (context.flags.referrer !== void 0 && context.flags.referrer !== "")
4902
+ throw new NaroError(
4903
+ "VALIDATION_FAILED",
4904
+ "Speed Insights has no referrer; drop --referrer."
4905
+ );
4777
4906
  const client = await context.client();
4778
4907
  const projectId = await context.resolveProjectId();
4779
- const query = queryOf(context.flags);
4908
+ const query = queryOf2(context.flags);
4909
+ const filters = query.filters ?? {};
4780
4910
  if (context.flags.route !== void 0) {
4781
4911
  const result = await client.insights.paths(projectId, {
4782
4912
  ...query,
4783
4913
  route: context.flags.route
4784
4914
  });
4785
- context.output.result({ projectId, ...result }, routePathsLines(result));
4915
+ context.output.result(
4916
+ { projectId, ...result },
4917
+ routePathsLines(result, filters)
4918
+ );
4786
4919
  return;
4787
4920
  }
4788
4921
  const overview = await client.insights.overview(projectId, query);
@@ -4792,7 +4925,7 @@ async function runInsights(context) {
4792
4925
  );
4793
4926
  context.output.result(
4794
4927
  { projectId, ...overview },
4795
- reportLines(overview.report, overview.deployments)
4928
+ reportLines(overview.report, overview.deployments, filters)
4796
4929
  );
4797
4930
  }
4798
4931
  async function setInsights(context, enabled) {
@@ -4801,6 +4934,12 @@ async function setInsights(context, enabled) {
4801
4934
  "VALIDATION_FAILED",
4802
4935
  "Speed Insights has one switch for production and preview; drop --env."
4803
4936
  );
4937
+ const filter = givenFilterFlag(context.flags);
4938
+ if (filter !== void 0)
4939
+ throw new NaroError(
4940
+ "VALIDATION_FAILED",
4941
+ `Filters only narrow a report; drop --${filter}.`
4942
+ );
4804
4943
  const client = await context.client();
4805
4944
  const projectId = await context.resolveProjectId();
4806
4945
  const result = await client.insights.setEnabled(projectId, enabled);
@@ -8061,9 +8200,13 @@ Usage:
8061
8200
  naro logs [deployment-id] Runtime logs (or build logs of a deployment)
8062
8201
  [--type build|runtime] [--env production|preview] [--level error] [--follow]
8063
8202
 
8064
- naro insights [--metric lcp] [--range 7d] [--env preview]
8203
+ naro analytics [--range 7d] [--path p] [--referrer host] [--country KR] [--device mobile] [--browser safari] [--os ios]
8204
+ Web Analytics: visitors and page views, then the top pages,
8205
+ referrers, countries, devices, browsers and operating systems
8206
+ naro insights [--metric lcp] [--range 7d] [--env preview] [--path p] [--country KR] [--device mobile] [--browser safari] [--os ios]
8065
8207
  Speed Insights: Web Vitals p75 by page, device, the elements
8066
- behind slow visits and deployment (production by default)
8208
+ behind slow visits and deployment, and the page load: its p75
8209
+ and the mean of each step (production by default)
8067
8210
  naro insights --route '/posts/[id]' The paths opened under one route
8068
8211
  naro insights enable | disable Turn measuring on or off (production pages get a
8069
8212
  same-origin script, preview pages too; past measurements are kept)
@@ -8221,8 +8364,14 @@ Global options:
8221
8364
  --prod Deploy/target production
8222
8365
  --wait Poll until the domain goes live (naro domains verify)
8223
8366
  --metric <m> lcp, inp, cls, fcp or ttfb; lcp by default (naro insights)
8224
- --range <r> 24h, 7d or 30d; 7d by default (naro insights)
8367
+ --range <r> 24h, 7d or 30d; 7d by default (naro analytics, naro insights)
8225
8368
  --route <pattern> A route from the pages table, e.g. '/posts/[id]': the paths under it (naro insights)
8369
+ --path <p> Only this page, starting with /, e.g. /pricing (naro analytics, naro insights)
8370
+ --referrer <host> Only visits that came from this lowercase host, e.g. t.co (naro analytics only: Speed Insights has no referrer)
8371
+ --country <cc> Only this country, an uppercase two-letter code like KR (naro analytics, naro insights)
8372
+ --device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
8373
+ --browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
8374
+ --os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
8226
8375
  --env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
8227
8376
  --no-www Register the apex alone (naro domains add)
8228
8377
  --file <path> SQL file to run (naro db query, naro policy create)
@@ -8438,6 +8587,9 @@ function route(parsed) {
8438
8587
  return { handler: runCancel, positionals };
8439
8588
  case "logs":
8440
8589
  return { handler: runLogs, positionals };
8590
+ // Spec 2026-10-08-analytics-cli-pageload §5.
8591
+ case "analytics":
8592
+ return positionals.length === 0 ? { handler: runAnalytics, positionals: [] } : null;
8441
8593
  // Spec 2026-10-08 §5.
8442
8594
  case "insights": {
8443
8595
  if (positionals.length > 1) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -34,11 +34,11 @@
34
34
  "typescript": "7.0.2",
35
35
  "vitest": "3.2.7",
36
36
  "naro-js": "0.3.0",
37
- "@next-deploy/data-core": "0.1.0",
38
37
  "@next-deploy/build": "0.1.0",
39
38
  "@next-deploy/config": "0.1.0",
40
39
  "@next-deploy/core": "0.1.0",
41
- "@next-deploy/sdk": "0.1.0"
40
+ "@next-deploy/sdk": "0.1.0",
41
+ "@next-deploy/data-core": "0.1.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup && pnpm build:types",