@thejoseki/clawform 2.3.1 → 2.4.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 +141 -120
- package/bin/clawform.js +161 -151
- package/package.json +65 -65
- package/src/aws/catalog.js +116 -116
- package/src/aws/changeset.js +231 -231
- package/src/aws/drift.js +59 -59
- package/src/aws/exports.js +213 -213
- package/src/aws/inventory.js +156 -156
- package/src/commands/activate.js +137 -105
- package/src/commands/ci-init.js +393 -212
- package/src/commands/cost-report.js +163 -163
- package/src/commands/deactivate.js +9 -2
- package/src/commands/deploy.js +413 -413
- package/src/commands/diff.js +39 -39
- package/src/commands/drift.js +35 -35
- package/src/commands/init-plugin.js +6 -6
- package/src/commands/init.js +360 -360
- package/src/commands/rollback.js +126 -126
- package/src/commands/status.js +147 -147
- package/src/commands/sync.js +50 -50
- package/src/commands/update.js +24 -0
- package/src/commands/validate.js +349 -349
- package/src/hooks/block-dangerous.js +62 -62
- package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
- package/src/hooks/cfn-lint-after-edit.js +81 -81
- package/src/hooks/check-catalog-fresh-eval.js +63 -63
- package/src/hooks/check-catalog-fresh.js +33 -33
- package/src/hooks/session-context-eval.js +81 -81
- package/src/hooks/session-context.js +41 -41
- package/src/hooks/subagent-context-eval.js +44 -44
- package/src/hooks/subagent-context.js +48 -48
- package/src/lib/audit-synthesis.js +24 -24
- package/src/lib/aws-client.js +15 -15
- package/src/lib/cfn-yaml.js +92 -92
- package/src/lib/config.js +76 -76
- package/src/lib/constants.js +85 -85
- package/src/lib/defaults.js +16 -16
- package/src/lib/install-workflow.js +28 -28
- package/src/lib/kit-source.js +43 -5
- package/src/lib/license-client.js +84 -84
- package/src/lib/license-config.js +162 -162
- package/src/lib/license-store.js +43 -8
- package/src/lib/license.js +163 -150
- package/src/lib/lint-overrides.js +226 -226
- package/src/lib/logger.js +16 -16
- package/src/lib/project-config.js +145 -145
- package/src/lib/providers/polar.js +215 -215
- package/src/lib/session-state.js +91 -91
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
// clawform cost-report — month-to-date AWS spend, grouped by the Project / Env /
|
|
2
|
-
// CostCenter tag (or by service). The POST-deploy half of cost governance:
|
|
3
|
-
// cost-guard estimates before you commit a resource; this reads Cost Explorer
|
|
4
|
-
// for what actually got billed, so a team without a FinOps function can see the
|
|
5
|
-
// real number broken down the same way their stacks are tagged.
|
|
6
|
-
//
|
|
7
|
-
// Cost Explorer is a GLOBAL service with only a us-east-1 endpoint — the client
|
|
8
|
-
// is pinned there regardless of the project's deploy region, and (unlike the
|
|
9
|
-
// CFN commands) no AWS_REGION/AWS_PROFILE is *required*: the SDK's default
|
|
10
|
-
// credential chain (env keys, SSO, instance role) is enough, which matters for
|
|
11
|
-
// CI/cron use. CE bills ~$0.01 per GetCostAndUsage request; one logical query
|
|
12
|
-
// per invocation (followed across NextPageToken pages — truncating silently
|
|
13
|
-
// would understate the TOTAL, the one number this command exists to get right).
|
|
14
|
-
//
|
|
15
|
-
// Pure helpers (monthRange / groupByDef / parseCostRows / renderRows) are
|
|
16
|
-
// exported and unit-tested without AWS; costReport() is the thin SDK wrapper.
|
|
17
|
-
|
|
18
|
-
import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer';
|
|
19
|
-
import { logger } from '../lib/logger.js';
|
|
1
|
+
// clawform cost-report — month-to-date AWS spend, grouped by the Project / Env /
|
|
2
|
+
// CostCenter tag (or by service). The POST-deploy half of cost governance:
|
|
3
|
+
// cost-guard estimates before you commit a resource; this reads Cost Explorer
|
|
4
|
+
// for what actually got billed, so a team without a FinOps function can see the
|
|
5
|
+
// real number broken down the same way their stacks are tagged.
|
|
6
|
+
//
|
|
7
|
+
// Cost Explorer is a GLOBAL service with only a us-east-1 endpoint — the client
|
|
8
|
+
// is pinned there regardless of the project's deploy region, and (unlike the
|
|
9
|
+
// CFN commands) no AWS_REGION/AWS_PROFILE is *required*: the SDK's default
|
|
10
|
+
// credential chain (env keys, SSO, instance role) is enough, which matters for
|
|
11
|
+
// CI/cron use. CE bills ~$0.01 per GetCostAndUsage request; one logical query
|
|
12
|
+
// per invocation (followed across NextPageToken pages — truncating silently
|
|
13
|
+
// would understate the TOTAL, the one number this command exists to get right).
|
|
14
|
+
//
|
|
15
|
+
// Pure helpers (monthRange / groupByDef / parseCostRows / renderRows) are
|
|
16
|
+
// exported and unit-tested without AWS; costReport() is the thin SDK wrapper.
|
|
17
|
+
|
|
18
|
+
import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer';
|
|
19
|
+
import { logger } from '../lib/logger.js';
|
|
20
20
|
import { assertLicensed } from '../lib/license.js';
|
|
21
21
|
import { resolveClient as resolveLicenseClient } from '../lib/license-client.js';
|
|
22
|
-
|
|
23
|
-
const GROUP_BY = {
|
|
24
|
-
project: { Type: 'TAG', Key: 'Project' },
|
|
25
|
-
env: { Type: 'TAG', Key: 'Env' },
|
|
26
|
-
costcenter: { Type: 'TAG', Key: 'CostCenter' },
|
|
27
|
-
service: { Type: 'DIMENSION', Key: 'SERVICE' },
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Resolve the CE GroupBy definition. Defaults to the Project tag — the grouping
|
|
31
|
-
// that matches how every clawform deploy tags its stacks (rules/tagging.md).
|
|
32
|
-
export function groupByDef(groupBy = 'project') {
|
|
33
|
-
const key = String(groupBy).toLowerCase();
|
|
34
|
-
const def = GROUP_BY[key];
|
|
35
|
-
if (!def) {
|
|
36
|
-
throw new Error(`Unknown --group-by "${groupBy}". Expected one of: ${Object.keys(GROUP_BY).join(' | ')}.`);
|
|
37
|
-
}
|
|
38
|
-
return { key, def };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
42
|
-
function ymd(d) { return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`; }
|
|
43
|
-
|
|
44
|
-
// Compute the CE TimePeriod for a month. `month` is 'YYYY-MM' (defaults to the
|
|
45
|
-
// month containing `today`). CE's End is EXCLUSIVE and may not be in the future,
|
|
46
|
-
// so for the current month we clamp End to tomorrow (month-to-date); for a past
|
|
47
|
-
// month End is the first of the next month (the full month).
|
|
48
|
-
//
|
|
49
|
-
// `today` is injected so the computation is deterministic in tests.
|
|
50
|
-
export function monthRange(month, today) {
|
|
51
|
-
const now = today instanceof Date ? today : new Date();
|
|
52
|
-
let year, mon; // mon is 1-12
|
|
53
|
-
if (month) {
|
|
54
|
-
const m = /^(\d{4})-(\d{2})$/.exec(String(month).trim());
|
|
55
|
-
if (!m) throw new Error(`Invalid --month "${month}". Expected YYYY-MM.`);
|
|
56
|
-
year = Number(m[1]);
|
|
57
|
-
mon = Number(m[2]);
|
|
58
|
-
if (mon < 1 || mon > 12) throw new Error(`Invalid month number in "${month}".`);
|
|
59
|
-
} else {
|
|
60
|
-
year = now.getUTCFullYear();
|
|
61
|
-
mon = now.getUTCMonth() + 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const start = new Date(Date.UTC(year, mon - 1, 1));
|
|
65
|
-
const nextMonthFirst = new Date(Date.UTC(year, mon, 1));
|
|
66
|
-
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
|
67
|
-
|
|
68
|
-
if (start >= tomorrow) {
|
|
69
|
-
throw new Error(`Month ${year}-${pad2(mon)} is in the future — no cost data yet.`);
|
|
70
|
-
}
|
|
71
|
-
const end = nextMonthFirst < tomorrow ? nextMonthFirst : tomorrow;
|
|
72
|
-
// "partial" = the reported month is still accruing charges, i.e. it's the
|
|
73
|
-
// current UTC month — NOT "end was clamped": on the month's last day the
|
|
74
|
-
// clamp is a no-op (tomorrow == nextMonthFirst) but today's spend is still
|
|
75
|
-
// incomplete, and the label must say so. All date math is UTC because Cost
|
|
76
|
-
// Explorer's day boundaries are UTC.
|
|
77
|
-
const partial = year === now.getUTCFullYear() && mon === now.getUTCMonth() + 1;
|
|
78
|
-
return { Start: ymd(start), End: ymd(end), label: `${year}-${pad2(mon)}`, partial };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Flatten a CE GetCostAndUsage response into sorted rows. Sums each group key
|
|
82
|
-
// across every period returned (we use MONTHLY granularity, so usually one).
|
|
83
|
-
// TAG group keys arrive as "Project$value" — strip to the value; an empty value
|
|
84
|
-
// becomes "(untagged)" so unallocated spend is visible, not hidden.
|
|
85
|
-
export function parseCostRows(response) {
|
|
86
|
-
const totals = new Map();
|
|
87
|
-
let unit = 'USD';
|
|
88
|
-
for (const period of response?.ResultsByTime ?? []) {
|
|
89
|
-
for (const g of period.Groups ?? []) {
|
|
90
|
-
const rawKey = g.Keys?.[0] ?? '';
|
|
91
|
-
const value = rawKey.includes('$') ? rawKey.slice(rawKey.indexOf('$') + 1) : rawKey;
|
|
92
|
-
const key = value === '' ? '(untagged)' : value;
|
|
93
|
-
const metric = g.Metrics?.UnblendedCost ?? {};
|
|
94
|
-
const amount = Number(metric.Amount ?? 0);
|
|
95
|
-
if (metric.Unit) unit = metric.Unit;
|
|
96
|
-
totals.set(key, (totals.get(key) ?? 0) + (Number.isFinite(amount) ? amount : 0));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const rows = [...totals.entries()]
|
|
100
|
-
.map(([key, amount]) => ({ key, amount, unit }))
|
|
101
|
-
.sort((a, b) => b.amount - a.amount);
|
|
102
|
-
const total = rows.reduce((s, r) => s + r.amount, 0);
|
|
103
|
-
return { rows, total, unit };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function renderRows({ rows, total, unit }, meta) {
|
|
107
|
-
const lines = [];
|
|
108
|
-
const span = meta.partial ? `${meta.label} (month-to-date)` : meta.label;
|
|
109
|
-
lines.push(`Cost by ${meta.groupBy} — ${span} — ${meta.Start} → ${meta.End} (exclusive)`);
|
|
110
|
-
lines.push('');
|
|
111
|
-
if (rows.length === 0) {
|
|
112
|
-
lines.push(' (no cost data for this period)');
|
|
113
|
-
return lines.join('\n');
|
|
114
|
-
}
|
|
115
|
-
const width = Math.max(8, ...rows.map((r) => r.key.length));
|
|
116
|
-
for (const r of rows) {
|
|
117
|
-
lines.push(` ${r.key.padEnd(width)} ${r.amount.toFixed(2).padStart(10)} ${r.unit}`);
|
|
118
|
-
}
|
|
119
|
-
lines.push('');
|
|
120
|
-
lines.push(` ${'TOTAL'.padEnd(width)} ${total.toFixed(2).padStart(10)} ${unit}`);
|
|
121
|
-
|
|
122
|
-
const untagged = rows.find((r) => r.key === '(untagged)');
|
|
123
|
-
if (untagged && total > 0 && untagged.amount / total > 0.5 && meta.def.Type === 'TAG') {
|
|
124
|
-
lines.push('');
|
|
125
|
-
lines.push(
|
|
126
|
-
` Note: >50% of spend is (untagged) for tag "${meta.def.Key}". Activate it under ` +
|
|
127
|
-
`Billing → Cost allocation tags so per-${meta.groupBy} breakdown becomes meaningful ` +
|
|
128
|
-
`(activation is not retroactive).`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return lines.join('\n');
|
|
132
|
-
}
|
|
133
|
-
|
|
22
|
+
|
|
23
|
+
const GROUP_BY = {
|
|
24
|
+
project: { Type: 'TAG', Key: 'Project' },
|
|
25
|
+
env: { Type: 'TAG', Key: 'Env' },
|
|
26
|
+
costcenter: { Type: 'TAG', Key: 'CostCenter' },
|
|
27
|
+
service: { Type: 'DIMENSION', Key: 'SERVICE' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Resolve the CE GroupBy definition. Defaults to the Project tag — the grouping
|
|
31
|
+
// that matches how every clawform deploy tags its stacks (rules/tagging.md).
|
|
32
|
+
export function groupByDef(groupBy = 'project') {
|
|
33
|
+
const key = String(groupBy).toLowerCase();
|
|
34
|
+
const def = GROUP_BY[key];
|
|
35
|
+
if (!def) {
|
|
36
|
+
throw new Error(`Unknown --group-by "${groupBy}". Expected one of: ${Object.keys(GROUP_BY).join(' | ')}.`);
|
|
37
|
+
}
|
|
38
|
+
return { key, def };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
42
|
+
function ymd(d) { return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`; }
|
|
43
|
+
|
|
44
|
+
// Compute the CE TimePeriod for a month. `month` is 'YYYY-MM' (defaults to the
|
|
45
|
+
// month containing `today`). CE's End is EXCLUSIVE and may not be in the future,
|
|
46
|
+
// so for the current month we clamp End to tomorrow (month-to-date); for a past
|
|
47
|
+
// month End is the first of the next month (the full month).
|
|
48
|
+
//
|
|
49
|
+
// `today` is injected so the computation is deterministic in tests.
|
|
50
|
+
export function monthRange(month, today) {
|
|
51
|
+
const now = today instanceof Date ? today : new Date();
|
|
52
|
+
let year, mon; // mon is 1-12
|
|
53
|
+
if (month) {
|
|
54
|
+
const m = /^(\d{4})-(\d{2})$/.exec(String(month).trim());
|
|
55
|
+
if (!m) throw new Error(`Invalid --month "${month}". Expected YYYY-MM.`);
|
|
56
|
+
year = Number(m[1]);
|
|
57
|
+
mon = Number(m[2]);
|
|
58
|
+
if (mon < 1 || mon > 12) throw new Error(`Invalid month number in "${month}".`);
|
|
59
|
+
} else {
|
|
60
|
+
year = now.getUTCFullYear();
|
|
61
|
+
mon = now.getUTCMonth() + 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const start = new Date(Date.UTC(year, mon - 1, 1));
|
|
65
|
+
const nextMonthFirst = new Date(Date.UTC(year, mon, 1));
|
|
66
|
+
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
|
67
|
+
|
|
68
|
+
if (start >= tomorrow) {
|
|
69
|
+
throw new Error(`Month ${year}-${pad2(mon)} is in the future — no cost data yet.`);
|
|
70
|
+
}
|
|
71
|
+
const end = nextMonthFirst < tomorrow ? nextMonthFirst : tomorrow;
|
|
72
|
+
// "partial" = the reported month is still accruing charges, i.e. it's the
|
|
73
|
+
// current UTC month — NOT "end was clamped": on the month's last day the
|
|
74
|
+
// clamp is a no-op (tomorrow == nextMonthFirst) but today's spend is still
|
|
75
|
+
// incomplete, and the label must say so. All date math is UTC because Cost
|
|
76
|
+
// Explorer's day boundaries are UTC.
|
|
77
|
+
const partial = year === now.getUTCFullYear() && mon === now.getUTCMonth() + 1;
|
|
78
|
+
return { Start: ymd(start), End: ymd(end), label: `${year}-${pad2(mon)}`, partial };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Flatten a CE GetCostAndUsage response into sorted rows. Sums each group key
|
|
82
|
+
// across every period returned (we use MONTHLY granularity, so usually one).
|
|
83
|
+
// TAG group keys arrive as "Project$value" — strip to the value; an empty value
|
|
84
|
+
// becomes "(untagged)" so unallocated spend is visible, not hidden.
|
|
85
|
+
export function parseCostRows(response) {
|
|
86
|
+
const totals = new Map();
|
|
87
|
+
let unit = 'USD';
|
|
88
|
+
for (const period of response?.ResultsByTime ?? []) {
|
|
89
|
+
for (const g of period.Groups ?? []) {
|
|
90
|
+
const rawKey = g.Keys?.[0] ?? '';
|
|
91
|
+
const value = rawKey.includes('$') ? rawKey.slice(rawKey.indexOf('$') + 1) : rawKey;
|
|
92
|
+
const key = value === '' ? '(untagged)' : value;
|
|
93
|
+
const metric = g.Metrics?.UnblendedCost ?? {};
|
|
94
|
+
const amount = Number(metric.Amount ?? 0);
|
|
95
|
+
if (metric.Unit) unit = metric.Unit;
|
|
96
|
+
totals.set(key, (totals.get(key) ?? 0) + (Number.isFinite(amount) ? amount : 0));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const rows = [...totals.entries()]
|
|
100
|
+
.map(([key, amount]) => ({ key, amount, unit }))
|
|
101
|
+
.sort((a, b) => b.amount - a.amount);
|
|
102
|
+
const total = rows.reduce((s, r) => s + r.amount, 0);
|
|
103
|
+
return { rows, total, unit };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function renderRows({ rows, total, unit }, meta) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
const span = meta.partial ? `${meta.label} (month-to-date)` : meta.label;
|
|
109
|
+
lines.push(`Cost by ${meta.groupBy} — ${span} — ${meta.Start} → ${meta.End} (exclusive)`);
|
|
110
|
+
lines.push('');
|
|
111
|
+
if (rows.length === 0) {
|
|
112
|
+
lines.push(' (no cost data for this period)');
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
const width = Math.max(8, ...rows.map((r) => r.key.length));
|
|
116
|
+
for (const r of rows) {
|
|
117
|
+
lines.push(` ${r.key.padEnd(width)} ${r.amount.toFixed(2).padStart(10)} ${r.unit}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(` ${'TOTAL'.padEnd(width)} ${total.toFixed(2).padStart(10)} ${unit}`);
|
|
121
|
+
|
|
122
|
+
const untagged = rows.find((r) => r.key === '(untagged)');
|
|
123
|
+
if (untagged && total > 0 && untagged.amount / total > 0.5 && meta.def.Type === 'TAG') {
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(
|
|
126
|
+
` Note: >50% of spend is (untagged) for tag "${meta.def.Key}". Activate it under ` +
|
|
127
|
+
`Billing → Cost allocation tags so per-${meta.groupBy} breakdown becomes meaningful ` +
|
|
128
|
+
`(activation is not retroactive).`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
134
|
export default async function costReport({ month, groupBy, profile, today } = {}) {
|
|
135
|
-
await assertLicensed({ command: 'cost-report', client: resolveLicenseClient() });
|
|
136
|
-
const { key, def } = groupByDef(groupBy);
|
|
137
|
-
const range = monthRange(month, today);
|
|
138
|
-
|
|
139
|
-
// Unlike the CFN commands, no profile/region is REQUIRED here: CE is global
|
|
140
|
-
// (us-east-1) and the SDK's default credential chain covers env keys / SSO /
|
|
141
|
-
// instance roles — the common CI shapes. An explicit --profile still wins.
|
|
142
|
-
if (profile) process.env.AWS_PROFILE = profile;
|
|
143
|
-
|
|
144
|
-
const client = new CostExplorerClient({ region: 'us-east-1' });
|
|
145
|
-
|
|
146
|
-
logger.step(`clawform cost-report — ${range.label} by ${key}`);
|
|
147
|
-
|
|
148
|
-
// GetCostAndUsage paginates via NextPageToken; follow it — a truncated group
|
|
149
|
-
// list silently understates the TOTAL.
|
|
150
|
-
const results = [];
|
|
151
|
-
let nextPageToken;
|
|
152
|
-
do {
|
|
153
|
-
const resp = await client.send(new GetCostAndUsageCommand({
|
|
154
|
-
TimePeriod: { Start: range.Start, End: range.End },
|
|
155
|
-
Granularity: 'MONTHLY',
|
|
156
|
-
Metrics: ['UnblendedCost'],
|
|
157
|
-
GroupBy: [def],
|
|
158
|
-
...(nextPageToken ? { NextPageToken: nextPageToken } : {}),
|
|
159
|
-
}));
|
|
160
|
-
results.push(...(resp.ResultsByTime ?? []));
|
|
161
|
-
nextPageToken = resp.NextPageToken;
|
|
162
|
-
} while (nextPageToken);
|
|
163
|
-
|
|
164
|
-
const parsed = parseCostRows({ ResultsByTime: results });
|
|
165
|
-
console.log(renderRows(parsed, { ...range, groupBy: key, def }));
|
|
166
|
-
}
|
|
135
|
+
await assertLicensed({ command: 'cost-report', client: resolveLicenseClient() });
|
|
136
|
+
const { key, def } = groupByDef(groupBy);
|
|
137
|
+
const range = monthRange(month, today);
|
|
138
|
+
|
|
139
|
+
// Unlike the CFN commands, no profile/region is REQUIRED here: CE is global
|
|
140
|
+
// (us-east-1) and the SDK's default credential chain covers env keys / SSO /
|
|
141
|
+
// instance roles — the common CI shapes. An explicit --profile still wins.
|
|
142
|
+
if (profile) process.env.AWS_PROFILE = profile;
|
|
143
|
+
|
|
144
|
+
const client = new CostExplorerClient({ region: 'us-east-1' });
|
|
145
|
+
|
|
146
|
+
logger.step(`clawform cost-report — ${range.label} by ${key}`);
|
|
147
|
+
|
|
148
|
+
// GetCostAndUsage paginates via NextPageToken; follow it — a truncated group
|
|
149
|
+
// list silently understates the TOTAL.
|
|
150
|
+
const results = [];
|
|
151
|
+
let nextPageToken;
|
|
152
|
+
do {
|
|
153
|
+
const resp = await client.send(new GetCostAndUsageCommand({
|
|
154
|
+
TimePeriod: { Start: range.Start, End: range.End },
|
|
155
|
+
Granularity: 'MONTHLY',
|
|
156
|
+
Metrics: ['UnblendedCost'],
|
|
157
|
+
GroupBy: [def],
|
|
158
|
+
...(nextPageToken ? { NextPageToken: nextPageToken } : {}),
|
|
159
|
+
}));
|
|
160
|
+
results.push(...(resp.ResultsByTime ?? []));
|
|
161
|
+
nextPageToken = resp.NextPageToken;
|
|
162
|
+
} while (nextPageToken);
|
|
163
|
+
|
|
164
|
+
const parsed = parseCostRows({ ResultsByTime: results });
|
|
165
|
+
console.log(renderRows(parsed, { ...range, groupBy: key, def }));
|
|
166
|
+
}
|
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
import { logger } from '../lib/logger.js';
|
|
8
8
|
import { resolveClient } from '../lib/license-client.js';
|
|
9
|
-
import {
|
|
9
|
+
import { readLicenseRaw, clearLicense } from '../lib/license-store.js';
|
|
10
10
|
|
|
11
11
|
export default async function deactivate(_opts = {}, {
|
|
12
12
|
client = resolveClient(),
|
|
13
|
-
read
|
|
13
|
+
// Raw read on purpose: a record invalidated by a schema/version change still
|
|
14
|
+
// points at a live Polar slot. Reading it unsigned is what lets the owner
|
|
15
|
+
// reclaim that orphaned slot — the alternative strands it until support acts.
|
|
16
|
+
// Safe: deactivating grants nothing (see readLicenseRaw), and the vendor call
|
|
17
|
+
// is KEY-SCOPED — Polar only releases an instanceId that belongs to the key in
|
|
18
|
+
// the same record, so a hand-written record can only release activations of a
|
|
19
|
+
// key its author already holds, which is already full control of that licence.
|
|
20
|
+
read = readLicenseRaw,
|
|
14
21
|
clear = clearLicense,
|
|
15
22
|
log = (m) => logger.ok(m),
|
|
16
23
|
warn = (m) => logger.warn(m),
|