@ainyc/canonry 1.41.0 → 1.45.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/assets/assets/index--ev1Bjls.css +1 -0
- package/assets/assets/index-BsF7MVAu.js +281 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-TKMBOLZB.js → chunk-B4EP44AR.js} +490 -49
- package/dist/{chunk-EUBC5EGC.js → chunk-SVPQUYTG.js} +59 -1
- package/dist/cli.js +363 -27
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-DXGTIRF5.js → intelligence-service-TXWOESFH.js} +1 -1
- package/package.json +5 -5
- package/assets/assets/index-Djm1st6N.css +0 -1
- package/assets/assets/index-iy29Cmx8.js +0 -281
|
@@ -19,11 +19,13 @@ __export(schema_exports, {
|
|
|
19
19
|
apiKeys: () => apiKeys,
|
|
20
20
|
auditLog: () => auditLog,
|
|
21
21
|
bingConnections: () => bingConnections,
|
|
22
|
+
bingCoverageSnapshots: () => bingCoverageSnapshots,
|
|
22
23
|
bingKeywordStats: () => bingKeywordStats,
|
|
23
24
|
bingUrlInspections: () => bingUrlInspections,
|
|
24
25
|
competitors: () => competitors,
|
|
25
26
|
gaAiReferrals: () => gaAiReferrals,
|
|
26
27
|
gaConnections: () => gaConnections,
|
|
28
|
+
gaSocialReferrals: () => gaSocialReferrals,
|
|
27
29
|
gaTrafficSnapshots: () => gaTrafficSnapshots,
|
|
28
30
|
gaTrafficSummaries: () => gaTrafficSummaries,
|
|
29
31
|
googleConnections: () => googleConnections,
|
|
@@ -237,6 +239,17 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
|
|
|
237
239
|
index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
|
|
238
240
|
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
239
241
|
]);
|
|
242
|
+
var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
|
|
243
|
+
id: text("id").primaryKey(),
|
|
244
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
245
|
+
date: text("date").notNull(),
|
|
246
|
+
indexed: integer("indexed").notNull().default(0),
|
|
247
|
+
notIndexed: integer("not_indexed").notNull().default(0),
|
|
248
|
+
unknown: integer("unknown").notNull().default(0),
|
|
249
|
+
createdAt: text("created_at").notNull()
|
|
250
|
+
}, (table) => [
|
|
251
|
+
uniqueIndex("idx_bing_coverage_snap_project_date").on(table.projectId, table.date)
|
|
252
|
+
]);
|
|
240
253
|
var bingConnections = sqliteTable("bing_connections", {
|
|
241
254
|
id: text("id").primaryKey(),
|
|
242
255
|
domain: text("domain").notNull(),
|
|
@@ -318,6 +331,22 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
|
|
|
318
331
|
index("idx_ga_ai_ref_source").on(table.source),
|
|
319
332
|
uniqueIndex("idx_ga_ai_ref_unique_v2").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension)
|
|
320
333
|
]);
|
|
334
|
+
var gaSocialReferrals = sqliteTable("ga_social_referrals", {
|
|
335
|
+
id: text("id").primaryKey(),
|
|
336
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
337
|
+
date: text("date").notNull(),
|
|
338
|
+
source: text("source").notNull(),
|
|
339
|
+
medium: text("medium").notNull(),
|
|
340
|
+
/** GA4 default channel group (e.g. 'Organic Social', 'Paid Social') */
|
|
341
|
+
channelGroup: text("channel_group").notNull().default("Organic Social"),
|
|
342
|
+
sessions: integer("sessions").notNull().default(0),
|
|
343
|
+
users: integer("users").notNull().default(0),
|
|
344
|
+
syncedAt: text("synced_at").notNull()
|
|
345
|
+
}, (table) => [
|
|
346
|
+
index("idx_ga_social_ref_project_date").on(table.projectId, table.date),
|
|
347
|
+
index("idx_ga_social_ref_source").on(table.source),
|
|
348
|
+
uniqueIndex("idx_ga_social_ref_unique").on(table.projectId, table.date, table.source, table.medium, table.channelGroup)
|
|
349
|
+
]);
|
|
321
350
|
var gaTrafficSummaries = sqliteTable("ga_traffic_summaries", {
|
|
322
351
|
id: text("id").primaryKey(),
|
|
323
352
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -762,7 +791,34 @@ var MIGRATIONS = [
|
|
|
762
791
|
`ALTER TABLE insights ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
763
792
|
`CREATE INDEX IF NOT EXISTS idx_insights_run ON insights(run_id)`,
|
|
764
793
|
`ALTER TABLE health_snapshots ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
|
|
765
|
-
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)
|
|
794
|
+
`CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)`,
|
|
795
|
+
// v25: Social media referral tracking — ga_social_referrals table
|
|
796
|
+
// Uses GA4's native sessionDefaultChannelGroup for social classification
|
|
797
|
+
`CREATE TABLE IF NOT EXISTS ga_social_referrals (
|
|
798
|
+
id TEXT PRIMARY KEY,
|
|
799
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
800
|
+
date TEXT NOT NULL,
|
|
801
|
+
source TEXT NOT NULL,
|
|
802
|
+
medium TEXT NOT NULL,
|
|
803
|
+
channel_group TEXT NOT NULL DEFAULT 'Organic Social',
|
|
804
|
+
sessions INTEGER NOT NULL DEFAULT 0,
|
|
805
|
+
users INTEGER NOT NULL DEFAULT 0,
|
|
806
|
+
synced_at TEXT NOT NULL
|
|
807
|
+
)`,
|
|
808
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_social_ref_project_date ON ga_social_referrals(project_id, date)`,
|
|
809
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_social_ref_source ON ga_social_referrals(source)`,
|
|
810
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_social_ref_unique ON ga_social_referrals(project_id, date, source, medium, channel_group)`,
|
|
811
|
+
// v26: Bing coverage snapshots for historical tracking (mirrors gsc_coverage_snapshots)
|
|
812
|
+
`CREATE TABLE IF NOT EXISTS bing_coverage_snapshots (
|
|
813
|
+
id TEXT PRIMARY KEY,
|
|
814
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
815
|
+
date TEXT NOT NULL,
|
|
816
|
+
indexed INTEGER NOT NULL DEFAULT 0,
|
|
817
|
+
not_indexed INTEGER NOT NULL DEFAULT 0,
|
|
818
|
+
unknown INTEGER NOT NULL DEFAULT 0,
|
|
819
|
+
created_at TEXT NOT NULL
|
|
820
|
+
)`,
|
|
821
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_coverage_snap_project_date ON bing_coverage_snapshots(project_id, date)`
|
|
766
822
|
];
|
|
767
823
|
function isDuplicateColumnError(err) {
|
|
768
824
|
if (!(err instanceof Error)) return false;
|
|
@@ -1206,9 +1262,11 @@ export {
|
|
|
1206
1262
|
gscSearchData,
|
|
1207
1263
|
gscUrlInspections,
|
|
1208
1264
|
gscCoverageSnapshots,
|
|
1265
|
+
bingCoverageSnapshots,
|
|
1209
1266
|
bingUrlInspections,
|
|
1210
1267
|
gaTrafficSnapshots,
|
|
1211
1268
|
gaAiReferrals,
|
|
1269
|
+
gaSocialReferrals,
|
|
1212
1270
|
gaTrafficSummaries,
|
|
1213
1271
|
usageCounters,
|
|
1214
1272
|
insights,
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
setGoogleAuthConfig,
|
|
29
29
|
showFirstRunNotice,
|
|
30
30
|
trackEvent
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-B4EP44AR.js";
|
|
32
32
|
import {
|
|
33
33
|
apiKeys,
|
|
34
34
|
competitors,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
projects,
|
|
39
39
|
querySnapshots,
|
|
40
40
|
runs
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-SVPQUYTG.js";
|
|
42
42
|
|
|
43
43
|
// src/cli.ts
|
|
44
44
|
import { pathToFileURL } from "url";
|
|
@@ -355,7 +355,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
355
355
|
console.log(` Errors: ${providerErrors}`);
|
|
356
356
|
}
|
|
357
357
|
async function backfillInsightsCommand(project, opts) {
|
|
358
|
-
const { IntelligenceService } = await import("./intelligence-service-
|
|
358
|
+
const { IntelligenceService } = await import("./intelligence-service-TXWOESFH.js");
|
|
359
359
|
const config = loadConfig();
|
|
360
360
|
const db = createClient(config.database);
|
|
361
361
|
migrate(db);
|
|
@@ -619,6 +619,7 @@ var ApiClient = class {
|
|
|
619
619
|
const serializedBody = body != null ? JSON.stringify(body) : void 0;
|
|
620
620
|
const headers = {
|
|
621
621
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
622
|
+
"Accept": "application/json",
|
|
622
623
|
...serializedBody != null ? { "Content-Type": "application/json" } : {}
|
|
623
624
|
};
|
|
624
625
|
let res;
|
|
@@ -860,6 +861,10 @@ var ApiClient = class {
|
|
|
860
861
|
async bingCoverage(project) {
|
|
861
862
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage`);
|
|
862
863
|
}
|
|
864
|
+
async bingCoverageHistory(project, params) {
|
|
865
|
+
const qs = params?.limit != null ? `?limit=${params.limit}` : "";
|
|
866
|
+
return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage/history${qs}`);
|
|
867
|
+
}
|
|
863
868
|
async bingInspections(project, params) {
|
|
864
869
|
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
865
870
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/inspections${qs}`);
|
|
@@ -907,6 +912,15 @@ var ApiClient = class {
|
|
|
907
912
|
async gaAiReferralHistory(project) {
|
|
908
913
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/ai-referral-history`);
|
|
909
914
|
}
|
|
915
|
+
async gaSocialReferralHistory(project) {
|
|
916
|
+
return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-history`);
|
|
917
|
+
}
|
|
918
|
+
async gaSocialReferralTrend(project) {
|
|
919
|
+
return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-trend`);
|
|
920
|
+
}
|
|
921
|
+
async gaAttributionTrend(project) {
|
|
922
|
+
return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/attribution-trend`);
|
|
923
|
+
}
|
|
910
924
|
async gaSessionHistory(project) {
|
|
911
925
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/session-history`);
|
|
912
926
|
}
|
|
@@ -1143,6 +1157,26 @@ Bing Index Coverage for "${project}"
|
|
|
1143
1157
|
console.log(` Last inspected: ${result.lastInspectedAt}`);
|
|
1144
1158
|
}
|
|
1145
1159
|
}
|
|
1160
|
+
async function bingCoverageHistory(project, opts) {
|
|
1161
|
+
const client = getClient();
|
|
1162
|
+
const rows = await client.bingCoverageHistory(project, { limit: opts.limit });
|
|
1163
|
+
if (opts.format === "json") {
|
|
1164
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (rows.length === 0) {
|
|
1168
|
+
console.log('No coverage history found. Run "canonry bing coverage" or "canonry bing refresh" first to capture a snapshot.');
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
console.log(`
|
|
1172
|
+
Bing Coverage History for "${project}" (${rows.length} snapshots):
|
|
1173
|
+
`);
|
|
1174
|
+
console.log(` ${"DATE".padEnd(12)}${"INDEXED".padEnd(10)}${"NOT INDEXED".padEnd(14)}UNKNOWN`);
|
|
1175
|
+
console.log(` ${"\u2500".repeat(12)}${"\u2500".repeat(10)}${"\u2500".repeat(14)}${"\u2500".repeat(10)}`);
|
|
1176
|
+
for (const row of rows) {
|
|
1177
|
+
console.log(` ${row.date.padEnd(12)}${String(row.indexed).padEnd(10)}${String(row.notIndexed).padEnd(14)}${String(row.unknown)}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1146
1180
|
async function bingInspect(project, url, format) {
|
|
1147
1181
|
const client = getClient();
|
|
1148
1182
|
const result = await client.bingInspectUrl(project, url);
|
|
@@ -1343,6 +1377,22 @@ var BING_CLI_COMMANDS = [
|
|
|
1343
1377
|
await bingSetSite(project, siteUrl, input.format);
|
|
1344
1378
|
}
|
|
1345
1379
|
},
|
|
1380
|
+
{
|
|
1381
|
+
path: ["bing", "coverage-history"],
|
|
1382
|
+
usage: "canonry bing coverage-history <project> [--limit <n>] [--format json]",
|
|
1383
|
+
options: { limit: stringOption() },
|
|
1384
|
+
run: async (input) => {
|
|
1385
|
+
const project = requireProject(input, "bing.coverage-history", "canonry bing coverage-history <project> [--limit <n>] [--format json]");
|
|
1386
|
+
await bingCoverageHistory(project, {
|
|
1387
|
+
limit: parseIntegerOption(input, "limit", {
|
|
1388
|
+
command: "bing.coverage-history",
|
|
1389
|
+
message: "--limit must be a positive integer",
|
|
1390
|
+
usage: "canonry bing coverage-history <project> [--limit <n>] [--format json]"
|
|
1391
|
+
}),
|
|
1392
|
+
format: input.format
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1346
1396
|
{
|
|
1347
1397
|
path: ["bing", "coverage"],
|
|
1348
1398
|
usage: "canonry bing coverage <project> [--format json]",
|
|
@@ -1422,12 +1472,12 @@ var BING_CLI_COMMANDS = [
|
|
|
1422
1472
|
},
|
|
1423
1473
|
{
|
|
1424
1474
|
path: ["bing"],
|
|
1425
|
-
usage: "canonry bing <connect|disconnect|status|sites|set-site|coverage|inspect|inspections|request-indexing|performance|refresh> <project> [args]",
|
|
1475
|
+
usage: "canonry bing <connect|disconnect|status|sites|set-site|coverage|coverage-history|inspect|inspections|request-indexing|performance|refresh> <project> [args]",
|
|
1426
1476
|
run: async (input) => {
|
|
1427
1477
|
unknownSubcommand(input.positionals[0], {
|
|
1428
1478
|
command: "bing",
|
|
1429
|
-
usage: "canonry bing <connect|disconnect|status|sites|set-site|coverage|inspect|inspections|request-indexing|performance|refresh> <project> [args]",
|
|
1430
|
-
available: ["connect", "disconnect", "status", "sites", "set-site", "coverage", "inspect", "inspections", "request-indexing", "performance", "refresh"]
|
|
1479
|
+
usage: "canonry bing <connect|disconnect|status|sites|set-site|coverage|coverage-history|inspect|inspections|request-indexing|performance|refresh> <project> [args]",
|
|
1480
|
+
available: ["connect", "disconnect", "status", "sites", "set-site", "coverage", "coverage-history", "inspect", "inspections", "request-indexing", "performance", "refresh"]
|
|
1431
1481
|
});
|
|
1432
1482
|
}
|
|
1433
1483
|
}
|
|
@@ -1709,14 +1759,21 @@ async function gaStatus(project, format) {
|
|
|
1709
1759
|
}
|
|
1710
1760
|
async function gaSync(project, opts) {
|
|
1711
1761
|
const client = getClient3();
|
|
1712
|
-
const
|
|
1762
|
+
const body = {};
|
|
1763
|
+
if (opts?.days) body.days = opts.days;
|
|
1764
|
+
if (opts?.only) body.only = opts.only;
|
|
1765
|
+
const result = await client.gaSync(project, body);
|
|
1713
1766
|
if (opts?.format === "json") {
|
|
1714
1767
|
console.log(JSON.stringify(result, null, 2));
|
|
1715
1768
|
return;
|
|
1716
1769
|
}
|
|
1717
1770
|
console.log(`GA4 sync complete for "${project}".`);
|
|
1771
|
+
if (result.syncedComponents) {
|
|
1772
|
+
console.log(` Components: ${result.syncedComponents.join(", ")}`);
|
|
1773
|
+
}
|
|
1718
1774
|
console.log(` Page rows: ${result.rowCount}`);
|
|
1719
1775
|
console.log(` AI rows: ${result.aiReferralCount}`);
|
|
1776
|
+
console.log(` Social rows: ${result.socialReferralCount}`);
|
|
1720
1777
|
console.log(` Period: ${result.days} days`);
|
|
1721
1778
|
console.log(` Synced at: ${result.syncedAt}`);
|
|
1722
1779
|
}
|
|
@@ -1729,7 +1786,7 @@ async function gaTraffic(project, opts) {
|
|
|
1729
1786
|
console.log(JSON.stringify(result, null, 2));
|
|
1730
1787
|
return;
|
|
1731
1788
|
}
|
|
1732
|
-
if (result.topPages.length === 0 && result.aiReferrals.length === 0) {
|
|
1789
|
+
if (result.topPages.length === 0 && result.aiReferrals.length === 0 && result.socialReferrals.length === 0) {
|
|
1733
1790
|
console.log('No GA4 traffic data. Run "canonry ga sync <project>" first.');
|
|
1734
1791
|
return;
|
|
1735
1792
|
}
|
|
@@ -1756,6 +1813,23 @@ async function gaTraffic(project, opts) {
|
|
|
1756
1813
|
}
|
|
1757
1814
|
console.log();
|
|
1758
1815
|
}
|
|
1816
|
+
if (result.socialReferrals.length > 0) {
|
|
1817
|
+
const chanWidth = 12;
|
|
1818
|
+
if (result.socialSessions > 0) {
|
|
1819
|
+
const share = result.totalSessions > 0 ? Math.round(result.socialSessions / result.totalSessions * 100) : 0;
|
|
1820
|
+
console.log(` Social Sessions: ${result.socialSessions} (${share}% of total)`);
|
|
1821
|
+
}
|
|
1822
|
+
console.log(" SOCIAL REFERRAL SOURCES");
|
|
1823
|
+
console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
|
|
1824
|
+
console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
|
|
1825
|
+
for (const ref of result.socialReferrals) {
|
|
1826
|
+
const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
|
|
1827
|
+
console.log(
|
|
1828
|
+
` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${chanLabel.padEnd(chanWidth)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
console.log();
|
|
1832
|
+
}
|
|
1759
1833
|
if (result.topPages.length > 0) {
|
|
1760
1834
|
const pageWidth = Math.min(60, Math.max(15, ...result.topPages.map((r) => r.landingPage.length)));
|
|
1761
1835
|
console.log(` TOP LANDING PAGES`);
|
|
@@ -1798,6 +1872,31 @@ async function gaAiReferralHistory(project, format) {
|
|
|
1798
1872
|
);
|
|
1799
1873
|
}
|
|
1800
1874
|
}
|
|
1875
|
+
async function gaSocialReferralHistory(project, format) {
|
|
1876
|
+
const client = getClient3();
|
|
1877
|
+
const result = await client.gaSocialReferralHistory(project);
|
|
1878
|
+
if (format === "json") {
|
|
1879
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (result.length === 0) {
|
|
1883
|
+
console.log('No social referral history. Run "canonry ga sync <project>" first.');
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const dateWidth = 12;
|
|
1887
|
+
const sourceWidth = Math.min(30, Math.max(10, ...result.map((r) => r.source.length)));
|
|
1888
|
+
const chanWidth = 12;
|
|
1889
|
+
console.log(`GA4 Social Referral History for "${project}":
|
|
1890
|
+
`);
|
|
1891
|
+
console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
|
|
1892
|
+
console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
|
|
1893
|
+
for (const row of result) {
|
|
1894
|
+
const chanLabel = row.channelGroup === "Paid Social" ? "paid" : "organic";
|
|
1895
|
+
console.log(
|
|
1896
|
+
` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${chanLabel.padEnd(chanWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1801
1900
|
async function gaCoverage(project, format) {
|
|
1802
1901
|
const client = getClient3();
|
|
1803
1902
|
const result = await client.gaCoverage(project);
|
|
@@ -1821,6 +1920,182 @@ async function gaCoverage(project, format) {
|
|
|
1821
1920
|
);
|
|
1822
1921
|
}
|
|
1823
1922
|
}
|
|
1923
|
+
async function gaSocialReferralSummary(project, opts) {
|
|
1924
|
+
const client = getClient3();
|
|
1925
|
+
const traffic = await client.gaTraffic(project);
|
|
1926
|
+
if (opts?.trend) {
|
|
1927
|
+
const trend = await client.gaSocialReferralTrend(project);
|
|
1928
|
+
if (opts.format === "json") {
|
|
1929
|
+
console.log(JSON.stringify({
|
|
1930
|
+
socialSessions: traffic.socialSessions,
|
|
1931
|
+
socialUsers: traffic.socialUsers,
|
|
1932
|
+
totalSessions: traffic.totalSessions,
|
|
1933
|
+
socialSharePct: traffic.socialSharePct,
|
|
1934
|
+
topSources: traffic.socialReferrals.slice(0, 5).map((r) => ({ source: r.source, sessions: r.sessions, channel: r.channelGroup })),
|
|
1935
|
+
trend
|
|
1936
|
+
}, null, 2));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
console.log(`Social Traffic Summary for "${project}"
|
|
1940
|
+
`);
|
|
1941
|
+
console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
|
|
1942
|
+
console.log(` Users: ${traffic.socialUsers}`);
|
|
1943
|
+
console.log();
|
|
1944
|
+
const fmtTrend = (pct) => pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${pct}%`;
|
|
1945
|
+
console.log(` 7d trend: ${fmtTrend(trend.trend7dPct)} (${trend.socialSessions7d} vs ${trend.socialSessionsPrev7d})`);
|
|
1946
|
+
console.log(` 30d trend: ${fmtTrend(trend.trend30dPct)} (${trend.socialSessions30d} vs ${trend.socialSessionsPrev30d})`);
|
|
1947
|
+
if (trend.biggestMover) {
|
|
1948
|
+
const m = trend.biggestMover;
|
|
1949
|
+
console.log(` Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d})`);
|
|
1950
|
+
}
|
|
1951
|
+
console.log();
|
|
1952
|
+
if (traffic.socialReferrals.length > 0) {
|
|
1953
|
+
console.log(" TOP SOURCES");
|
|
1954
|
+
for (const ref of traffic.socialReferrals.slice(0, 5)) {
|
|
1955
|
+
const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
|
|
1956
|
+
console.log(` ${ref.source.padEnd(20)} ${String(ref.sessions).padEnd(8)} sessions (${chanLabel})`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (opts?.format === "json") {
|
|
1962
|
+
console.log(JSON.stringify({
|
|
1963
|
+
socialSessions: traffic.socialSessions,
|
|
1964
|
+
socialUsers: traffic.socialUsers,
|
|
1965
|
+
totalSessions: traffic.totalSessions,
|
|
1966
|
+
socialSharePct: traffic.socialSharePct,
|
|
1967
|
+
topSources: traffic.socialReferrals.slice(0, 5).map((r) => ({ source: r.source, sessions: r.sessions, channel: r.channelGroup }))
|
|
1968
|
+
}, null, 2));
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
console.log(`Social Traffic Summary for "${project}"
|
|
1972
|
+
`);
|
|
1973
|
+
console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
|
|
1974
|
+
console.log(` Users: ${traffic.socialUsers}`);
|
|
1975
|
+
if (traffic.socialReferrals.length > 0) {
|
|
1976
|
+
console.log();
|
|
1977
|
+
console.log(" TOP SOURCES");
|
|
1978
|
+
for (const ref of traffic.socialReferrals.slice(0, 5)) {
|
|
1979
|
+
const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
|
|
1980
|
+
console.log(` ${ref.source.padEnd(20)} ${String(ref.sessions).padEnd(8)} sessions (${chanLabel})`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async function gaAttribution(project, opts) {
|
|
1985
|
+
const client = getClient3();
|
|
1986
|
+
const traffic = await client.gaTraffic(project);
|
|
1987
|
+
const fmtTrend = (pct) => pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${pct}%`;
|
|
1988
|
+
if (opts?.trend) {
|
|
1989
|
+
const trend = await client.gaAttributionTrend(project);
|
|
1990
|
+
if (opts.format === "json") {
|
|
1991
|
+
console.log(JSON.stringify({
|
|
1992
|
+
totalSessions: traffic.totalSessions,
|
|
1993
|
+
totalUsers: traffic.totalUsers,
|
|
1994
|
+
organicSessions: traffic.totalOrganicSessions,
|
|
1995
|
+
aiSessions: traffic.aiSessionsDeduped,
|
|
1996
|
+
aiUsers: traffic.aiUsersDeduped,
|
|
1997
|
+
socialSessions: traffic.socialSessions,
|
|
1998
|
+
socialUsers: traffic.socialUsers,
|
|
1999
|
+
aiSharePct: traffic.aiSharePct,
|
|
2000
|
+
socialSharePct: traffic.socialSharePct,
|
|
2001
|
+
organicSharePct: traffic.organicSharePct,
|
|
2002
|
+
aiReferrals: traffic.aiReferrals,
|
|
2003
|
+
socialReferrals: traffic.socialReferrals,
|
|
2004
|
+
trend
|
|
2005
|
+
}, null, 2));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
if (traffic.totalSessions === 0) {
|
|
2009
|
+
console.log('No GA4 traffic data. Run "canonry ga sync <project>" first.');
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
console.log(`GA4 Attribution Overview for "${project}"
|
|
2013
|
+
`);
|
|
2014
|
+
console.log(` Total Sessions: ${traffic.totalSessions}`);
|
|
2015
|
+
console.log(` Total Users: ${traffic.totalUsers}`);
|
|
2016
|
+
console.log();
|
|
2017
|
+
console.log(" CHANNEL BREAKDOWN 7d trend 30d trend");
|
|
2018
|
+
console.log(` Organic Search: ${String(traffic.totalOrganicSessions).padEnd(6)} (${String(traffic.organicSharePct).padStart(2)}%) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
|
|
2019
|
+
console.log(` AI Referrals: ${String(traffic.aiSessionsDeduped).padEnd(6)} (${String(traffic.aiSharePct).padStart(2)}%) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)}`);
|
|
2020
|
+
console.log(` Social: ${String(traffic.socialSessions).padEnd(6)} (${String(traffic.socialSharePct).padStart(2)}%) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
|
|
2021
|
+
const otherSessions2 = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsDeduped - traffic.socialSessions;
|
|
2022
|
+
if (otherSessions2 > 0) {
|
|
2023
|
+
const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions2 / traffic.totalSessions * 100) : 0;
|
|
2024
|
+
console.log(` Other: ${String(otherSessions2).padEnd(6)} (${String(otherPct).padStart(2)}%)`);
|
|
2025
|
+
}
|
|
2026
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
2027
|
+
console.log(` Total: ${String(traffic.totalSessions).padEnd(6)} ${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
|
|
2028
|
+
if (trend.aiBiggestMover) {
|
|
2029
|
+
const m = trend.aiBiggestMover;
|
|
2030
|
+
console.log(`
|
|
2031
|
+
AI Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
|
|
2032
|
+
}
|
|
2033
|
+
if (trend.socialBiggestMover) {
|
|
2034
|
+
const m = trend.socialBiggestMover;
|
|
2035
|
+
console.log(` Social Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
|
|
2036
|
+
}
|
|
2037
|
+
if (traffic.lastSyncedAt) {
|
|
2038
|
+
console.log(`
|
|
2039
|
+
Last synced: ${traffic.lastSyncedAt}`);
|
|
2040
|
+
}
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (opts?.format === "json") {
|
|
2044
|
+
console.log(JSON.stringify({
|
|
2045
|
+
totalSessions: traffic.totalSessions,
|
|
2046
|
+
totalUsers: traffic.totalUsers,
|
|
2047
|
+
organicSessions: traffic.totalOrganicSessions,
|
|
2048
|
+
aiSessions: traffic.aiSessionsDeduped,
|
|
2049
|
+
aiUsers: traffic.aiUsersDeduped,
|
|
2050
|
+
socialSessions: traffic.socialSessions,
|
|
2051
|
+
socialUsers: traffic.socialUsers,
|
|
2052
|
+
aiSharePct: traffic.aiSharePct,
|
|
2053
|
+
socialSharePct: traffic.socialSharePct,
|
|
2054
|
+
organicSharePct: traffic.organicSharePct,
|
|
2055
|
+
aiReferrals: traffic.aiReferrals,
|
|
2056
|
+
socialReferrals: traffic.socialReferrals
|
|
2057
|
+
}, null, 2));
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (traffic.totalSessions === 0) {
|
|
2061
|
+
console.log('No GA4 traffic data. Run "canonry ga sync <project>" first.');
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
console.log(`GA4 Attribution Overview for "${project}"
|
|
2065
|
+
`);
|
|
2066
|
+
console.log(` Total Sessions: ${traffic.totalSessions}`);
|
|
2067
|
+
console.log(` Total Users: ${traffic.totalUsers}`);
|
|
2068
|
+
console.log();
|
|
2069
|
+
console.log(" CHANNEL BREAKDOWN");
|
|
2070
|
+
console.log(` Organic Search: ${traffic.totalOrganicSessions} sessions (${traffic.organicSharePct}%)`);
|
|
2071
|
+
console.log(` AI Referrals: ${traffic.aiSessionsDeduped} sessions (${traffic.aiSharePct}%)`);
|
|
2072
|
+
console.log(` Social: ${traffic.socialSessions} sessions (${traffic.socialSharePct}%)`);
|
|
2073
|
+
const otherSessions = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsDeduped - traffic.socialSessions;
|
|
2074
|
+
if (otherSessions > 0) {
|
|
2075
|
+
const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions / traffic.totalSessions * 100) : 0;
|
|
2076
|
+
console.log(` Other: ${otherSessions} sessions (${otherPct}%)`);
|
|
2077
|
+
}
|
|
2078
|
+
if (traffic.aiReferrals.length > 0) {
|
|
2079
|
+
console.log();
|
|
2080
|
+
console.log(" AI SOURCES");
|
|
2081
|
+
for (const ref of traffic.aiReferrals.slice(0, 10)) {
|
|
2082
|
+
const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
|
|
2083
|
+
console.log(` ${ref.source.padEnd(25)} ${String(ref.sessions).padEnd(8)} sessions (${dimLabel})`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (traffic.socialReferrals.length > 0) {
|
|
2087
|
+
console.log();
|
|
2088
|
+
console.log(" SOCIAL SOURCES");
|
|
2089
|
+
for (const ref of traffic.socialReferrals.slice(0, 10)) {
|
|
2090
|
+
const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
|
|
2091
|
+
console.log(` ${ref.source.padEnd(25)} ${String(ref.sessions).padEnd(8)} sessions (${chanLabel})`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (traffic.lastSyncedAt) {
|
|
2095
|
+
console.log(`
|
|
2096
|
+
Last synced: ${traffic.lastSyncedAt}`);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1824
2099
|
|
|
1825
2100
|
// src/cli-commands/ga.ts
|
|
1826
2101
|
var GA_CLI_COMMANDS = [
|
|
@@ -1864,16 +2139,19 @@ var GA_CLI_COMMANDS = [
|
|
|
1864
2139
|
},
|
|
1865
2140
|
{
|
|
1866
2141
|
path: ["ga", "sync"],
|
|
1867
|
-
usage: "canonry ga sync <project> [--days 30] [--format json]",
|
|
2142
|
+
usage: "canonry ga sync <project> [--days 30] [--only traffic|ai|social] [--format json]",
|
|
1868
2143
|
options: {
|
|
1869
|
-
days: stringOption()
|
|
2144
|
+
days: stringOption(),
|
|
2145
|
+
only: stringOption()
|
|
1870
2146
|
},
|
|
1871
2147
|
run: async (input) => {
|
|
1872
|
-
const project = requireProject(input, "ga.sync", "canonry ga sync <project> [--days 30] [--format json]");
|
|
2148
|
+
const project = requireProject(input, "ga.sync", "canonry ga sync <project> [--days 30] [--only traffic|ai|social] [--format json]");
|
|
1873
2149
|
const daysStr = getString(input.values, "days");
|
|
1874
2150
|
const days = daysStr ? parseInt(daysStr, 10) : void 0;
|
|
2151
|
+
const only = getString(input.values, "only");
|
|
1875
2152
|
await gaSync(project, {
|
|
1876
2153
|
days,
|
|
2154
|
+
only,
|
|
1877
2155
|
format: input.format
|
|
1878
2156
|
});
|
|
1879
2157
|
}
|
|
@@ -1910,14 +2188,50 @@ var GA_CLI_COMMANDS = [
|
|
|
1910
2188
|
await gaAiReferralHistory(project, input.format);
|
|
1911
2189
|
}
|
|
1912
2190
|
},
|
|
2191
|
+
{
|
|
2192
|
+
path: ["ga", "social-referral-history"],
|
|
2193
|
+
usage: "canonry ga social-referral-history <project> [--format json]",
|
|
2194
|
+
run: async (input) => {
|
|
2195
|
+
const project = requireProject(input, "ga.social-referral-history", "canonry ga social-referral-history <project> [--format json]");
|
|
2196
|
+
await gaSocialReferralHistory(project, input.format);
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
path: ["ga", "social-referral-summary"],
|
|
2201
|
+
usage: "canonry ga social-referral-summary <project> [--trend] [--format json]",
|
|
2202
|
+
options: {
|
|
2203
|
+
trend: { type: "boolean", default: false }
|
|
2204
|
+
},
|
|
2205
|
+
run: async (input) => {
|
|
2206
|
+
const project = requireProject(input, "ga.social-referral-summary", "canonry ga social-referral-summary <project> [--trend] [--format json]");
|
|
2207
|
+
await gaSocialReferralSummary(project, {
|
|
2208
|
+
trend: input.values.trend === true,
|
|
2209
|
+
format: input.format
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
path: ["ga", "attribution"],
|
|
2215
|
+
usage: "canonry ga attribution <project> [--trend] [--format json]",
|
|
2216
|
+
options: {
|
|
2217
|
+
trend: { type: "boolean", default: false }
|
|
2218
|
+
},
|
|
2219
|
+
run: async (input) => {
|
|
2220
|
+
const project = requireProject(input, "ga.attribution", "canonry ga attribution <project> [--trend] [--format json]");
|
|
2221
|
+
await gaAttribution(project, {
|
|
2222
|
+
trend: input.values.trend === true,
|
|
2223
|
+
format: input.format
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
1913
2227
|
{
|
|
1914
2228
|
path: ["ga"],
|
|
1915
|
-
usage: "canonry ga <
|
|
2229
|
+
usage: "canonry ga <subcommand> <project> [args]",
|
|
1916
2230
|
run: async (input) => {
|
|
1917
2231
|
unknownSubcommand(input.positionals[0], {
|
|
1918
2232
|
command: "ga",
|
|
1919
|
-
usage: "canonry ga <
|
|
1920
|
-
available: ["connect", "disconnect", "status", "sync", "traffic", "coverage", "ai-referral-history"]
|
|
2233
|
+
usage: "canonry ga <subcommand> <project> [args]",
|
|
2234
|
+
available: ["connect", "disconnect", "status", "sync", "traffic", "coverage", "ai-referral-history", "social-referral-history", "social-referral-summary", "attribution"]
|
|
1921
2235
|
});
|
|
1922
2236
|
}
|
|
1923
2237
|
}
|
|
@@ -4016,7 +4330,7 @@ async function triggerRun(project, opts) {
|
|
|
4016
4330
|
console.log(` ${loc} ${id} ${r.status}`);
|
|
4017
4331
|
}
|
|
4018
4332
|
if (opts?.wait) {
|
|
4019
|
-
const pending = locationRuns.filter((r) => r.id && r.status !== "conflict");
|
|
4333
|
+
const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
|
|
4020
4334
|
if (pending.length > 0) {
|
|
4021
4335
|
process.stderr.write(`Waiting for ${pending.length} run(s)`);
|
|
4022
4336
|
await Promise.all(
|
|
@@ -4036,7 +4350,7 @@ async function triggerRun(project, opts) {
|
|
|
4036
4350
|
return;
|
|
4037
4351
|
}
|
|
4038
4352
|
const run = response;
|
|
4039
|
-
if (opts?.wait) {
|
|
4353
|
+
if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
|
|
4040
4354
|
process.stderr.write(`Run ${run.id} started`);
|
|
4041
4355
|
const result = await pollRun(client, run.id);
|
|
4042
4356
|
if (opts?.format === "json") {
|
|
@@ -4047,6 +4361,15 @@ async function triggerRun(project, opts) {
|
|
|
4047
4361
|
}
|
|
4048
4362
|
return;
|
|
4049
4363
|
}
|
|
4364
|
+
if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
|
|
4365
|
+
const result = run.id ? await client.getRun(run.id) : run;
|
|
4366
|
+
if (opts?.format === "json") {
|
|
4367
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4368
|
+
} else {
|
|
4369
|
+
printRunDetail(result);
|
|
4370
|
+
}
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4050
4373
|
if (opts?.format === "json") {
|
|
4051
4374
|
console.log(JSON.stringify(run, null, 2));
|
|
4052
4375
|
return;
|
|
@@ -5219,7 +5542,7 @@ var SNAPSHOT_CLI_COMMANDS = [
|
|
|
5219
5542
|
// src/commands/insights.ts
|
|
5220
5543
|
async function listInsights(project, opts) {
|
|
5221
5544
|
const client = createApiClient();
|
|
5222
|
-
const insights = await client.getInsights(project, { dismissed: opts.dismissed });
|
|
5545
|
+
const insights = await client.getInsights(project, { dismissed: opts.dismissed, runId: opts.runId });
|
|
5223
5546
|
if (opts.format === "json") {
|
|
5224
5547
|
console.log(JSON.stringify(insights, null, 2));
|
|
5225
5548
|
return;
|
|
@@ -5296,15 +5619,17 @@ async function showHealth(project, opts) {
|
|
|
5296
5619
|
var INTELLIGENCE_CLI_COMMANDS = [
|
|
5297
5620
|
{
|
|
5298
5621
|
path: ["insights"],
|
|
5299
|
-
usage: "canonry insights <project> [--dismissed] [--format json]",
|
|
5622
|
+
usage: "canonry insights <project> [--dismissed] [--run-id <id>] [--format json]",
|
|
5300
5623
|
options: {
|
|
5301
|
-
dismissed: { type: "boolean" }
|
|
5624
|
+
dismissed: { type: "boolean" },
|
|
5625
|
+
"run-id": { type: "string" }
|
|
5302
5626
|
},
|
|
5303
5627
|
run: async (input) => {
|
|
5304
|
-
const usage = "canonry insights <project> [--dismissed] [--format json]";
|
|
5628
|
+
const usage = "canonry insights <project> [--dismissed] [--run-id <id>] [--format json]";
|
|
5305
5629
|
const project = requireProject(input, "insights", usage);
|
|
5306
5630
|
const dismissed = input.values.dismissed === true;
|
|
5307
|
-
|
|
5631
|
+
const runId = getString(input.values, "run-id");
|
|
5632
|
+
await listInsights(project, { dismissed, runId, format: input.format });
|
|
5308
5633
|
}
|
|
5309
5634
|
},
|
|
5310
5635
|
{
|
|
@@ -5329,8 +5654,11 @@ var INTELLIGENCE_CLI_COMMANDS = [
|
|
|
5329
5654
|
const usage = "canonry health <project> [--history] [--limit <n>] [--format json]";
|
|
5330
5655
|
const project = requireProject(input, "health", usage);
|
|
5331
5656
|
const history = input.values.history === true;
|
|
5332
|
-
const
|
|
5333
|
-
|
|
5657
|
+
const limit = parseIntegerOption(input, "limit", {
|
|
5658
|
+
command: "health",
|
|
5659
|
+
usage,
|
|
5660
|
+
message: "--limit must be an integer"
|
|
5661
|
+
});
|
|
5334
5662
|
await showHealth(project, { history, limit, format: input.format });
|
|
5335
5663
|
}
|
|
5336
5664
|
}
|
|
@@ -5937,15 +6265,23 @@ async function serveCommand(format = "text") {
|
|
|
5937
6265
|
const db = createClient(config.database);
|
|
5938
6266
|
migrate(db);
|
|
5939
6267
|
const app = await createServer({ config, db });
|
|
5940
|
-
|
|
6268
|
+
let shuttingDown = false;
|
|
6269
|
+
const shutdown = (signal) => {
|
|
6270
|
+
if (shuttingDown) return;
|
|
6271
|
+
shuttingDown = true;
|
|
6272
|
+
if (format === "text") {
|
|
6273
|
+
console.log(`
|
|
6274
|
+
Received ${signal}, stopping server...`);
|
|
6275
|
+
}
|
|
5941
6276
|
app.close().then(() => {
|
|
5942
6277
|
process.exit(0);
|
|
5943
|
-
}).catch(() => {
|
|
6278
|
+
}).catch((err) => {
|
|
6279
|
+
console.error("Error during shutdown:", err);
|
|
5944
6280
|
process.exit(1);
|
|
5945
6281
|
});
|
|
5946
6282
|
};
|
|
5947
|
-
process.on("SIGTERM", shutdown);
|
|
5948
|
-
process.on("SIGINT", shutdown);
|
|
6283
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
6284
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5949
6285
|
try {
|
|
5950
6286
|
await app.listen({ host, port });
|
|
5951
6287
|
const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
|