flarepilot 0.1.0 → 0.2.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/package.json +1 -1
- package/src/cli.js +11 -1
- package/src/commands/cost.js +578 -0
- package/src/lib/cf.js +24 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ps } from "./commands/ps.js";
|
|
|
17
17
|
import { logs } from "./commands/logs.js";
|
|
18
18
|
import { open } from "./commands/open.js";
|
|
19
19
|
import { doctor } from "./commands/doctor.js";
|
|
20
|
+
import { cost } from "./commands/cost.js";
|
|
20
21
|
import { fmt } from "./lib/output.js";
|
|
21
22
|
|
|
22
23
|
var program = new Command();
|
|
@@ -24,7 +25,7 @@ var program = new Command();
|
|
|
24
25
|
program
|
|
25
26
|
.name("flarepilot")
|
|
26
27
|
.description("Deploy and manage apps on Cloudflare Containers")
|
|
27
|
-
.version("0.
|
|
28
|
+
.version("0.2.0");
|
|
28
29
|
|
|
29
30
|
// --- Auth ---
|
|
30
31
|
|
|
@@ -205,6 +206,15 @@ program
|
|
|
205
206
|
console.log("");
|
|
206
207
|
});
|
|
207
208
|
|
|
209
|
+
// --- Cost ---
|
|
210
|
+
|
|
211
|
+
program
|
|
212
|
+
.command("cost [name]")
|
|
213
|
+
.description("Show estimated costs for an app or all apps")
|
|
214
|
+
.option("--since <period>", "Date range: Nd (e.g. 7d) or YYYY-MM-DD (default: month to date)")
|
|
215
|
+
.option("--json", "Output as JSON")
|
|
216
|
+
.action(cost);
|
|
217
|
+
|
|
208
218
|
// --- Doctor ---
|
|
209
219
|
|
|
210
220
|
program
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfig,
|
|
3
|
+
getAppConfig,
|
|
4
|
+
listWorkerScripts,
|
|
5
|
+
listContainerApps,
|
|
6
|
+
getDONamespaceId,
|
|
7
|
+
cfGraphQL,
|
|
8
|
+
} from "../lib/cf.js";
|
|
9
|
+
import { phase, status, fatal, fmt, table } from "../lib/output.js";
|
|
10
|
+
import { resolveAppName } from "../lib/link.js";
|
|
11
|
+
|
|
12
|
+
// --- Pricing (Workers Paid plan, $5/mo) ---
|
|
13
|
+
|
|
14
|
+
var PRICING = {
|
|
15
|
+
workerRequests: { included: 10_000_000, rate: 0.30 / 1_000_000 },
|
|
16
|
+
workerCpuMs: { included: 30_000_000, rate: 0.02 / 1_000_000 },
|
|
17
|
+
doRequests: { included: 1_000_000, rate: 0.15 / 1_000_000 },
|
|
18
|
+
doGbSeconds: { included: 400_000, rate: 12.50 / 1_000_000 },
|
|
19
|
+
containerVcpuSec: { included: 375 * 60, rate: 0.000020 },
|
|
20
|
+
containerMemGibSec: { included: 25 * 3600, rate: 0.0000025 },
|
|
21
|
+
containerDiskGbSec: { included: 200 * 3600, rate: 0.00000007 },
|
|
22
|
+
containerEgressGb: { included: 0, rate: 0.025 },
|
|
23
|
+
platform: 5.0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// --- Date range parsing ---
|
|
27
|
+
|
|
28
|
+
function parseDateRange(since) {
|
|
29
|
+
var now = new Date();
|
|
30
|
+
var until = now;
|
|
31
|
+
var start;
|
|
32
|
+
|
|
33
|
+
if (!since) {
|
|
34
|
+
// Default: 1st of current month
|
|
35
|
+
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
36
|
+
} else if (/^\d+d$/.test(since)) {
|
|
37
|
+
var days = parseInt(since);
|
|
38
|
+
start = new Date(now.getTime() - days * 86400_000);
|
|
39
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(since)) {
|
|
40
|
+
start = new Date(since + "T00:00:00Z");
|
|
41
|
+
if (isNaN(start.getTime())) {
|
|
42
|
+
fatal(`Invalid date: ${since}`, "Use YYYY-MM-DD or Nd (e.g. 7d)");
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
fatal(
|
|
46
|
+
`Invalid --since value: ${since}`,
|
|
47
|
+
"Use YYYY-MM-DD or Nd (e.g. 7d, 30d)"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var sinceISO = start.toISOString().slice(0, 19) + "Z";
|
|
52
|
+
var untilISO = until.toISOString().slice(0, 19) + "Z";
|
|
53
|
+
|
|
54
|
+
var label = formatDateRange(start, until);
|
|
55
|
+
return { sinceISO, untilISO, sinceDate: start, untilDate: until, label };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatDateRange(start, end) {
|
|
59
|
+
var opts = { month: "short", day: "numeric" };
|
|
60
|
+
var s = start.toLocaleDateString("en-US", opts);
|
|
61
|
+
var e = end.toLocaleDateString("en-US", opts);
|
|
62
|
+
return `${s} – ${e}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- GraphQL query strings ---
|
|
66
|
+
|
|
67
|
+
var workersGQL = `query Workers($accountTag: string!, $filter: WorkersInvocationsAdaptiveFilter_InputObject!) {
|
|
68
|
+
viewer {
|
|
69
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
70
|
+
workersInvocationsAdaptive(limit: 10000, filter: $filter) {
|
|
71
|
+
dimensions { scriptName }
|
|
72
|
+
sum { requests cpuTimeUs }
|
|
73
|
+
avg { sampleInterval }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}`;
|
|
78
|
+
|
|
79
|
+
var doRequestsGQL = `query DORequests($accountTag: string!, $filter: DurableObjectsInvocationsAdaptiveGroupsFilter_InputObject!) {
|
|
80
|
+
viewer {
|
|
81
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
82
|
+
durableObjectsInvocationsAdaptiveGroups(limit: 10000, filter: $filter) {
|
|
83
|
+
dimensions { namespaceId }
|
|
84
|
+
sum { requests }
|
|
85
|
+
avg { sampleInterval }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
var doDurationGQL = `query DODuration($accountTag: string!, $filter: DurableObjectsPeriodicGroupsFilter_InputObject!) {
|
|
92
|
+
viewer {
|
|
93
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
94
|
+
durableObjectsPeriodicGroups(limit: 10000, filter: $filter) {
|
|
95
|
+
dimensions { namespaceId }
|
|
96
|
+
sum { activeTime inboundWebsocketMsgCount }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}`;
|
|
101
|
+
|
|
102
|
+
var containersGQL = `query Containers($accountTag: string!, $filter: AccountContainersMetricsAdaptiveGroupsFilter_InputObject!) {
|
|
103
|
+
viewer {
|
|
104
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
105
|
+
containersMetricsAdaptiveGroups(limit: 10000, filter: $filter) {
|
|
106
|
+
dimensions { applicationId }
|
|
107
|
+
sum { cpuTimeSec allocatedMemory allocatedDisk txBytes }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}`;
|
|
112
|
+
|
|
113
|
+
// --- Aggregate raw GraphQL results per app ---
|
|
114
|
+
|
|
115
|
+
function aggregateResults(apps, analytics) {
|
|
116
|
+
var { workersData, doReqData, doDurData, containersData } = analytics;
|
|
117
|
+
|
|
118
|
+
// Index workers data by scriptName
|
|
119
|
+
var workerRows =
|
|
120
|
+
workersData?.viewer?.accounts?.[0]?.workersInvocationsAdaptive || [];
|
|
121
|
+
var workersByScript = {};
|
|
122
|
+
for (var row of workerRows) {
|
|
123
|
+
var sn = row.dimensions.scriptName;
|
|
124
|
+
var si = row.avg?.sampleInterval || 1;
|
|
125
|
+
if (!workersByScript[sn]) workersByScript[sn] = { requests: 0, cpuMs: 0 };
|
|
126
|
+
workersByScript[sn].requests += (row.sum?.requests || 0) * si;
|
|
127
|
+
workersByScript[sn].cpuMs += ((row.sum?.cpuTimeUs || 0) / 1000) * si;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Index DO requests by namespaceId
|
|
131
|
+
var doReqRows =
|
|
132
|
+
doReqData?.viewer?.accounts?.[0]?.durableObjectsInvocationsAdaptiveGroups || [];
|
|
133
|
+
var doReqByNs = {};
|
|
134
|
+
for (var row of doReqRows) {
|
|
135
|
+
var ns = row.dimensions.namespaceId;
|
|
136
|
+
var si = row.avg?.sampleInterval || 1;
|
|
137
|
+
if (!doReqByNs[ns]) doReqByNs[ns] = 0;
|
|
138
|
+
doReqByNs[ns] += (row.sum?.requests || 0) * si;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Index DO duration by namespaceId
|
|
142
|
+
var doDurRows =
|
|
143
|
+
doDurData?.viewer?.accounts?.[0]?.durableObjectsPeriodicGroups || [];
|
|
144
|
+
var doDurByNs = {};
|
|
145
|
+
for (var row of doDurRows) {
|
|
146
|
+
var ns = row.dimensions.namespaceId;
|
|
147
|
+
if (!doDurByNs[ns]) doDurByNs[ns] = { activeTime: 0, wsInbound: 0 };
|
|
148
|
+
doDurByNs[ns].activeTime += row.sum?.activeTime || 0;
|
|
149
|
+
doDurByNs[ns].wsInbound += row.sum?.inboundWebsocketMsgCount || 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Index container metrics by applicationId
|
|
153
|
+
var containerRows =
|
|
154
|
+
containersData?.viewer?.accounts?.[0]?.containersMetricsAdaptiveGroups || [];
|
|
155
|
+
var containersByAppId = {};
|
|
156
|
+
for (var row of containerRows) {
|
|
157
|
+
var appId = row.dimensions.applicationId;
|
|
158
|
+
if (!containersByAppId[appId]) {
|
|
159
|
+
containersByAppId[appId] = { cpuTimeSec: 0, allocatedMemory: 0, allocatedDisk: 0, txBytes: 0 };
|
|
160
|
+
}
|
|
161
|
+
containersByAppId[appId].cpuTimeSec += row.sum?.cpuTimeSec || 0;
|
|
162
|
+
containersByAppId[appId].allocatedMemory += row.sum?.allocatedMemory || 0;
|
|
163
|
+
containersByAppId[appId].allocatedDisk += row.sum?.allocatedDisk || 0;
|
|
164
|
+
containersByAppId[appId].txBytes += row.sum?.txBytes || 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build per-app usage
|
|
168
|
+
return apps.map((app) => {
|
|
169
|
+
var scriptName = `flarepilot-${app.name}`;
|
|
170
|
+
var w = workersByScript[scriptName] || { requests: 0, cpuMs: 0 };
|
|
171
|
+
var nsId = app.namespaceId;
|
|
172
|
+
|
|
173
|
+
var doDuration = nsId ? doDurByNs[nsId] || {} : {};
|
|
174
|
+
// Billable DO requests = HTTP invocations + inbound WS messages at 20:1 ratio
|
|
175
|
+
var doRequests = (nsId ? doReqByNs[nsId] || 0 : 0) + (doDuration.wsInbound || 0) / 20;
|
|
176
|
+
|
|
177
|
+
// Real container metrics from containersMetricsAdaptiveGroups
|
|
178
|
+
var c = app.containerAppId ? containersByAppId[app.containerAppId] || {} : {};
|
|
179
|
+
var containerVcpuSec = c.cpuTimeSec || 0;
|
|
180
|
+
// allocatedMemory is in byte-seconds → convert to GiB-seconds
|
|
181
|
+
var containerMemGibSec = (c.allocatedMemory || 0) / (1024 * 1024 * 1024);
|
|
182
|
+
// allocatedDisk is in byte-seconds → convert to GB-seconds
|
|
183
|
+
var containerDiskGbSec = (c.allocatedDisk || 0) / 1_000_000_000;
|
|
184
|
+
// txBytes → GB
|
|
185
|
+
var containerEgressGb = (c.txBytes || 0) / 1_000_000_000;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
name: app.name,
|
|
189
|
+
usage: {
|
|
190
|
+
workerRequests: Math.round(w.requests),
|
|
191
|
+
workerCpuMs: Math.round(w.cpuMs),
|
|
192
|
+
doRequests: Math.round(doRequests),
|
|
193
|
+
doWsMsgs: Math.round(doDuration.wsInbound || 0),
|
|
194
|
+
doGbSeconds: Math.round(((doDuration.activeTime || 0) / 1_000_000) * 128 / 1024), // activeTime is µs, 128MiB DO memory → GB-s
|
|
195
|
+
containerVcpuSec,
|
|
196
|
+
containerMemGibSec,
|
|
197
|
+
containerDiskGbSec,
|
|
198
|
+
containerEgressGb,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- Cost calculation ---
|
|
205
|
+
|
|
206
|
+
function calculateAppCosts(usage) {
|
|
207
|
+
return {
|
|
208
|
+
workerRequests: usage.workerRequests * PRICING.workerRequests.rate,
|
|
209
|
+
workerCpuMs: usage.workerCpuMs * PRICING.workerCpuMs.rate,
|
|
210
|
+
doRequests: usage.doRequests * PRICING.doRequests.rate,
|
|
211
|
+
doGbSeconds: usage.doGbSeconds * PRICING.doGbSeconds.rate,
|
|
212
|
+
containerVcpuSec: usage.containerVcpuSec * PRICING.containerVcpuSec.rate,
|
|
213
|
+
containerMemGibSec: usage.containerMemGibSec * PRICING.containerMemGibSec.rate,
|
|
214
|
+
containerDiskGbSec: usage.containerDiskGbSec * PRICING.containerDiskGbSec.rate,
|
|
215
|
+
containerEgressGb: usage.containerEgressGb * PRICING.containerEgressGb.rate,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function applyFreeTier(appResults) {
|
|
220
|
+
// Sum gross usage fleet-wide
|
|
221
|
+
var totals = {
|
|
222
|
+
workerRequests: 0,
|
|
223
|
+
workerCpuMs: 0,
|
|
224
|
+
doRequests: 0,
|
|
225
|
+
doGbSeconds: 0,
|
|
226
|
+
containerVcpuSec: 0,
|
|
227
|
+
containerMemGibSec: 0,
|
|
228
|
+
containerDiskGbSec: 0,
|
|
229
|
+
containerEgressGb: 0,
|
|
230
|
+
};
|
|
231
|
+
for (var app of appResults) {
|
|
232
|
+
for (var key of Object.keys(totals)) {
|
|
233
|
+
totals[key] += app.usage[key];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Calculate fleet-wide overage costs (usage beyond free tier)
|
|
238
|
+
var fleetOverage = {};
|
|
239
|
+
for (var key of Object.keys(totals)) {
|
|
240
|
+
var included = PRICING[key].included;
|
|
241
|
+
var overageUsage = Math.max(0, totals[key] - included);
|
|
242
|
+
fleetOverage[key] = overageUsage * PRICING[key].rate;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Gross fleet total
|
|
246
|
+
var grossFleetTotal = 0;
|
|
247
|
+
for (var app of appResults) {
|
|
248
|
+
var costs = calculateAppCosts(app.usage);
|
|
249
|
+
app.grossCosts = costs;
|
|
250
|
+
var appGross = Object.values(costs).reduce((a, b) => a + b, 0);
|
|
251
|
+
app.grossTotal = appGross;
|
|
252
|
+
grossFleetTotal += appGross;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Net fleet total (after free tier)
|
|
256
|
+
var netFleetTotal = Object.values(fleetOverage).reduce((a, b) => a + b, 0);
|
|
257
|
+
var freeTierDiscount = grossFleetTotal - netFleetTotal;
|
|
258
|
+
|
|
259
|
+
// Distribute discount proportionally
|
|
260
|
+
for (var app of appResults) {
|
|
261
|
+
if (grossFleetTotal > 0) {
|
|
262
|
+
var share = app.grossTotal / grossFleetTotal;
|
|
263
|
+
app.freeTierDiscount = freeTierDiscount * share;
|
|
264
|
+
} else {
|
|
265
|
+
app.freeTierDiscount = 0;
|
|
266
|
+
}
|
|
267
|
+
app.netTotal = Math.max(0, app.grossTotal - app.freeTierDiscount);
|
|
268
|
+
|
|
269
|
+
// Categorize costs for display
|
|
270
|
+
app.workersCost = app.grossCosts.workerRequests + app.grossCosts.workerCpuMs;
|
|
271
|
+
app.doCost = app.grossCosts.doRequests + app.grossCosts.doGbSeconds;
|
|
272
|
+
app.containerCost =
|
|
273
|
+
app.grossCosts.containerVcpuSec +
|
|
274
|
+
app.grossCosts.containerMemGibSec +
|
|
275
|
+
app.grossCosts.containerDiskGbSec +
|
|
276
|
+
app.grossCosts.containerEgressGb;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { appResults, freeTierDiscount, netFleetTotal, grossFleetTotal };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Formatting helpers ---
|
|
283
|
+
|
|
284
|
+
function fmtCost(n) {
|
|
285
|
+
return "$" + n.toFixed(2);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function fmtUsage(n, unit) {
|
|
289
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M " + unit;
|
|
290
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K " + unit;
|
|
291
|
+
return n.toLocaleString("en-US") + " " + unit;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function fmtDuration(seconds) {
|
|
295
|
+
var hrs = seconds / 3600;
|
|
296
|
+
if (hrs >= 1) return hrs.toFixed(1) + " vCPU-hrs";
|
|
297
|
+
var mins = seconds / 60;
|
|
298
|
+
return mins.toFixed(1) + " vCPU-min";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function fmtGibHours(gibSec) {
|
|
302
|
+
var hrs = gibSec / 3600;
|
|
303
|
+
if (hrs >= 1) return hrs.toFixed(1) + " GiB-hrs";
|
|
304
|
+
var mins = gibSec / 60;
|
|
305
|
+
return mins.toFixed(1) + " GiB-min";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function fmtGbHours(gbSec) {
|
|
309
|
+
var hrs = gbSec / 3600;
|
|
310
|
+
if (hrs >= 1) return hrs.toFixed(1) + " GB-hrs";
|
|
311
|
+
var mins = gbSec / 60;
|
|
312
|
+
return mins.toFixed(1) + " GB-min";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Render single app ---
|
|
316
|
+
|
|
317
|
+
function renderSingleApp(app, range) {
|
|
318
|
+
console.log("");
|
|
319
|
+
console.log(
|
|
320
|
+
` ${fmt.bold("Estimated cost for")} ${fmt.app(app.name)} ${fmt.dim(`(${range.label})`)}`
|
|
321
|
+
);
|
|
322
|
+
console.log("");
|
|
323
|
+
|
|
324
|
+
var header = " COMPONENT USAGE ESTIMATED COST";
|
|
325
|
+
var sep = " " + "─".repeat(50);
|
|
326
|
+
|
|
327
|
+
console.log(fmt.bold(header));
|
|
328
|
+
console.log(fmt.dim(sep));
|
|
329
|
+
|
|
330
|
+
var rows = [
|
|
331
|
+
["Workers", fmtUsage(app.usage.workerRequests, "requests"), fmtCost(app.grossCosts.workerRequests)],
|
|
332
|
+
["", fmtUsage(app.usage.workerCpuMs, "CPU-ms"), fmtCost(app.grossCosts.workerCpuMs)],
|
|
333
|
+
["Durable Obj", fmtUsage(app.usage.doRequests, "requests"), fmtCost(app.grossCosts.doRequests)],
|
|
334
|
+
app.usage.doWsMsgs > 0
|
|
335
|
+
? ["", fmtUsage(app.usage.doWsMsgs, "WS msgs") + fmt.dim(" (20:1)"), ""]
|
|
336
|
+
: null,
|
|
337
|
+
["", fmtUsage(app.usage.doGbSeconds, "GB-s"), fmtCost(app.grossCosts.doGbSeconds)],
|
|
338
|
+
["Containers", fmtDuration(app.usage.containerVcpuSec), fmtCost(app.grossCosts.containerVcpuSec)],
|
|
339
|
+
["", fmtGibHours(app.usage.containerMemGibSec) + " mem", fmtCost(app.grossCosts.containerMemGibSec)],
|
|
340
|
+
["", fmtGbHours(app.usage.containerDiskGbSec) + " disk", fmtCost(app.grossCosts.containerDiskGbSec)],
|
|
341
|
+
["", fmtUsage(app.usage.containerEgressGb, "GB egress"), fmtCost(app.grossCosts.containerEgressGb)],
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
for (var row of rows.filter(Boolean)) {
|
|
345
|
+
var comp = row[0] ? fmt.bold(row[0].padEnd(14)) : " ".repeat(14);
|
|
346
|
+
var usage = row[1].padEnd(22);
|
|
347
|
+
console.log(` ${comp} ${usage} ${row[2]}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log(fmt.dim(sep));
|
|
351
|
+
console.log(
|
|
352
|
+
` ${" ".repeat(14)} ${"".padEnd(22)} ${fmt.bold(fmtCost(app.grossTotal))}`
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (app.freeTierDiscount > 0) {
|
|
356
|
+
console.log(
|
|
357
|
+
` ${" ".repeat(14)} ${fmt.dim("Free tier".padEnd(22))} ${fmt.dim("-" + fmtCost(app.freeTierDiscount))}`
|
|
358
|
+
);
|
|
359
|
+
console.log(
|
|
360
|
+
` ${" ".repeat(14)} ${fmt.bold("Net".padEnd(22))} ${fmt.bold(fmtCost(app.netTotal))}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(fmt.dim(" Estimates based on Cloudflare Workers Paid plan pricing."));
|
|
366
|
+
console.log("");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- Render fleet ---
|
|
370
|
+
|
|
371
|
+
function renderFleet(fleet, range) {
|
|
372
|
+
console.log("");
|
|
373
|
+
console.log(
|
|
374
|
+
` ${fmt.bold("Estimated costs")} ${fmt.dim(`(${range.label})`)}`
|
|
375
|
+
);
|
|
376
|
+
console.log("");
|
|
377
|
+
|
|
378
|
+
var headers = ["NAME", "WORKERS", "DO", "CONTAINERS", "TOTAL"];
|
|
379
|
+
var rows = fleet.appResults.map((a) => [
|
|
380
|
+
fmt.app(a.name),
|
|
381
|
+
fmtCost(a.workersCost),
|
|
382
|
+
fmtCost(a.doCost),
|
|
383
|
+
fmtCost(a.containerCost),
|
|
384
|
+
fmtCost(a.grossTotal),
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
console.log(table(headers, rows));
|
|
388
|
+
console.log("");
|
|
389
|
+
|
|
390
|
+
var labelW = 44;
|
|
391
|
+
console.log(
|
|
392
|
+
" " + fmt.dim("Subtotal".padEnd(labelW)) + fmtCost(fleet.grossFleetTotal)
|
|
393
|
+
);
|
|
394
|
+
if (fleet.freeTierDiscount > 0) {
|
|
395
|
+
console.log(
|
|
396
|
+
" " +
|
|
397
|
+
fmt.dim("Free tier".padEnd(labelW)) +
|
|
398
|
+
fmt.dim("-" + fmtCost(fleet.freeTierDiscount))
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
console.log(
|
|
402
|
+
" " + fmt.dim("Platform".padEnd(labelW)) + fmtCost(PRICING.platform)
|
|
403
|
+
);
|
|
404
|
+
console.log(" " + fmt.dim("─".repeat(labelW + 8)));
|
|
405
|
+
console.log(
|
|
406
|
+
" " +
|
|
407
|
+
fmt.bold("TOTAL".padEnd(labelW)) +
|
|
408
|
+
fmt.bold(fmtCost(fleet.netFleetTotal + PRICING.platform))
|
|
409
|
+
);
|
|
410
|
+
console.log("");
|
|
411
|
+
console.log(fmt.dim(" Estimates based on Cloudflare Workers Paid plan pricing."));
|
|
412
|
+
console.log("");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// --- Main command ---
|
|
416
|
+
|
|
417
|
+
export async function cost(name, options) {
|
|
418
|
+
var config = getConfig();
|
|
419
|
+
var range = parseDateRange(options.since);
|
|
420
|
+
|
|
421
|
+
// Discover apps and container applications in parallel
|
|
422
|
+
var [allScripts, containerApps] = await Promise.all([
|
|
423
|
+
listWorkerScripts(config),
|
|
424
|
+
listContainerApps(config),
|
|
425
|
+
]);
|
|
426
|
+
var fpScripts = allScripts.filter((s) => s.id.startsWith("flarepilot-"));
|
|
427
|
+
|
|
428
|
+
if (fpScripts.length === 0) {
|
|
429
|
+
fatal(
|
|
430
|
+
"No apps deployed.",
|
|
431
|
+
`Run ${fmt.cmd("flarepilot deploy")} to deploy your first app.`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Build containerAppId lookup: "flarepilot-{name}" → applicationId
|
|
436
|
+
var containerAppMap = {};
|
|
437
|
+
for (var ca of containerApps) {
|
|
438
|
+
containerAppMap[ca.name] = ca.id;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
var singleApp = name ? resolveAppName(name) : null;
|
|
442
|
+
|
|
443
|
+
if (singleApp) {
|
|
444
|
+
if (!fpScripts.find((s) => s.id === `flarepilot-${singleApp}`)) {
|
|
445
|
+
fatal(`App ${fmt.app(singleApp)} not found.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
phase("Fetching analytics");
|
|
450
|
+
|
|
451
|
+
// Build app list with namespace IDs and configs
|
|
452
|
+
var targetScripts = singleApp
|
|
453
|
+
? fpScripts.filter((s) => s.id === `flarepilot-${singleApp}`)
|
|
454
|
+
: fpScripts;
|
|
455
|
+
|
|
456
|
+
var apps = [];
|
|
457
|
+
await Promise.all(
|
|
458
|
+
targetScripts.map(async (s) => {
|
|
459
|
+
var appName = s.id.replace(/^flarepilot-/, "");
|
|
460
|
+
var [nsId, appConfig] = await Promise.all([
|
|
461
|
+
getDONamespaceId(config, s.id, "AppContainer"),
|
|
462
|
+
getAppConfig(config, appName),
|
|
463
|
+
]);
|
|
464
|
+
apps.push({
|
|
465
|
+
name: appName,
|
|
466
|
+
namespaceId: nsId,
|
|
467
|
+
appConfig,
|
|
468
|
+
containerAppId: containerAppMap[s.id] || null,
|
|
469
|
+
});
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
474
|
+
|
|
475
|
+
status(`Querying ${apps.length} app${apps.length > 1 ? "s" : ""}...`);
|
|
476
|
+
|
|
477
|
+
// Fetch all analytics in parallel
|
|
478
|
+
var scriptNames = apps.map((a) => `flarepilot-${a.name}`);
|
|
479
|
+
var namespaceIds = apps.map((a) => a.namespaceId).filter(Boolean);
|
|
480
|
+
var containerAppIds = apps.map((a) => a.containerAppId).filter(Boolean);
|
|
481
|
+
|
|
482
|
+
var queries = [];
|
|
483
|
+
|
|
484
|
+
// 1. Workers
|
|
485
|
+
queries.push(
|
|
486
|
+
cfGraphQL(config, workersGQL, {
|
|
487
|
+
accountTag: config.accountId,
|
|
488
|
+
filter: {
|
|
489
|
+
datetimeHour_geq: range.sinceISO,
|
|
490
|
+
datetimeHour_leq: range.untilISO,
|
|
491
|
+
scriptName_in: scriptNames,
|
|
492
|
+
},
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
// 2-4. DO queries
|
|
497
|
+
if (namespaceIds.length > 0) {
|
|
498
|
+
var doFilter = {
|
|
499
|
+
datetimeHour_geq: range.sinceISO,
|
|
500
|
+
datetimeHour_leq: range.untilISO,
|
|
501
|
+
namespaceId_in: namespaceIds,
|
|
502
|
+
};
|
|
503
|
+
queries.push(
|
|
504
|
+
cfGraphQL(config, doRequestsGQL, { accountTag: config.accountId, filter: doFilter })
|
|
505
|
+
);
|
|
506
|
+
queries.push(
|
|
507
|
+
cfGraphQL(config, doDurationGQL, { accountTag: config.accountId, filter: doFilter })
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
queries.push(Promise.resolve(null), Promise.resolve(null));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 5. Container metrics
|
|
514
|
+
if (containerAppIds.length > 0) {
|
|
515
|
+
queries.push(
|
|
516
|
+
cfGraphQL(config, containersGQL, {
|
|
517
|
+
accountTag: config.accountId,
|
|
518
|
+
filter: {
|
|
519
|
+
datetimeHour_geq: range.sinceISO,
|
|
520
|
+
datetimeHour_leq: range.untilISO,
|
|
521
|
+
applicationId_in: containerAppIds,
|
|
522
|
+
},
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
} else {
|
|
526
|
+
queries.push(Promise.resolve(null));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
var [workersData, doReqData, doDurData, containersData] =
|
|
530
|
+
await Promise.all(queries);
|
|
531
|
+
var analytics = { workersData, doReqData, doDurData, containersData };
|
|
532
|
+
|
|
533
|
+
// Aggregate and calculate costs
|
|
534
|
+
var appResults = aggregateResults(apps, analytics);
|
|
535
|
+
var fleet = applyFreeTier(appResults);
|
|
536
|
+
|
|
537
|
+
// Output
|
|
538
|
+
if (options.json) {
|
|
539
|
+
var jsonOut = singleApp
|
|
540
|
+
? {
|
|
541
|
+
app: fleet.appResults[0].name,
|
|
542
|
+
period: range.label,
|
|
543
|
+
since: range.sinceISO,
|
|
544
|
+
until: range.untilISO,
|
|
545
|
+
usage: fleet.appResults[0].usage,
|
|
546
|
+
costs: fleet.appResults[0].grossCosts,
|
|
547
|
+
grossTotal: fleet.appResults[0].grossTotal,
|
|
548
|
+
freeTierDiscount: fleet.appResults[0].freeTierDiscount,
|
|
549
|
+
netTotal: fleet.appResults[0].netTotal,
|
|
550
|
+
}
|
|
551
|
+
: {
|
|
552
|
+
period: range.label,
|
|
553
|
+
since: range.sinceISO,
|
|
554
|
+
until: range.untilISO,
|
|
555
|
+
apps: fleet.appResults.map((a) => ({
|
|
556
|
+
name: a.name,
|
|
557
|
+
usage: a.usage,
|
|
558
|
+
costs: a.grossCosts,
|
|
559
|
+
grossTotal: a.grossTotal,
|
|
560
|
+
freeTierDiscount: a.freeTierDiscount,
|
|
561
|
+
netTotal: a.netTotal,
|
|
562
|
+
})),
|
|
563
|
+
grossFleetTotal: fleet.grossFleetTotal,
|
|
564
|
+
freeTierDiscount: fleet.freeTierDiscount,
|
|
565
|
+
netFleetTotal: fleet.netFleetTotal,
|
|
566
|
+
platform: PRICING.platform,
|
|
567
|
+
total: fleet.netFleetTotal + PRICING.platform,
|
|
568
|
+
};
|
|
569
|
+
console.log(JSON.stringify(jsonOut, null, 2));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (singleApp) {
|
|
574
|
+
renderSingleApp(fleet.appResults[0], range);
|
|
575
|
+
} else {
|
|
576
|
+
renderFleet(fleet, range);
|
|
577
|
+
}
|
|
578
|
+
}
|
package/src/lib/cf.js
CHANGED
|
@@ -71,6 +71,30 @@ export async function cfApi(method, path, body, apiToken, contentType) {
|
|
|
71
71
|
return res.text();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// --- CF GraphQL Analytics API ---
|
|
75
|
+
|
|
76
|
+
export async function cfGraphQL(config, query, variables) {
|
|
77
|
+
var res = await fetch("https://api.cloudflare.com/client/v4/graphql", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({ query, variables }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
var text = await res.text();
|
|
88
|
+
throw new Error(`CF GraphQL: ${res.status} ${text}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var json = await res.json();
|
|
92
|
+
if (json.errors && json.errors.length > 0) {
|
|
93
|
+
throw new Error(`CF GraphQL: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
return json.data;
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
// --- Registry ---
|
|
75
99
|
|
|
76
100
|
export async function getRegistryCredentials(config) {
|