flarepilot 0.1.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 +32 -0
- package/src/cli.js +223 -0
- package/src/commands/apps.js +139 -0
- package/src/commands/auth.js +91 -0
- package/src/commands/config.js +225 -0
- package/src/commands/deploy.js +289 -0
- package/src/commands/doctor.js +93 -0
- package/src/commands/domains.js +273 -0
- package/src/commands/logs.js +100 -0
- package/src/commands/open.js +48 -0
- package/src/commands/ps.js +86 -0
- package/src/commands/scale.js +158 -0
- package/src/lib/bundle.js +70 -0
- package/src/lib/cf.js +450 -0
- package/src/lib/docker.js +33 -0
- package/src/lib/link.js +33 -0
- package/src/lib/output.js +102 -0
- package/worker-template/package.json +9 -0
- package/worker-template/src/index.js +103 -0
package/src/lib/cf.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
var CONFIG_DIR = join(homedir(), ".flarepilot");
|
|
6
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
9
|
+
var CF_REGISTRY = "registry.cloudflare.com";
|
|
10
|
+
|
|
11
|
+
export { CF_REGISTRY };
|
|
12
|
+
|
|
13
|
+
// --- Auth config (~/.flarepilot/config.json is the only local file) ---
|
|
14
|
+
|
|
15
|
+
export function getConfig() {
|
|
16
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
17
|
+
console.error("Not authenticated. Run `flarepilot auth` first.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function tryGetConfig() {
|
|
24
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveConfig(config) {
|
|
33
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- CF API base ---
|
|
38
|
+
|
|
39
|
+
export async function cfApi(method, path, body, apiToken, contentType) {
|
|
40
|
+
if (!apiToken) {
|
|
41
|
+
var config = getConfig();
|
|
42
|
+
apiToken = config.apiToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var headers = {
|
|
46
|
+
Authorization: `Bearer ${apiToken}`,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (contentType) {
|
|
50
|
+
headers["Content-Type"] = contentType;
|
|
51
|
+
} else if (body && typeof body === "object") {
|
|
52
|
+
headers["Content-Type"] = "application/json";
|
|
53
|
+
body = JSON.stringify(body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var res = await fetch(`${CF_API}${path}`, {
|
|
57
|
+
method,
|
|
58
|
+
headers,
|
|
59
|
+
body: method === "GET" ? undefined : body,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
var text = await res.text();
|
|
64
|
+
throw new Error(`CF API ${method} ${path}: ${res.status} ${text}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var ct = res.headers.get("content-type") || "";
|
|
68
|
+
if (ct.includes("application/json")) {
|
|
69
|
+
return res.json();
|
|
70
|
+
}
|
|
71
|
+
return res.text();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Registry ---
|
|
75
|
+
|
|
76
|
+
export async function getRegistryCredentials(config) {
|
|
77
|
+
var res = await cfApi(
|
|
78
|
+
"POST",
|
|
79
|
+
`/accounts/${config.accountId}/containers/registries/${CF_REGISTRY}/credentials`,
|
|
80
|
+
{ permissions: ["push", "pull"], expiration_minutes: 15 },
|
|
81
|
+
config.apiToken
|
|
82
|
+
);
|
|
83
|
+
return res.result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Worker scripts ---
|
|
87
|
+
|
|
88
|
+
export async function uploadWorker(config, scriptName, code, metadata) {
|
|
89
|
+
var form = new FormData();
|
|
90
|
+
form.append(
|
|
91
|
+
"metadata",
|
|
92
|
+
new Blob([JSON.stringify(metadata)], { type: "application/json" })
|
|
93
|
+
);
|
|
94
|
+
form.append(
|
|
95
|
+
"index.js",
|
|
96
|
+
new Blob([code], { type: "application/javascript+module" }),
|
|
97
|
+
"index.js"
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
var res = await fetch(
|
|
101
|
+
`${CF_API}/accounts/${config.accountId}/workers/scripts/${scriptName}`,
|
|
102
|
+
{
|
|
103
|
+
method: "PUT",
|
|
104
|
+
headers: { Authorization: `Bearer ${config.apiToken}` },
|
|
105
|
+
body: form,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
var text = await res.text();
|
|
111
|
+
throw new Error(`Worker upload failed: ${res.status} ${text}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return res.json();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function deleteWorker(config, scriptName) {
|
|
118
|
+
return cfApi(
|
|
119
|
+
"DELETE",
|
|
120
|
+
`/accounts/${config.accountId}/workers/scripts/${scriptName}`,
|
|
121
|
+
null,
|
|
122
|
+
config.apiToken
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function patchWorkerSettings(config, scriptName, settings) {
|
|
127
|
+
var form = new FormData();
|
|
128
|
+
form.append(
|
|
129
|
+
"settings",
|
|
130
|
+
new Blob([JSON.stringify(settings)], { type: "application/json" })
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
var res = await fetch(
|
|
134
|
+
`${CF_API}/accounts/${config.accountId}/workers/scripts/${scriptName}/settings`,
|
|
135
|
+
{
|
|
136
|
+
method: "PATCH",
|
|
137
|
+
headers: { Authorization: `Bearer ${config.apiToken}` },
|
|
138
|
+
body: form,
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
var text = await res.text();
|
|
144
|
+
throw new Error(`Settings update failed: ${res.status} ${text}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return res.json();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function listWorkerScripts(config) {
|
|
151
|
+
var res = await cfApi(
|
|
152
|
+
"GET",
|
|
153
|
+
`/accounts/${config.accountId}/workers/scripts`,
|
|
154
|
+
null,
|
|
155
|
+
config.apiToken
|
|
156
|
+
);
|
|
157
|
+
return res.result || [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- App config (stored in the deployed worker's FLAREPILOT_APP_CONFIG binding) ---
|
|
161
|
+
|
|
162
|
+
export async function getAppConfig(config, name) {
|
|
163
|
+
var res = await cfApi(
|
|
164
|
+
"GET",
|
|
165
|
+
`/accounts/${config.accountId}/workers/scripts/flarepilot-${name}/settings`,
|
|
166
|
+
null,
|
|
167
|
+
config.apiToken
|
|
168
|
+
);
|
|
169
|
+
var bindings = res.result?.bindings || [];
|
|
170
|
+
var binding = bindings.find((b) => b.name === "FLAREPILOT_APP_CONFIG");
|
|
171
|
+
if (!binding) return null;
|
|
172
|
+
return JSON.parse(binding.text);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function pushAppConfig(config, name, appConfig) {
|
|
176
|
+
var { getWorkerBundle } = await import("./bundle.js");
|
|
177
|
+
var code = getWorkerBundle();
|
|
178
|
+
var metadata = buildWorkerMetadata(appConfig, { firstDeploy: false });
|
|
179
|
+
await uploadWorker(config, `flarepilot-${name}`, code, metadata);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Metadata builder (full deploy) ---
|
|
183
|
+
|
|
184
|
+
export function buildWorkerMetadata(appConfig, { firstDeploy = false } = {}) {
|
|
185
|
+
var metadata = {
|
|
186
|
+
main_module: "index.js",
|
|
187
|
+
compatibility_date: "2025-10-08",
|
|
188
|
+
bindings: [
|
|
189
|
+
{
|
|
190
|
+
type: "durable_object_namespace",
|
|
191
|
+
name: "APP_CONTAINER",
|
|
192
|
+
class_name: "AppContainer",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: "plain_text",
|
|
196
|
+
name: "FLAREPILOT_APP_CONFIG",
|
|
197
|
+
text: JSON.stringify(appConfig),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
observability: {
|
|
201
|
+
enabled: appConfig.observability !== false,
|
|
202
|
+
},
|
|
203
|
+
containers: [
|
|
204
|
+
{
|
|
205
|
+
class_name: "AppContainer",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (firstDeploy) {
|
|
211
|
+
metadata.migrations = {
|
|
212
|
+
new_tag: "v1",
|
|
213
|
+
new_sqlite_classes: ["AppContainer"],
|
|
214
|
+
};
|
|
215
|
+
} else {
|
|
216
|
+
metadata.migrations = {
|
|
217
|
+
old_tag: "v1",
|
|
218
|
+
new_tag: "v1",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return metadata;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Container applications ---
|
|
226
|
+
|
|
227
|
+
export async function getDONamespaceId(config, scriptName, className) {
|
|
228
|
+
var res = await cfApi(
|
|
229
|
+
"GET",
|
|
230
|
+
`/accounts/${config.accountId}/workers/durable_objects/namespaces`,
|
|
231
|
+
null,
|
|
232
|
+
config.apiToken
|
|
233
|
+
);
|
|
234
|
+
var namespaces = res.result || [];
|
|
235
|
+
var ns = namespaces.find(
|
|
236
|
+
(n) => n.script === scriptName && n.class === className
|
|
237
|
+
);
|
|
238
|
+
return ns ? ns.id : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function listContainerApps(config) {
|
|
242
|
+
var res = await cfApi(
|
|
243
|
+
"GET",
|
|
244
|
+
`/accounts/${config.accountId}/containers/applications`,
|
|
245
|
+
null,
|
|
246
|
+
config.apiToken
|
|
247
|
+
);
|
|
248
|
+
return res.result || [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function findContainerApp(config, name) {
|
|
252
|
+
var apps = await listContainerApps(config);
|
|
253
|
+
return apps.find((a) => a.name === name) || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function createContainerApp(config, app) {
|
|
257
|
+
var res = await cfApi(
|
|
258
|
+
"POST",
|
|
259
|
+
`/accounts/${config.accountId}/containers/applications`,
|
|
260
|
+
app,
|
|
261
|
+
config.apiToken
|
|
262
|
+
);
|
|
263
|
+
return res.result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function deleteContainerApp(config, appId) {
|
|
267
|
+
return cfApi(
|
|
268
|
+
"DELETE",
|
|
269
|
+
`/accounts/${config.accountId}/containers/applications/${appId}`,
|
|
270
|
+
null,
|
|
271
|
+
config.apiToken
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function modifyContainerApp(config, appId, changes) {
|
|
276
|
+
var res = await cfApi(
|
|
277
|
+
"PATCH",
|
|
278
|
+
`/accounts/${config.accountId}/containers/applications/${appId}`,
|
|
279
|
+
changes,
|
|
280
|
+
config.apiToken
|
|
281
|
+
);
|
|
282
|
+
return res.result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function createRollout(config, appId, rollout) {
|
|
286
|
+
var res = await cfApi(
|
|
287
|
+
"POST",
|
|
288
|
+
`/accounts/${config.accountId}/containers/applications/${appId}/rollouts`,
|
|
289
|
+
rollout,
|
|
290
|
+
config.apiToken
|
|
291
|
+
);
|
|
292
|
+
return res.result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Tail/logs ---
|
|
296
|
+
|
|
297
|
+
export async function createTail(config, scriptName) {
|
|
298
|
+
var res = await cfApi(
|
|
299
|
+
"POST",
|
|
300
|
+
`/accounts/${config.accountId}/workers/scripts/${scriptName}/tails`,
|
|
301
|
+
{},
|
|
302
|
+
config.apiToken
|
|
303
|
+
);
|
|
304
|
+
return res.result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function deleteTail(config, scriptName, tailId) {
|
|
308
|
+
return cfApi(
|
|
309
|
+
"DELETE",
|
|
310
|
+
`/accounts/${config.accountId}/workers/scripts/${scriptName}/tails/${tailId}`,
|
|
311
|
+
null,
|
|
312
|
+
config.apiToken
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- Zones ---
|
|
317
|
+
|
|
318
|
+
export async function listZones(config) {
|
|
319
|
+
var all = [];
|
|
320
|
+
var page = 1;
|
|
321
|
+
while (true) {
|
|
322
|
+
var res = await cfApi(
|
|
323
|
+
"GET",
|
|
324
|
+
`/zones?account.id=${config.accountId}&per_page=50&status=active&page=${page}`,
|
|
325
|
+
null,
|
|
326
|
+
config.apiToken
|
|
327
|
+
);
|
|
328
|
+
var zones = res.result || [];
|
|
329
|
+
all.push(...zones);
|
|
330
|
+
if (zones.length < 50) break;
|
|
331
|
+
page++;
|
|
332
|
+
}
|
|
333
|
+
return all;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function findZoneForHostname(zones, hostname) {
|
|
337
|
+
// Find the zone whose name is a suffix of the hostname (longest match wins)
|
|
338
|
+
var match = null;
|
|
339
|
+
for (var zone of zones) {
|
|
340
|
+
if (hostname === zone.name || hostname.endsWith("." + zone.name)) {
|
|
341
|
+
if (!match || zone.name.length > match.name.length) {
|
|
342
|
+
match = zone;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return match;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- DNS records ---
|
|
350
|
+
|
|
351
|
+
export async function listDnsRecords(config, zoneId, params) {
|
|
352
|
+
var qs = new URLSearchParams(params || {}).toString();
|
|
353
|
+
var path = `/zones/${zoneId}/dns_records${qs ? "?" + qs : ""}`;
|
|
354
|
+
var res = await cfApi("GET", path, null, config.apiToken);
|
|
355
|
+
return res.result || [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function createDnsRecord(config, zoneId, record) {
|
|
359
|
+
return cfApi(
|
|
360
|
+
"POST",
|
|
361
|
+
`/zones/${zoneId}/dns_records`,
|
|
362
|
+
record,
|
|
363
|
+
config.apiToken
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function deleteDnsRecord(config, zoneId, recordId) {
|
|
368
|
+
return cfApi(
|
|
369
|
+
"DELETE",
|
|
370
|
+
`/zones/${zoneId}/dns_records/${recordId}`,
|
|
371
|
+
null,
|
|
372
|
+
config.apiToken
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- Workers custom domains ---
|
|
377
|
+
|
|
378
|
+
export async function addWorkerDomain(config, scriptName, hostname, zoneId) {
|
|
379
|
+
return cfApi(
|
|
380
|
+
"PUT",
|
|
381
|
+
`/accounts/${config.accountId}/workers/domains`,
|
|
382
|
+
{
|
|
383
|
+
hostname,
|
|
384
|
+
zone_id: zoneId,
|
|
385
|
+
service: scriptName,
|
|
386
|
+
environment: "production",
|
|
387
|
+
},
|
|
388
|
+
config.apiToken
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function removeWorkerDomain(config, hostname) {
|
|
393
|
+
// Find the domain ID by hostname
|
|
394
|
+
var res = await cfApi(
|
|
395
|
+
"GET",
|
|
396
|
+
`/accounts/${config.accountId}/workers/domains`,
|
|
397
|
+
null,
|
|
398
|
+
config.apiToken
|
|
399
|
+
);
|
|
400
|
+
var domains = res.result || [];
|
|
401
|
+
var domain = domains.find((d) => d.hostname === hostname);
|
|
402
|
+
if (!domain) return;
|
|
403
|
+
|
|
404
|
+
return cfApi(
|
|
405
|
+
"DELETE",
|
|
406
|
+
`/accounts/${config.accountId}/workers/domains/${domain.id}`,
|
|
407
|
+
null,
|
|
408
|
+
config.apiToken
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function listWorkerDomainsForService(config, scriptName) {
|
|
413
|
+
var res = await cfApi(
|
|
414
|
+
"GET",
|
|
415
|
+
`/accounts/${config.accountId}/workers/domains`,
|
|
416
|
+
null,
|
|
417
|
+
config.apiToken
|
|
418
|
+
);
|
|
419
|
+
return (res.result || []).filter((d) => d.service === scriptName);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- Workers subdomain ---
|
|
423
|
+
|
|
424
|
+
export async function getWorkersSubdomain(config) {
|
|
425
|
+
try {
|
|
426
|
+
var res = await cfApi(
|
|
427
|
+
"GET",
|
|
428
|
+
`/accounts/${config.accountId}/workers/subdomain`,
|
|
429
|
+
null,
|
|
430
|
+
config.apiToken
|
|
431
|
+
);
|
|
432
|
+
return res.result?.subdomain || null;
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function enableWorkerSubdomain(config, scriptName) {
|
|
439
|
+
return cfApi(
|
|
440
|
+
"POST",
|
|
441
|
+
`/accounts/${config.accountId}/workers/services/${scriptName}/environments/production/subdomain`,
|
|
442
|
+
{ enabled: true },
|
|
443
|
+
config.apiToken
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function getAppUrl(subdomain, name) {
|
|
448
|
+
if (!subdomain) return null;
|
|
449
|
+
return `https://flarepilot-${name}.${subdomain}.workers.dev`;
|
|
450
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
function ensureDocker() {
|
|
4
|
+
try {
|
|
5
|
+
execSync("docker version", { stdio: "pipe" });
|
|
6
|
+
} catch {
|
|
7
|
+
console.error("Docker is not running. Install and start Docker first.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dockerBuild(contextPath, tag) {
|
|
13
|
+
ensureDocker();
|
|
14
|
+
execSync(
|
|
15
|
+
`docker build --platform linux/amd64 --provenance=false -t ${tag} ${contextPath}`,
|
|
16
|
+
{ stdio: "pipe" }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function dockerTag(source, target) {
|
|
21
|
+
execSync(`docker tag ${source} ${target}`, { stdio: "pipe" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dockerPush(tag) {
|
|
25
|
+
execSync(`docker push ${tag}`, { stdio: "pipe" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function dockerLogin(registry, username, password) {
|
|
29
|
+
execSync(
|
|
30
|
+
`docker login --password-stdin --username ${username} ${registry}`,
|
|
31
|
+
{ input: password, stdio: "pipe" }
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/lib/link.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { fatal, fmt } from "./output.js";
|
|
3
|
+
|
|
4
|
+
var LINK_FILE = ".flarepilot.json";
|
|
5
|
+
|
|
6
|
+
export function readLink() {
|
|
7
|
+
try {
|
|
8
|
+
var data = JSON.parse(readFileSync(LINK_FILE, "utf-8"));
|
|
9
|
+
return data.app || null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function linkApp(name) {
|
|
16
|
+
writeFileSync(LINK_FILE, JSON.stringify({ app: name }) + "\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function unlinkApp() {
|
|
20
|
+
try {
|
|
21
|
+
unlinkSync(LINK_FILE);
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveAppName(name) {
|
|
26
|
+
if (name) return name;
|
|
27
|
+
var linked = readLink();
|
|
28
|
+
if (linked) return linked;
|
|
29
|
+
fatal(
|
|
30
|
+
"No app specified.",
|
|
31
|
+
`Provide an app name or run ${fmt.cmd("flarepilot deploy")} in this directory first.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
|
|
3
|
+
export function phase(msg) {
|
|
4
|
+
process.stderr.write(`\n${kleur.bold().cyan("==>")} ${kleur.bold(msg)}\n`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function status(msg) {
|
|
8
|
+
process.stderr.write(` ${msg}\n`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function error(msg, fix) {
|
|
12
|
+
process.stderr.write(`\n${kleur.red("Error:")} ${msg}\n`);
|
|
13
|
+
if (fix) process.stderr.write(` ${fix}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fatal(msg, fix) {
|
|
17
|
+
error(msg, fix);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function success(msg) {
|
|
22
|
+
process.stderr.write(`\n${kleur.green("-->")} ${msg}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hint(label, msg) {
|
|
26
|
+
process.stderr.write(`\n${kleur.dim(label + ":")} ${msg}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export var fmt = {
|
|
30
|
+
app: (name) => kleur.cyan(name),
|
|
31
|
+
cmd: (cmd) => kleur.bold().cyan(cmd),
|
|
32
|
+
key: (k) => kleur.green(k),
|
|
33
|
+
val: (v) => kleur.yellow(v),
|
|
34
|
+
dim: (t) => kleur.dim(t),
|
|
35
|
+
bold: (t) => kleur.bold(t),
|
|
36
|
+
url: (u) => kleur.underline().cyan(u),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function stripAnsi(str) {
|
|
40
|
+
return String(str).replace(/\x1B\[[0-9;]*m/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function padEnd(str, len) {
|
|
44
|
+
var visible = stripAnsi(str).length;
|
|
45
|
+
return str + " ".repeat(Math.max(0, len - visible));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var ADJECTIVES = [
|
|
49
|
+
"autumn", "bold", "calm", "crimson", "dawn", "dark", "dry", "dusk",
|
|
50
|
+
"fading", "flat", "floral", "fragrant", "frosty", "gentle", "green",
|
|
51
|
+
"hazy", "hidden", "icy", "lively", "long", "misty", "morning", "muddy",
|
|
52
|
+
"nameless", "old", "patient", "plain", "polished", "proud", "purple",
|
|
53
|
+
"quiet", "rapid", "red", "restless", "rough", "shy", "silent", "small",
|
|
54
|
+
"snowy", "solitary", "sparkling", "spring", "still", "summer", "twilight",
|
|
55
|
+
"wandering", "weathered", "white", "wild", "winter", "withered", "young",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
var NOUNS = [
|
|
59
|
+
"bird", "brook", "bush", "cloud", "creek", "dew", "dream", "dust",
|
|
60
|
+
"field", "fire", "flower", "fog", "forest", "frost", "gale", "gate",
|
|
61
|
+
"glacier", "grass", "grove", "haze", "hill", "lake", "leaf", "light",
|
|
62
|
+
"meadow", "moon", "moss", "night", "paper", "path", "peak", "pine",
|
|
63
|
+
"pond", "rain", "reef", "ridge", "river", "rock", "rose", "sea",
|
|
64
|
+
"shadow", "shore", "sky", "smoke", "snow", "sound", "star", "stone",
|
|
65
|
+
"stream", "sun", "surf", "thunder", "tree", "violet", "water", "wave",
|
|
66
|
+
"wind", "wood",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function generateAppName() {
|
|
70
|
+
var adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
71
|
+
var noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
72
|
+
var num = Math.floor(1000 + Math.random() * 9000);
|
|
73
|
+
return `${adj}-${noun}-${num}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function table(headers, rows) {
|
|
77
|
+
if (rows.length === 0) return "";
|
|
78
|
+
|
|
79
|
+
var allRows = [headers, ...rows];
|
|
80
|
+
var widths = [];
|
|
81
|
+
for (var row of allRows) {
|
|
82
|
+
for (var i = 0; i < row.length; i++) {
|
|
83
|
+
var len = stripAnsi(String(row[i] || "")).length;
|
|
84
|
+
if (!widths[i] || len > widths[i]) widths[i] = len;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var lines = [];
|
|
89
|
+
lines.push(
|
|
90
|
+
headers
|
|
91
|
+
.map((h, i) => kleur.bold(padEnd(String(h), widths[i] + 2)))
|
|
92
|
+
.join("")
|
|
93
|
+
);
|
|
94
|
+
lines.push(kleur.dim(widths.map((w) => "─".repeat(w)).join(" ")));
|
|
95
|
+
for (var row of rows) {
|
|
96
|
+
lines.push(
|
|
97
|
+
row.map((cell, i) => padEnd(String(cell || ""), widths[i] + 2)).join("")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|