aads-cli 1.0.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/LICENSE +21 -0
- package/README.ja.md +159 -0
- package/README.md +159 -0
- package/data/campaign-layer-policy.json +68 -0
- package/dist/analysis/anomaly-detection.d.ts +16 -0
- package/dist/analysis/anomaly-detection.d.ts.map +1 -0
- package/dist/analysis/anomaly-detection.js +55 -0
- package/dist/analysis/anomaly-detection.js.map +1 -0
- package/dist/analysis/auto-to-manual.d.ts +11 -0
- package/dist/analysis/auto-to-manual.d.ts.map +1 -0
- package/dist/analysis/auto-to-manual.js +57 -0
- package/dist/analysis/auto-to-manual.js.map +1 -0
- package/dist/analysis/campaign-layer-classifier.d.ts +10 -0
- package/dist/analysis/campaign-layer-classifier.d.ts.map +1 -0
- package/dist/analysis/campaign-layer-classifier.js +50 -0
- package/dist/analysis/campaign-layer-classifier.js.map +1 -0
- package/dist/analysis/campaign-structure.d.ts +3 -0
- package/dist/analysis/campaign-structure.d.ts.map +1 -0
- package/dist/analysis/campaign-structure.js +51 -0
- package/dist/analysis/campaign-structure.js.map +1 -0
- package/dist/analysis/cpc-optimizer.d.ts +11 -0
- package/dist/analysis/cpc-optimizer.d.ts.map +1 -0
- package/dist/analysis/cpc-optimizer.js +46 -0
- package/dist/analysis/cpc-optimizer.js.map +1 -0
- package/dist/analysis/performance-analyzer.d.ts +3 -0
- package/dist/analysis/performance-analyzer.d.ts.map +1 -0
- package/dist/analysis/performance-analyzer.js +58 -0
- package/dist/analysis/performance-analyzer.js.map +1 -0
- package/dist/analysis/sku-classifier.d.ts +3 -0
- package/dist/analysis/sku-classifier.d.ts.map +1 -0
- package/dist/analysis/sku-classifier.js +64 -0
- package/dist/analysis/sku-classifier.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +351 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/campaign-layer-policy.d.ts +28 -0
- package/dist/config/campaign-layer-policy.d.ts.map +1 -0
- package/dist/config/campaign-layer-policy.js +56 -0
- package/dist/config/campaign-layer-policy.js.map +1 -0
- package/dist/config/constants.d.ts +74 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +61 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/optimisation-config.d.ts +24 -0
- package/dist/config/optimisation-config.d.ts.map +1 -0
- package/dist/config/optimisation-config.js +41 -0
- package/dist/config/optimisation-config.js.map +1 -0
- package/dist/core/header-mapper.d.ts +6 -0
- package/dist/core/header-mapper.d.ts.map +1 -0
- package/dist/core/header-mapper.js +34 -0
- package/dist/core/header-mapper.js.map +1 -0
- package/dist/core/normalizer.d.ts +9 -0
- package/dist/core/normalizer.d.ts.map +1 -0
- package/dist/core/normalizer.js +76 -0
- package/dist/core/normalizer.js.map +1 -0
- package/dist/io/csv-reader.d.ts +3 -0
- package/dist/io/csv-reader.d.ts.map +1 -0
- package/dist/io/csv-reader.js +52 -0
- package/dist/io/csv-reader.js.map +1 -0
- package/dist/io/excel-reader.d.ts +4 -0
- package/dist/io/excel-reader.d.ts.map +1 -0
- package/dist/io/excel-reader.js +83 -0
- package/dist/io/excel-reader.js.map +1 -0
- package/dist/io/excel-writer.d.ts +7 -0
- package/dist/io/excel-writer.d.ts.map +1 -0
- package/dist/io/excel-writer.js +42 -0
- package/dist/io/excel-writer.js.map +1 -0
- package/dist/pipeline/analyze-pipeline.d.ts +8 -0
- package/dist/pipeline/analyze-pipeline.d.ts.map +1 -0
- package/dist/pipeline/analyze-pipeline.js +145 -0
- package/dist/pipeline/analyze-pipeline.js.map +1 -0
- package/dist/pipeline/types.d.ts +142 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +2 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/ranking/keyword-matcher.d.ts +10 -0
- package/dist/ranking/keyword-matcher.d.ts.map +1 -0
- package/dist/ranking/keyword-matcher.js +62 -0
- package/dist/ranking/keyword-matcher.js.map +1 -0
- package/dist/ranking/ranking-db.d.ts +14 -0
- package/dist/ranking/ranking-db.d.ts.map +1 -0
- package/dist/ranking/ranking-db.js +97 -0
- package/dist/ranking/ranking-db.js.map +1 -0
- package/dist/ranking/seo-factor.d.ts +5 -0
- package/dist/ranking/seo-factor.d.ts.map +1 -0
- package/dist/ranking/seo-factor.js +57 -0
- package/dist/ranking/seo-factor.js.map +1 -0
- package/dist/ranking/types.d.ts +25 -0
- package/dist/ranking/types.d.ts.map +1 -0
- package/dist/ranking/types.js +2 -0
- package/dist/ranking/types.js.map +1 -0
- package/dist/schemas/types.d.ts +32 -0
- package/dist/schemas/types.d.ts.map +1 -0
- package/dist/schemas/types.js +2 -0
- package/dist/schemas/types.js.map +1 -0
- package/dist/utils/date-utils.d.ts +17 -0
- package/dist/utils/date-utils.d.ts.map +1 -0
- package/dist/utils/date-utils.js +56 -0
- package/dist/utils/date-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +45 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { safeDivide } from "../core/normalizer.js";
|
|
2
|
+
export const buildCampaignStructure = (records) => {
|
|
3
|
+
const campaigns = new Map();
|
|
4
|
+
for (const record of records) {
|
|
5
|
+
const campaignKey = record.campaignId || record.campaignName;
|
|
6
|
+
if (!campaignKey)
|
|
7
|
+
continue;
|
|
8
|
+
const campaign = campaigns.get(campaignKey) ??
|
|
9
|
+
{
|
|
10
|
+
campaignId: record.campaignId,
|
|
11
|
+
campaignName: record.campaignName,
|
|
12
|
+
adGroups: new Map(),
|
|
13
|
+
};
|
|
14
|
+
const adGroupKey = record.adGroupId || record.adGroupName || "__unknown__";
|
|
15
|
+
const adGroup = campaign.adGroups.get(adGroupKey) ??
|
|
16
|
+
{
|
|
17
|
+
adGroupId: record.adGroupId,
|
|
18
|
+
adGroupName: record.adGroupName,
|
|
19
|
+
keywordCount: 0,
|
|
20
|
+
productTargetCount: 0,
|
|
21
|
+
spend: 0,
|
|
22
|
+
sales: 0,
|
|
23
|
+
};
|
|
24
|
+
if (record.keywordText)
|
|
25
|
+
adGroup.keywordCount += 1;
|
|
26
|
+
if (record.productTargetingExpression)
|
|
27
|
+
adGroup.productTargetCount += 1;
|
|
28
|
+
adGroup.spend += record.spend;
|
|
29
|
+
adGroup.sales += record.sales;
|
|
30
|
+
campaign.adGroups.set(adGroupKey, adGroup);
|
|
31
|
+
campaigns.set(campaignKey, campaign);
|
|
32
|
+
}
|
|
33
|
+
return [...campaigns.values()]
|
|
34
|
+
.map((campaign) => ({
|
|
35
|
+
campaignId: campaign.campaignId,
|
|
36
|
+
campaignName: campaign.campaignName,
|
|
37
|
+
adGroups: [...campaign.adGroups.values()]
|
|
38
|
+
.map((adGroup) => ({
|
|
39
|
+
adGroupId: adGroup.adGroupId,
|
|
40
|
+
adGroupName: adGroup.adGroupName,
|
|
41
|
+
keywordCount: adGroup.keywordCount,
|
|
42
|
+
productTargetCount: adGroup.productTargetCount,
|
|
43
|
+
spend: adGroup.spend,
|
|
44
|
+
sales: adGroup.sales,
|
|
45
|
+
acos: safeDivide(adGroup.spend, adGroup.sales),
|
|
46
|
+
}))
|
|
47
|
+
.sort((a, b) => b.spend - a.spend),
|
|
48
|
+
}))
|
|
49
|
+
.sort((a, b) => a.campaignName.localeCompare(b.campaignName));
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=campaign-structure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-structure.js","sourceRoot":"","sources":["../../src/analysis/campaign-structure.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAiBnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAA2B,EAA2B,EAAE;IAC7F,MAAM,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;IAEzD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,YAAY,CAAC;QAC7D,IAAI,CAAC,WAAW;YAAE,SAAS;QAE3B,MAAM,QAAQ,GACZ,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC;YACzB;gBACC,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,QAAQ,EAAE,IAAI,GAAG,EAA8B;aACjB,CAAC;QAEnC,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,WAAW,IAAI,aAAa,CAAC;QAC3E,MAAM,OAAO,GACX,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;YAChC;gBACC,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,YAAY,EAAE,CAAC;gBACf,kBAAkB,EAAE,CAAC;gBACrB,KAAK,EAAE,CAAC;gBACR,KAAK,EAAE,CAAC;aACqB,CAAC;QAElC,IAAI,MAAM,CAAC,WAAW;YAAE,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC;QAClD,IAAI,MAAM,CAAC,0BAA0B;YAAE,OAAO,CAAC,kBAAkB,IAAI,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;QAC9B,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;QAE9B,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3C,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;SAC3B,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aACtC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACjB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,kBAAkB,EAAE,OAAO,CAAC,kBAAkB;YAC9C,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;SAC/C,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;KACrC,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;AAClE,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CpcRecommendation, NormalizedRecord, SkuClassification } from "../pipeline/types.js";
|
|
2
|
+
import type { SeoRankingData } from "../ranking/types.js";
|
|
3
|
+
import type { OptimisationConfig } from "../config/optimisation-config.js";
|
|
4
|
+
export interface CpcOptimizerOptions {
|
|
5
|
+
minClicks: number;
|
|
6
|
+
targetAcos: number;
|
|
7
|
+
seoRankingData?: SeoRankingData;
|
|
8
|
+
seoConfig?: OptimisationConfig["seo"];
|
|
9
|
+
}
|
|
10
|
+
export declare const generateCpcRecommendations: (records: NormalizedRecord[], skuClassifications: SkuClassification[], options: CpcOptimizerOptions) => CpcRecommendation[];
|
|
11
|
+
//# sourceMappingURL=cpc-optimizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpc-optimizer.d.ts","sourceRoot":"","sources":["../../src/analysis/cpc-optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,0BAA0B,GACrC,SAAS,gBAAgB,EAAE,EAC3B,oBAAoB,iBAAiB,EAAE,EACvC,SAAS,mBAAmB,KAC3B,iBAAiB,EA8CnB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { normalizeMatchType, safeDivide } from "../core/normalizer.js";
|
|
2
|
+
import { applySeoAdjustment } from "../ranking/seo-factor.js";
|
|
3
|
+
export const generateCpcRecommendations = (records, skuClassifications, options) => {
|
|
4
|
+
const skuAdjust = new Map();
|
|
5
|
+
skuClassifications.forEach((item) => skuAdjust.set(item.sku, item.bidAdjust));
|
|
6
|
+
const result = [];
|
|
7
|
+
for (const record of records) {
|
|
8
|
+
if (!record.keywordText && !record.productTargetingExpression)
|
|
9
|
+
continue;
|
|
10
|
+
if (record.clicks < options.minClicks)
|
|
11
|
+
continue;
|
|
12
|
+
const avgCpc = safeDivide(record.spend, record.clicks);
|
|
13
|
+
const acos = safeDivide(record.spend, record.sales);
|
|
14
|
+
const acosFactor = acos > 0 ? options.targetAcos / acos : 1;
|
|
15
|
+
const skuFactor = skuAdjust.get(record.sku) ?? 1;
|
|
16
|
+
const currentBid = record.bid === "" ? avgCpc : Number(record.bid);
|
|
17
|
+
const recommendedBid = Math.max(1, Math.round(currentBid * acosFactor * skuFactor));
|
|
18
|
+
result.push({
|
|
19
|
+
campaignId: record.campaignId,
|
|
20
|
+
campaignName: record.campaignName,
|
|
21
|
+
adGroupId: record.adGroupId,
|
|
22
|
+
adGroupName: record.adGroupName,
|
|
23
|
+
keywordId: record.keywordId,
|
|
24
|
+
keywordText: record.keywordText || record.productTargetingExpression,
|
|
25
|
+
matchType: normalizeMatchType(record.matchType),
|
|
26
|
+
sku: record.sku,
|
|
27
|
+
clicks: record.clicks,
|
|
28
|
+
avgCpc,
|
|
29
|
+
currentBid,
|
|
30
|
+
recommendedBid,
|
|
31
|
+
bidAdjust: skuFactor,
|
|
32
|
+
reason: `targetAcos=${options.targetAcos.toFixed(2)} skuFactor=${skuFactor.toFixed(2)}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const sorted = result.sort((a, b) => b.clicks - a.clicks);
|
|
36
|
+
// Apply SEO adjustment if ranking data is available
|
|
37
|
+
if (options.seoRankingData && options.seoConfig?.enabled) {
|
|
38
|
+
return applySeoAdjustment(sorted, options.seoRankingData, {
|
|
39
|
+
enabled: options.seoConfig.enabled,
|
|
40
|
+
seoFactors: options.seoConfig.factors,
|
|
41
|
+
cpcCeiling: options.seoConfig.cpcCeiling,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return sorted;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=cpc-optimizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpc-optimizer.js","sourceRoot":"","sources":["../../src/analysis/cpc-optimizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-analyzer.d.ts","sourceRoot":"","sources":["../../src/analysis/performance-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAiB9E,eAAO,MAAM,kBAAkB,GAAI,SAAS,gBAAgB,EAAE,KAAG,eAAe,EA8D/E,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { safeDivide } from "../core/normalizer.js";
|
|
2
|
+
export const analyzePerformance = (records) => {
|
|
3
|
+
const campaigns = new Map();
|
|
4
|
+
for (const record of records) {
|
|
5
|
+
const key = record.campaignId || record.campaignName;
|
|
6
|
+
if (!key)
|
|
7
|
+
continue;
|
|
8
|
+
const current = campaigns.get(key) ??
|
|
9
|
+
{
|
|
10
|
+
campaignId: record.campaignId,
|
|
11
|
+
campaignName: record.campaignName,
|
|
12
|
+
targetingType: record.targetingType,
|
|
13
|
+
state: record.state,
|
|
14
|
+
clicks: 0,
|
|
15
|
+
impressions: 0,
|
|
16
|
+
spend: 0,
|
|
17
|
+
sales: 0,
|
|
18
|
+
orders: 0,
|
|
19
|
+
budget: 0,
|
|
20
|
+
budgetCount: 0,
|
|
21
|
+
};
|
|
22
|
+
current.clicks += record.clicks;
|
|
23
|
+
current.impressions += record.impressions;
|
|
24
|
+
current.spend += record.spend;
|
|
25
|
+
current.sales += record.sales;
|
|
26
|
+
current.orders += record.orders;
|
|
27
|
+
if (record.dailyBudget !== "") {
|
|
28
|
+
current.budget += record.dailyBudget;
|
|
29
|
+
current.budgetCount += 1;
|
|
30
|
+
}
|
|
31
|
+
campaigns.set(key, current);
|
|
32
|
+
}
|
|
33
|
+
return [...campaigns.values()]
|
|
34
|
+
.map((item) => {
|
|
35
|
+
const ctr = safeDivide(item.clicks, item.impressions);
|
|
36
|
+
const cvr = safeDivide(item.orders, item.clicks);
|
|
37
|
+
const acos = safeDivide(item.spend, item.sales);
|
|
38
|
+
const roas = safeDivide(item.sales, item.spend);
|
|
39
|
+
return {
|
|
40
|
+
campaignId: item.campaignId,
|
|
41
|
+
campaignName: item.campaignName,
|
|
42
|
+
targetingType: item.targetingType,
|
|
43
|
+
state: item.state,
|
|
44
|
+
clicks: item.clicks,
|
|
45
|
+
impressions: item.impressions,
|
|
46
|
+
spend: item.spend,
|
|
47
|
+
sales: item.sales,
|
|
48
|
+
orders: item.orders,
|
|
49
|
+
ctr,
|
|
50
|
+
cvr,
|
|
51
|
+
acos,
|
|
52
|
+
roas,
|
|
53
|
+
dailyBudget: item.budgetCount > 0 ? item.budget / item.budgetCount : "",
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
.sort((a, b) => b.sales - a.sales);
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=performance-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-analyzer.js","sourceRoot":"","sources":["../../src/analysis/performance-analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sku-classifier.d.ts","sourceRoot":"","sources":["../../src/analysis/sku-classifier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA+DhF,eAAO,MAAM,YAAY,GAAI,SAAS,gBAAgB,EAAE,KAAG,iBAAiB,EAa3E,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SKU_LABEL_RULES } from "../config/constants.js";
|
|
2
|
+
import { safeDivide } from "../core/normalizer.js";
|
|
3
|
+
const classifyOne = (sku, agg) => {
|
|
4
|
+
const cvr = safeDivide(agg.orders, agg.clicks);
|
|
5
|
+
const acos = safeDivide(agg.spend, agg.sales);
|
|
6
|
+
if (agg.clicks < 5) {
|
|
7
|
+
return {
|
|
8
|
+
sku,
|
|
9
|
+
label: "improve",
|
|
10
|
+
bidAdjust: SKU_LABEL_RULES.improve.bidAdjust,
|
|
11
|
+
budgetAdjust: SKU_LABEL_RULES.improve.budgetAdjust,
|
|
12
|
+
reason: "data-insufficient(clicks<5)",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (acos <= SKU_LABEL_RULES.focus.maxAcos && cvr >= SKU_LABEL_RULES.focus.minCvr) {
|
|
16
|
+
return {
|
|
17
|
+
sku,
|
|
18
|
+
label: "focus",
|
|
19
|
+
bidAdjust: SKU_LABEL_RULES.focus.bidAdjust,
|
|
20
|
+
budgetAdjust: SKU_LABEL_RULES.focus.budgetAdjust,
|
|
21
|
+
reason: "high-performance",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (acos <= SKU_LABEL_RULES.nurture.maxAcos && cvr >= SKU_LABEL_RULES.nurture.minCvr) {
|
|
25
|
+
return {
|
|
26
|
+
sku,
|
|
27
|
+
label: "nurture",
|
|
28
|
+
bidAdjust: SKU_LABEL_RULES.nurture.bidAdjust,
|
|
29
|
+
budgetAdjust: SKU_LABEL_RULES.nurture.budgetAdjust,
|
|
30
|
+
reason: "stable-growth",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (acos > SKU_LABEL_RULES.prune.minAcos || cvr < SKU_LABEL_RULES.prune.maxCvr) {
|
|
34
|
+
return {
|
|
35
|
+
sku,
|
|
36
|
+
label: "prune",
|
|
37
|
+
bidAdjust: SKU_LABEL_RULES.prune.bidAdjust,
|
|
38
|
+
budgetAdjust: SKU_LABEL_RULES.prune.budgetAdjust,
|
|
39
|
+
reason: "low-efficiency",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
sku,
|
|
44
|
+
label: "improve",
|
|
45
|
+
bidAdjust: SKU_LABEL_RULES.improve.bidAdjust,
|
|
46
|
+
budgetAdjust: SKU_LABEL_RULES.improve.budgetAdjust,
|
|
47
|
+
reason: "default",
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export const classifySkus = (records) => {
|
|
51
|
+
const bySku = new Map();
|
|
52
|
+
for (const record of records) {
|
|
53
|
+
if (!record.sku)
|
|
54
|
+
continue;
|
|
55
|
+
const prev = bySku.get(record.sku) ?? { clicks: 0, spend: 0, sales: 0, orders: 0 };
|
|
56
|
+
prev.clicks += record.clicks;
|
|
57
|
+
prev.spend += record.spend;
|
|
58
|
+
prev.sales += record.sales;
|
|
59
|
+
prev.orders += record.orders;
|
|
60
|
+
bySku.set(record.sku, prev);
|
|
61
|
+
}
|
|
62
|
+
return [...bySku.entries()].map(([sku, agg]) => classifyOne(sku, agg)).sort((a, b) => a.sku.localeCompare(b.sku));
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=sku-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sku-classifier.js","sourceRoot":"","sources":["../../src/analysis/sku-classifier.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadCampaignLayerPolicy } from "./config/campaign-layer-policy.js";
|
|
7
|
+
import { loadOptimisationConfig } from "./config/optimisation-config.js";
|
|
8
|
+
import { Logger } from "./utils/logger.js";
|
|
9
|
+
import { runAnalyzePipeline } from "./pipeline/analyze-pipeline.js";
|
|
10
|
+
import { writeXlsx } from "./io/excel-writer.js";
|
|
11
|
+
import { timestampForFilename } from "./utils/date-utils.js";
|
|
12
|
+
const logger = new Logger(process.env.LOG_LEVEL === "debug" ? "debug" : "info");
|
|
13
|
+
const toPct = (value) => `${(value * 100).toFixed(2)}%`;
|
|
14
|
+
const printLayerSummary = async (result, layerPolicyPath) => {
|
|
15
|
+
if (!result.layerClassification || result.layerClassification.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const policy = await loadCampaignLayerPolicy(layerPolicyPath);
|
|
18
|
+
if (!policy)
|
|
19
|
+
return;
|
|
20
|
+
const LAYER_ORDER = ["L0", "L1", "L2", "L3", "L4"];
|
|
21
|
+
const metricsMap = new Map(result.campaignMetrics.map((m) => [m.campaignName, m]));
|
|
22
|
+
const layerAgg = LAYER_ORDER.map((layerId) => {
|
|
23
|
+
const layer = policy.layers[layerId];
|
|
24
|
+
const classified = result.layerClassification.filter((c) => c.layer === layerId);
|
|
25
|
+
let totalSpend = 0;
|
|
26
|
+
let totalSales = 0;
|
|
27
|
+
let allPaused = true;
|
|
28
|
+
for (const c of classified) {
|
|
29
|
+
const m = metricsMap.get(c.campaignName);
|
|
30
|
+
if (m) {
|
|
31
|
+
totalSpend += m.spend;
|
|
32
|
+
totalSales += m.sales;
|
|
33
|
+
if (m.state !== "paused")
|
|
34
|
+
allPaused = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const acos = totalSales > 0 ? totalSpend / totalSales : 0;
|
|
38
|
+
return {
|
|
39
|
+
layer: layerId,
|
|
40
|
+
name: layer.name,
|
|
41
|
+
campaigns: classified.length,
|
|
42
|
+
spend: totalSpend,
|
|
43
|
+
sales: totalSales,
|
|
44
|
+
acos,
|
|
45
|
+
paused: classified.length > 0 && allPaused,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const totalSpend = layerAgg.reduce((sum, l) => sum + l.spend, 0);
|
|
49
|
+
console.log("\nLayer Summary");
|
|
50
|
+
console.table(layerAgg.map((row) => ({
|
|
51
|
+
Layer: row.layer,
|
|
52
|
+
Name: row.name,
|
|
53
|
+
Campaigns: row.campaigns,
|
|
54
|
+
Spend: `¥${Math.round(row.spend).toLocaleString()}`,
|
|
55
|
+
Sales: `¥${Math.round(row.sales).toLocaleString()}`,
|
|
56
|
+
ACOS: row.sales > 0 ? toPct(row.acos) : "-",
|
|
57
|
+
"Budget%": totalSpend > 0 ? toPct(row.spend / totalSpend) : "-",
|
|
58
|
+
Note: row.paused ? "(paused)" : "",
|
|
59
|
+
})));
|
|
60
|
+
const lowConfidence = result.layerClassification.filter((c) => c.confidence === "low");
|
|
61
|
+
if (lowConfidence.length > 0) {
|
|
62
|
+
console.log(`\n⚠ ${lowConfidence.length} campaign(s) classified with low confidence (fallback):`);
|
|
63
|
+
for (const c of lowConfidence) {
|
|
64
|
+
console.log(` - ${c.campaignName} → ${c.layer}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const toOutputFormat = (value, fallback = "console") => {
|
|
69
|
+
const raw = String(value ?? fallback)
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
if (raw === "console" || raw === "json" || raw === "markdown" || raw === "xlsx") {
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
return fallback;
|
|
76
|
+
};
|
|
77
|
+
const writeTextFile = async (filePath, content) => {
|
|
78
|
+
const resolved = path.resolve(filePath);
|
|
79
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
80
|
+
await writeFile(resolved, content, "utf8");
|
|
81
|
+
};
|
|
82
|
+
const program = new Command();
|
|
83
|
+
program
|
|
84
|
+
.name("aads")
|
|
85
|
+
.description("CLI tool for analyzing Amazon Ads Sponsored Products campaign performance")
|
|
86
|
+
.version("1.0.0");
|
|
87
|
+
program
|
|
88
|
+
.command("analyze")
|
|
89
|
+
.description("Analyze bulk sheet KPIs (CTR, CVR, ACOS, ROAS)")
|
|
90
|
+
.requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
|
|
91
|
+
.option("--layer-policy <file>", "Campaign layer policy JSON path")
|
|
92
|
+
.action(async (options) => {
|
|
93
|
+
const config = loadOptimisationConfig();
|
|
94
|
+
const result = await runAnalyzePipeline(options.input, config, {
|
|
95
|
+
layerPolicyPath: options.layerPolicy,
|
|
96
|
+
});
|
|
97
|
+
const totals = result.records.reduce((acc, row) => {
|
|
98
|
+
acc.clicks += row.clicks;
|
|
99
|
+
acc.impressions += row.impressions;
|
|
100
|
+
acc.spend += row.spend;
|
|
101
|
+
acc.sales += row.sales;
|
|
102
|
+
acc.orders += row.orders;
|
|
103
|
+
return acc;
|
|
104
|
+
}, { clicks: 0, impressions: 0, spend: 0, sales: 0, orders: 0 });
|
|
105
|
+
logger.info("Analyze completed", {
|
|
106
|
+
files: result.input.length,
|
|
107
|
+
rows: result.records.length,
|
|
108
|
+
period: result.dateRange
|
|
109
|
+
? `${result.dateRange.startDate} ~ ${result.dateRange.endDate} (${result.dateRange.days}d)`
|
|
110
|
+
: "unknown",
|
|
111
|
+
campaigns: result.campaignMetrics.length,
|
|
112
|
+
clicks: totals.clicks,
|
|
113
|
+
impressions: totals.impressions,
|
|
114
|
+
spend: totals.spend,
|
|
115
|
+
sales: totals.sales,
|
|
116
|
+
orders: totals.orders,
|
|
117
|
+
ctr: toPct(totals.impressions > 0 ? totals.clicks / totals.impressions : 0),
|
|
118
|
+
cvr: toPct(totals.clicks > 0 ? totals.orders / totals.clicks : 0),
|
|
119
|
+
acos: toPct(totals.sales > 0 ? totals.spend / totals.sales : 0),
|
|
120
|
+
roas: totals.spend > 0 ? (totals.sales / totals.spend).toFixed(2) : "0.00",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
program
|
|
124
|
+
.command("summary")
|
|
125
|
+
.description("Campaign structure summary with layer-level aggregation")
|
|
126
|
+
.requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
|
|
127
|
+
.option("--layer-policy <file>", "Campaign layer policy JSON path")
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
const config = loadOptimisationConfig();
|
|
130
|
+
const result = await runAnalyzePipeline(options.input, config, {
|
|
131
|
+
layerPolicyPath: options.layerPolicy,
|
|
132
|
+
});
|
|
133
|
+
console.log("\nCampaign KPI Summary");
|
|
134
|
+
console.table(result.campaignMetrics.slice(0, 20).map((item) => ({
|
|
135
|
+
campaign: item.campaignName,
|
|
136
|
+
clicks: item.clicks,
|
|
137
|
+
spend: Math.round(item.spend),
|
|
138
|
+
sales: Math.round(item.sales),
|
|
139
|
+
acos: toPct(item.acos),
|
|
140
|
+
roas: item.roas.toFixed(2),
|
|
141
|
+
})));
|
|
142
|
+
console.log("\nStructure Summary");
|
|
143
|
+
console.table(result.structure.slice(0, 20).map((campaign) => ({
|
|
144
|
+
campaign: campaign.campaignName,
|
|
145
|
+
adGroups: campaign.adGroups.length,
|
|
146
|
+
keywords: campaign.adGroups.reduce((sum, g) => sum + g.keywordCount, 0),
|
|
147
|
+
productTargets: campaign.adGroups.reduce((sum, g) => sum + g.productTargetCount, 0),
|
|
148
|
+
})));
|
|
149
|
+
await printLayerSummary(result, options.layerPolicy);
|
|
150
|
+
});
|
|
151
|
+
program
|
|
152
|
+
.command("cpc-report")
|
|
153
|
+
.description("CPC bid optimization report with SEO ranking integration")
|
|
154
|
+
.requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
|
|
155
|
+
.requiredOption("--output <file>", "Output xlsx path")
|
|
156
|
+
.option("--ranking-db <path>", "A_rank SQLite DB path for SEO-based CPC adjustment")
|
|
157
|
+
.action(async (options) => {
|
|
158
|
+
const config = loadOptimisationConfig();
|
|
159
|
+
const analyzed = await runAnalyzePipeline(options.input, config, {
|
|
160
|
+
rankingDbPath: options.rankingDb,
|
|
161
|
+
});
|
|
162
|
+
const header = [
|
|
163
|
+
"Campaign",
|
|
164
|
+
"Ad Group",
|
|
165
|
+
"Keyword",
|
|
166
|
+
"Clicks(14d)",
|
|
167
|
+
"AvgCPC(14d)",
|
|
168
|
+
"CurrentBid",
|
|
169
|
+
"RecommendedBid",
|
|
170
|
+
"BidAdjust",
|
|
171
|
+
"OrganicPos",
|
|
172
|
+
"SeoFactor",
|
|
173
|
+
"Reason",
|
|
174
|
+
];
|
|
175
|
+
const rows = analyzed.cpcRecommendations.map((item) => [
|
|
176
|
+
item.campaignName,
|
|
177
|
+
item.adGroupName,
|
|
178
|
+
item.keywordText,
|
|
179
|
+
item.clicks,
|
|
180
|
+
Math.round(item.avgCpc),
|
|
181
|
+
Math.round(item.currentBid),
|
|
182
|
+
item.recommendedBid,
|
|
183
|
+
item.bidAdjust.toFixed(2),
|
|
184
|
+
item.organicPosition != null ? `#${item.organicPosition}` : "-",
|
|
185
|
+
item.seoFactor != null ? item.seoFactor.toFixed(2) : "-",
|
|
186
|
+
item.reason,
|
|
187
|
+
]);
|
|
188
|
+
await writeXlsx(options.output, [{ name: "CPC_Optimisation_Report", header, rows }]);
|
|
189
|
+
logger.info("CPC report completed", {
|
|
190
|
+
output: options.output,
|
|
191
|
+
rows: rows.length,
|
|
192
|
+
seoEnabled: Boolean(analyzed.seoRankingData),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
program
|
|
196
|
+
.command("promotion-report")
|
|
197
|
+
.description("Auto-to-Manual promotion candidates and negative keyword suggestions")
|
|
198
|
+
.requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
|
|
199
|
+
.requiredOption("--output <file>", "Output xlsx path")
|
|
200
|
+
.action(async (options) => {
|
|
201
|
+
const config = loadOptimisationConfig();
|
|
202
|
+
const analyzed = await runAnalyzePipeline(options.input, config);
|
|
203
|
+
const promotionHeader = [
|
|
204
|
+
"Auto Campaign",
|
|
205
|
+
"Ad Group",
|
|
206
|
+
"Search Term",
|
|
207
|
+
"Clicks",
|
|
208
|
+
"Spend",
|
|
209
|
+
"CVR(%)",
|
|
210
|
+
"Suggested Match Type",
|
|
211
|
+
"Suggested Bid",
|
|
212
|
+
"Suggested Ad Group",
|
|
213
|
+
];
|
|
214
|
+
const promotionRows = analyzed.promotionCandidates.map((item) => [
|
|
215
|
+
item.campaignName,
|
|
216
|
+
item.adGroupName,
|
|
217
|
+
item.keywordText,
|
|
218
|
+
item.clicks,
|
|
219
|
+
Math.round(item.spend),
|
|
220
|
+
(item.cvr * 100).toFixed(2),
|
|
221
|
+
item.matchType,
|
|
222
|
+
item.recommendedBid,
|
|
223
|
+
item.recommendedAdGroupName,
|
|
224
|
+
]);
|
|
225
|
+
const negativeHeader = ["Campaign", "Ad Group", "Term", "MatchType", "Reason"];
|
|
226
|
+
const negativeRows = analyzed.negativeCandidates.map((item) => [
|
|
227
|
+
item.campaignName,
|
|
228
|
+
item.adGroupName,
|
|
229
|
+
item.keywordText,
|
|
230
|
+
item.matchType,
|
|
231
|
+
item.reason,
|
|
232
|
+
]);
|
|
233
|
+
await writeXlsx(options.output, [
|
|
234
|
+
{ name: "AutoToManual_Report", header: promotionHeader, rows: promotionRows },
|
|
235
|
+
{ name: "Negative_Keyword_Optimisation", header: negativeHeader, rows: negativeRows },
|
|
236
|
+
]);
|
|
237
|
+
logger.info("Promotion report completed", {
|
|
238
|
+
output: options.output,
|
|
239
|
+
promotionRows: promotionRows.length,
|
|
240
|
+
negativeRows: negativeRows.length,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
program
|
|
244
|
+
.command("seo-report")
|
|
245
|
+
.description("SEO ranking vs ad keyword integrated report")
|
|
246
|
+
.requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
|
|
247
|
+
.requiredOption("--ranking-db <path>", "A_rank SQLite DB path")
|
|
248
|
+
.option("--output <file>", "Output xlsx path")
|
|
249
|
+
.option("--format <type>", "console | json | xlsx", "console")
|
|
250
|
+
.action(async (options) => {
|
|
251
|
+
const config = loadOptimisationConfig();
|
|
252
|
+
const analyzed = await runAnalyzePipeline(options.input, config, {
|
|
253
|
+
rankingDbPath: options.rankingDb,
|
|
254
|
+
});
|
|
255
|
+
if (!analyzed.seoRankingData) {
|
|
256
|
+
logger.error("No SEO ranking data available. Check --ranking-db path and SEO_ENABLED setting.");
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const seoItems = analyzed.cpcRecommendations.map((rec) => ({
|
|
261
|
+
campaign: rec.campaignName,
|
|
262
|
+
adGroup: rec.adGroupName,
|
|
263
|
+
keyword: rec.keywordText,
|
|
264
|
+
clicks: rec.clicks,
|
|
265
|
+
avgCpc: Math.round(rec.avgCpc),
|
|
266
|
+
currentBid: Math.round(rec.currentBid),
|
|
267
|
+
recommendedBid: rec.recommendedBid,
|
|
268
|
+
organicPos: rec.organicPosition != null ? rec.organicPosition : null,
|
|
269
|
+
seoFactor: rec.seoFactor ?? 1.0,
|
|
270
|
+
reason: rec.reason,
|
|
271
|
+
}));
|
|
272
|
+
const format = toOutputFormat(options.format, "console");
|
|
273
|
+
if (format === "json") {
|
|
274
|
+
const content = JSON.stringify({
|
|
275
|
+
dbPath: analyzed.seoRankingData.dbPath,
|
|
276
|
+
snapshotDate: analyzed.seoRankingData.snapshotDate,
|
|
277
|
+
matchedKeywords: analyzed.seoRankingData.rankings.size,
|
|
278
|
+
items: seoItems,
|
|
279
|
+
}, null, 2);
|
|
280
|
+
if (options.output) {
|
|
281
|
+
await writeTextFile(options.output, content);
|
|
282
|
+
logger.info("SEO report JSON saved", { output: options.output });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.log(content);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (format === "xlsx" || (options.output && options.output.toLowerCase().endsWith(".xlsx"))) {
|
|
290
|
+
const outputPath = options.output ?? path.resolve("output", `seo-report-${timestampForFilename()}.xlsx`);
|
|
291
|
+
const header = [
|
|
292
|
+
"Campaign",
|
|
293
|
+
"Ad Group",
|
|
294
|
+
"Keyword",
|
|
295
|
+
"Clicks(14d)",
|
|
296
|
+
"AvgCPC",
|
|
297
|
+
"CurrentBid",
|
|
298
|
+
"RecommendedBid",
|
|
299
|
+
"OrganicPos",
|
|
300
|
+
"SeoFactor",
|
|
301
|
+
"Reason",
|
|
302
|
+
];
|
|
303
|
+
const rows = seoItems.map((item) => [
|
|
304
|
+
item.campaign,
|
|
305
|
+
item.adGroup,
|
|
306
|
+
item.keyword,
|
|
307
|
+
item.clicks,
|
|
308
|
+
item.avgCpc,
|
|
309
|
+
item.currentBid,
|
|
310
|
+
item.recommendedBid,
|
|
311
|
+
item.organicPos != null ? item.organicPos : "-",
|
|
312
|
+
item.seoFactor.toFixed(2),
|
|
313
|
+
item.reason,
|
|
314
|
+
]);
|
|
315
|
+
await writeXlsx(outputPath, [{ name: "SEO_Report", header, rows }]);
|
|
316
|
+
logger.info("SEO report generated", {
|
|
317
|
+
output: outputPath,
|
|
318
|
+
rows: rows.length,
|
|
319
|
+
matchedKeywords: analyzed.seoRankingData.rankings.size,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Console output
|
|
324
|
+
console.log("\nSEO Ranking Report");
|
|
325
|
+
console.log(`DB: ${analyzed.seoRankingData.dbPath}`);
|
|
326
|
+
console.log(`Snapshot: ${analyzed.seoRankingData.snapshotDate}`);
|
|
327
|
+
console.log(`Matched keywords: ${analyzed.seoRankingData.rankings.size}`);
|
|
328
|
+
console.log("");
|
|
329
|
+
const seoAdjusted = seoItems.filter((item) => item.seoFactor < 1.0);
|
|
330
|
+
if (seoAdjusted.length > 0) {
|
|
331
|
+
console.log("SEO-adjusted keywords:");
|
|
332
|
+
console.table(seoAdjusted.map((item) => ({
|
|
333
|
+
keyword: item.keyword,
|
|
334
|
+
organicPos: item.organicPos != null ? `#${item.organicPos}` : "-",
|
|
335
|
+
seoFactor: item.seoFactor.toFixed(2),
|
|
336
|
+
currentBid: item.currentBid,
|
|
337
|
+
recommendedBid: item.recommendedBid,
|
|
338
|
+
})));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log("No keywords with SEO adjustment (all factors = 1.0)");
|
|
342
|
+
}
|
|
343
|
+
console.log(`\nTotal CPC recommendations: ${seoItems.length}`);
|
|
344
|
+
console.log(`SEO-adjusted: ${seoAdjusted.length}`);
|
|
345
|
+
});
|
|
346
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
logger.error("Command failed", { error: message });
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
});
|
|
351
|
+
//# sourceMappingURL=cli.js.map
|