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/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flarepilot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Heroku/Fly.io-style deployments on Cloudflare Containers",
|
|
5
|
+
"bin": {
|
|
6
|
+
"flarepilot": "./src/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"worker-template"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cloudflare",
|
|
16
|
+
"containers",
|
|
17
|
+
"docker",
|
|
18
|
+
"deploy",
|
|
19
|
+
"paas",
|
|
20
|
+
"heroku",
|
|
21
|
+
"flyio"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/michaloo/flarepilot"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^13.0.0",
|
|
29
|
+
"esbuild": "^0.25.0",
|
|
30
|
+
"kleur": "^4.1.5"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { auth } from "./commands/auth.js";
|
|
5
|
+
import { appsList, appsInfo, appsDestroy } from "./commands/apps.js";
|
|
6
|
+
import { deploy } from "./commands/deploy.js";
|
|
7
|
+
import {
|
|
8
|
+
configShow,
|
|
9
|
+
configSet,
|
|
10
|
+
configGet,
|
|
11
|
+
configUnset,
|
|
12
|
+
configImport,
|
|
13
|
+
} from "./commands/config.js";
|
|
14
|
+
import { scale } from "./commands/scale.js";
|
|
15
|
+
import { domainsList, domainsAdd, domainsRemove } from "./commands/domains.js";
|
|
16
|
+
import { ps } from "./commands/ps.js";
|
|
17
|
+
import { logs } from "./commands/logs.js";
|
|
18
|
+
import { open } from "./commands/open.js";
|
|
19
|
+
import { doctor } from "./commands/doctor.js";
|
|
20
|
+
import { fmt } from "./lib/output.js";
|
|
21
|
+
|
|
22
|
+
var program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name("flarepilot")
|
|
26
|
+
.description("Deploy and manage apps on Cloudflare Containers")
|
|
27
|
+
.version("0.1.0");
|
|
28
|
+
|
|
29
|
+
// --- Auth ---
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command("auth")
|
|
33
|
+
.description("Authenticate with your Cloudflare API token")
|
|
34
|
+
.action(auth);
|
|
35
|
+
|
|
36
|
+
// --- Deploy ---
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("deploy [name] [path]")
|
|
40
|
+
.description("Deploy an app from a Dockerfile (name auto-generated if omitted)")
|
|
41
|
+
.option("-t, --tag <tag>", "Image tag (default: deploy-<timestamp>)")
|
|
42
|
+
.option("-e, --env <vars...>", "Set env vars (KEY=VALUE)")
|
|
43
|
+
.option(
|
|
44
|
+
"--regions <hints>",
|
|
45
|
+
"Comma-separated location hints (wnam,enam,sam,weur,eeur,apac,oc,afr,me)"
|
|
46
|
+
)
|
|
47
|
+
.option("-i, --instances <n>", "Instances per region", parseInt)
|
|
48
|
+
.option("--port <port>", "Container port", parseInt)
|
|
49
|
+
.option("--sleep <duration>", "Sleep after idle (e.g. 5m, 30s, never)", "30s")
|
|
50
|
+
.option("--instance-type <type>", "Instance type (lite, base, standard, large)")
|
|
51
|
+
.option("--vcpu <n>", "vCPU allocation (e.g. 0.0625, 0.5, 1, 2)", parseFloat)
|
|
52
|
+
.option("--memory <mb>", "Memory in MiB (e.g. 256, 512, 1024)", parseInt)
|
|
53
|
+
.option("--disk <mb>", "Disk in MB (e.g. 2000, 5000)", parseInt)
|
|
54
|
+
.option("--no-observability", "Disable Workers observability/logs")
|
|
55
|
+
.option("--json", "Output result as JSON")
|
|
56
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
57
|
+
.action(deploy);
|
|
58
|
+
|
|
59
|
+
// --- Apps (topic root = list) ---
|
|
60
|
+
|
|
61
|
+
var apps = program.command("apps").description("Manage apps");
|
|
62
|
+
|
|
63
|
+
apps
|
|
64
|
+
.command("list", { isDefault: true })
|
|
65
|
+
.description("List all deployed apps")
|
|
66
|
+
.option("--json", "Output as JSON")
|
|
67
|
+
.action(appsList);
|
|
68
|
+
|
|
69
|
+
apps
|
|
70
|
+
.command("info [name]")
|
|
71
|
+
.description("Show detailed app information")
|
|
72
|
+
.option("--json", "Output as JSON")
|
|
73
|
+
.action(appsInfo);
|
|
74
|
+
|
|
75
|
+
apps
|
|
76
|
+
.command("destroy [name]")
|
|
77
|
+
.description("Destroy an app and its resources")
|
|
78
|
+
.option("--confirm <name>", "Confirm by providing the app name")
|
|
79
|
+
.action(appsDestroy);
|
|
80
|
+
|
|
81
|
+
// --- Config (topic root = show) ---
|
|
82
|
+
|
|
83
|
+
var configCmd = program
|
|
84
|
+
.command("config")
|
|
85
|
+
.description("Manage app config/env vars");
|
|
86
|
+
|
|
87
|
+
configCmd
|
|
88
|
+
.command("show [name]", { isDefault: true })
|
|
89
|
+
.description("Show all env vars for an app")
|
|
90
|
+
.option("--json", "Output as JSON")
|
|
91
|
+
.action(configShow);
|
|
92
|
+
|
|
93
|
+
configCmd
|
|
94
|
+
.command("set <args...>")
|
|
95
|
+
.description("Set env vars ([name] KEY=VALUE ...) — applies live")
|
|
96
|
+
.action(configSet);
|
|
97
|
+
|
|
98
|
+
configCmd
|
|
99
|
+
.command("get <args...>")
|
|
100
|
+
.description("Get a single env var value ([name] KEY)")
|
|
101
|
+
.action(configGet);
|
|
102
|
+
|
|
103
|
+
configCmd
|
|
104
|
+
.command("unset <args...>")
|
|
105
|
+
.description("Remove env vars ([name] KEY ...) — applies live")
|
|
106
|
+
.action(configUnset);
|
|
107
|
+
|
|
108
|
+
configCmd
|
|
109
|
+
.command("import [name]")
|
|
110
|
+
.description("Import env vars from .env file or stdin")
|
|
111
|
+
.option("-f, --file <path>", "Path to .env file")
|
|
112
|
+
.action(configImport);
|
|
113
|
+
|
|
114
|
+
// --- Scale ---
|
|
115
|
+
|
|
116
|
+
program
|
|
117
|
+
.command("scale [name]")
|
|
118
|
+
.description("Show or adjust app scaling")
|
|
119
|
+
.option(
|
|
120
|
+
"-r, --regions <hints>",
|
|
121
|
+
"Comma-separated location hints (wnam,enam,sam,weur,eeur,apac,oc,afr,me)"
|
|
122
|
+
)
|
|
123
|
+
.option("-i, --instances <n>", "Instances per region", parseInt)
|
|
124
|
+
.option("--instance-type <type>", "Instance type (lite, base, standard, large)")
|
|
125
|
+
.option("--vcpu <n>", "vCPU allocation (e.g. 0.0625, 0.5, 1, 2)", parseFloat)
|
|
126
|
+
.option("--memory <mb>", "Memory in MiB (e.g. 256, 512, 1024)", parseInt)
|
|
127
|
+
.option("--disk <mb>", "Disk in MB (e.g. 2000, 5000)", parseInt)
|
|
128
|
+
.option("--json", "Output as JSON")
|
|
129
|
+
.action(scale);
|
|
130
|
+
|
|
131
|
+
// --- Domains (topic root = list) ---
|
|
132
|
+
|
|
133
|
+
var domainsCmd = program
|
|
134
|
+
.command("domains")
|
|
135
|
+
.description("Manage custom domains");
|
|
136
|
+
|
|
137
|
+
domainsCmd
|
|
138
|
+
.command("list [name]", { isDefault: true })
|
|
139
|
+
.description("List custom domains for an app")
|
|
140
|
+
.option("--json", "Output as JSON")
|
|
141
|
+
.action(domainsList);
|
|
142
|
+
|
|
143
|
+
domainsCmd
|
|
144
|
+
.command("add [args...]")
|
|
145
|
+
.description("Add a custom domain (interactive if no domain given)")
|
|
146
|
+
.action(domainsAdd);
|
|
147
|
+
|
|
148
|
+
domainsCmd
|
|
149
|
+
.command("remove <args...>")
|
|
150
|
+
.description("Remove a custom domain ([name] domain) — applies live")
|
|
151
|
+
.action(domainsRemove);
|
|
152
|
+
|
|
153
|
+
// --- PS ---
|
|
154
|
+
|
|
155
|
+
program
|
|
156
|
+
.command("ps [name]")
|
|
157
|
+
.description("Show app containers and status")
|
|
158
|
+
.option("--json", "Output as JSON")
|
|
159
|
+
.action(ps);
|
|
160
|
+
|
|
161
|
+
// --- Logs ---
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command("logs [name]")
|
|
165
|
+
.description("Stream live logs from an app")
|
|
166
|
+
.action(logs);
|
|
167
|
+
|
|
168
|
+
// --- Open ---
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command("open [name]")
|
|
172
|
+
.description("Open app in browser")
|
|
173
|
+
.action(open);
|
|
174
|
+
|
|
175
|
+
// --- Regions ---
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command("regions")
|
|
179
|
+
.description("List available deployment regions")
|
|
180
|
+
.option("--json", "Output as JSON")
|
|
181
|
+
.action(function (options) {
|
|
182
|
+
var regions = [
|
|
183
|
+
{ code: "wnam", name: "Western North America", location: "Los Angeles, Seattle, San Francisco" },
|
|
184
|
+
{ code: "enam", name: "Eastern North America", location: "New York, Chicago, Toronto" },
|
|
185
|
+
{ code: "sam", name: "South America", location: "São Paulo, Buenos Aires" },
|
|
186
|
+
{ code: "weur", name: "Western Europe", location: "London, Paris, Amsterdam, Frankfurt" },
|
|
187
|
+
{ code: "eeur", name: "Eastern Europe", location: "Warsaw, Helsinki, Bucharest" },
|
|
188
|
+
{ code: "apac", name: "Asia Pacific", location: "Tokyo, Singapore, Hong Kong, Mumbai" },
|
|
189
|
+
{ code: "oc", name: "Oceania", location: "Sydney, Auckland" },
|
|
190
|
+
{ code: "afr", name: "Africa", location: "Johannesburg, Nairobi" },
|
|
191
|
+
{ code: "me", name: "Middle East", location: "Dubai, Bahrain" },
|
|
192
|
+
];
|
|
193
|
+
if (options.json) {
|
|
194
|
+
console.log(JSON.stringify(regions, null, 2));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.log("");
|
|
198
|
+
for (var r of regions) {
|
|
199
|
+
console.log(` ${fmt.bold(r.code.padEnd(6))} ${r.name.padEnd(25)} ${fmt.dim(r.location)}`);
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(fmt.dim(" These are Durable Object locationHints. Cloudflare will attempt"));
|
|
203
|
+
console.log(fmt.dim(" to place containers near the specified region but exact placement"));
|
|
204
|
+
console.log(fmt.dim(" is not guaranteed."));
|
|
205
|
+
console.log("");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// --- Doctor ---
|
|
209
|
+
|
|
210
|
+
program
|
|
211
|
+
.command("doctor")
|
|
212
|
+
.description("Check system setup and connectivity")
|
|
213
|
+
.action(doctor);
|
|
214
|
+
|
|
215
|
+
// --- Top-level aliases ---
|
|
216
|
+
|
|
217
|
+
program
|
|
218
|
+
.command("destroy [name]")
|
|
219
|
+
.description("Destroy an app (alias for apps destroy)")
|
|
220
|
+
.option("--confirm <name>", "Confirm by providing the app name")
|
|
221
|
+
.action(appsDestroy);
|
|
222
|
+
|
|
223
|
+
program.parse();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfig,
|
|
3
|
+
getAppConfig,
|
|
4
|
+
deleteWorker,
|
|
5
|
+
listWorkerScripts,
|
|
6
|
+
getWorkersSubdomain,
|
|
7
|
+
findContainerApp,
|
|
8
|
+
deleteContainerApp,
|
|
9
|
+
} from "../lib/cf.js";
|
|
10
|
+
import { success, fatal, hint, fmt, table } from "../lib/output.js";
|
|
11
|
+
import { resolveAppName, readLink, unlinkApp } from "../lib/link.js";
|
|
12
|
+
import { createInterface } from "readline";
|
|
13
|
+
|
|
14
|
+
export async function appsList(options) {
|
|
15
|
+
var config = getConfig();
|
|
16
|
+
var scripts = await listWorkerScripts(config);
|
|
17
|
+
var apps = scripts.filter((s) => s.id.startsWith("flarepilot-"));
|
|
18
|
+
|
|
19
|
+
if (apps.length === 0) {
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log("[]");
|
|
22
|
+
} else {
|
|
23
|
+
process.stderr.write("No apps deployed.\n");
|
|
24
|
+
hint("Next", "flarepilot deploy");
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var data = apps.map((s) => ({
|
|
30
|
+
name: s.id.replace("flarepilot-", ""),
|
|
31
|
+
modified: s.modified_on || null,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
if (options.json) {
|
|
35
|
+
console.log(JSON.stringify(data, null, 2));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var rows = data.map((a) => [
|
|
40
|
+
fmt.app(a.name),
|
|
41
|
+
a.modified ? new Date(a.modified).toISOString() : "—",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
console.log(table(["NAME", "LAST MODIFIED"], rows));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function appsInfo(name, options) {
|
|
48
|
+
name = resolveAppName(name);
|
|
49
|
+
var config = getConfig();
|
|
50
|
+
var appConfig = await getAppConfig(config, name);
|
|
51
|
+
|
|
52
|
+
if (!appConfig) {
|
|
53
|
+
fatal(
|
|
54
|
+
`App ${fmt.app(name)} not found.`,
|
|
55
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.json) {
|
|
60
|
+
console.log(JSON.stringify(appConfig, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var subdomain = await getWorkersSubdomain(config);
|
|
65
|
+
var url = subdomain
|
|
66
|
+
? `https://flarepilot-${name}.${subdomain}.workers.dev`
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(`${fmt.bold("App:")} ${fmt.app(name)}`);
|
|
71
|
+
if (url) console.log(`${fmt.bold("URL:")} ${fmt.url(url)}`);
|
|
72
|
+
console.log(
|
|
73
|
+
`${fmt.bold("Image:")} ${appConfig.image || fmt.dim("(not deployed)")}`
|
|
74
|
+
);
|
|
75
|
+
console.log(`${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}`);
|
|
76
|
+
console.log(`${fmt.bold("Instances:")} ${appConfig.instances} per region`);
|
|
77
|
+
console.log(`${fmt.bold("Port:")} ${appConfig.port}`);
|
|
78
|
+
console.log(
|
|
79
|
+
`${fmt.bold("Domains:")} ${(appConfig.domains || []).join(", ") || fmt.dim("(none)")}`
|
|
80
|
+
);
|
|
81
|
+
console.log(
|
|
82
|
+
`${fmt.bold("Env vars:")} ${Object.keys(appConfig.env || {}).length}`
|
|
83
|
+
);
|
|
84
|
+
if (appConfig.deployedAt) {
|
|
85
|
+
console.log(`${fmt.bold("Deployed:")} ${appConfig.deployedAt}`);
|
|
86
|
+
}
|
|
87
|
+
if (appConfig.createdAt) {
|
|
88
|
+
console.log(`${fmt.bold("Created:")} ${appConfig.createdAt}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function appsDestroy(name, options) {
|
|
93
|
+
name = resolveAppName(name);
|
|
94
|
+
if (options.confirm !== name) {
|
|
95
|
+
if (process.stdin.isTTY) {
|
|
96
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
97
|
+
var answer = await new Promise((resolve) =>
|
|
98
|
+
rl.question(`Type "${name}" to confirm destruction: `, resolve)
|
|
99
|
+
);
|
|
100
|
+
rl.close();
|
|
101
|
+
if (answer.trim() !== name) {
|
|
102
|
+
fatal("Confirmation did not match. Aborting.");
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
fatal(
|
|
106
|
+
`Destroying ${fmt.app(name)} requires confirmation.`,
|
|
107
|
+
`Run: flarepilot apps destroy ${name} --confirm ${name}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var config = getConfig();
|
|
113
|
+
var scriptName = `flarepilot-${name}`;
|
|
114
|
+
|
|
115
|
+
// Delete container application first (before worker, since it references the DO namespace)
|
|
116
|
+
process.stderr.write(`Deleting container application...\n`);
|
|
117
|
+
try {
|
|
118
|
+
var containerApp = await findContainerApp(config, scriptName);
|
|
119
|
+
if (containerApp) {
|
|
120
|
+
await deleteContainerApp(config, containerApp.id);
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
process.stderr.write(` ${fmt.dim(`Warning: ${e.message}`)}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.stderr.write(`Deleting worker ${scriptName}...\n`);
|
|
127
|
+
try {
|
|
128
|
+
await deleteWorker(config, scriptName);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
fatal(`Could not delete ${fmt.app(name)}.`, e.message);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Remove .flarepilot.json if it points to this app
|
|
134
|
+
if (readLink() === name) {
|
|
135
|
+
unlinkApp();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
success(`App ${fmt.app(name)} destroyed.`);
|
|
139
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { cfApi, saveConfig } from "../lib/cf.js";
|
|
3
|
+
import { success, fatal, hint, fmt } from "../lib/output.js";
|
|
4
|
+
import kleur from "kleur";
|
|
5
|
+
|
|
6
|
+
var TOKEN_URL =
|
|
7
|
+
"https://dash.cloudflare.com/profile/api-tokens?" +
|
|
8
|
+
"permissionGroupKeys=" +
|
|
9
|
+
encodeURIComponent(
|
|
10
|
+
JSON.stringify([
|
|
11
|
+
{ key: "workers_scripts", type: "edit" },
|
|
12
|
+
{ key: "containers", type: "edit" },
|
|
13
|
+
{ key: "zone", type: "read" },
|
|
14
|
+
{ key: "dns", type: "edit" },
|
|
15
|
+
])
|
|
16
|
+
) +
|
|
17
|
+
"&name=flarepilot-cli";
|
|
18
|
+
|
|
19
|
+
function prompt(rl, question) {
|
|
20
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function auth() {
|
|
24
|
+
process.stderr.write(`\n${kleur.bold("Authenticate with Cloudflare")}\n\n`);
|
|
25
|
+
process.stderr.write("Create an API token:\n");
|
|
26
|
+
process.stderr.write(` ${fmt.url(TOKEN_URL)}\n\n`);
|
|
27
|
+
|
|
28
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
29
|
+
var apiToken = await prompt(rl, "Paste your API token: ");
|
|
30
|
+
|
|
31
|
+
apiToken = (apiToken || "").trim();
|
|
32
|
+
if (!apiToken) {
|
|
33
|
+
rl.close();
|
|
34
|
+
fatal("No token provided.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Verify token
|
|
38
|
+
process.stderr.write("\nVerifying...\n");
|
|
39
|
+
try {
|
|
40
|
+
await cfApi("GET", "/user/tokens/verify", null, apiToken);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
rl.close();
|
|
43
|
+
fatal("Token verification failed.", e.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get accounts
|
|
47
|
+
var accounts = await cfApi("GET", "/accounts", null, apiToken);
|
|
48
|
+
if (!accounts.result || accounts.result.length === 0) {
|
|
49
|
+
rl.close();
|
|
50
|
+
fatal("No accounts found for this token.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var account;
|
|
54
|
+
|
|
55
|
+
if (accounts.result.length === 1) {
|
|
56
|
+
account = accounts.result[0];
|
|
57
|
+
} else {
|
|
58
|
+
// Multiple accounts — let user pick
|
|
59
|
+
process.stderr.write("\nMultiple accounts found:\n\n");
|
|
60
|
+
for (var i = 0; i < accounts.result.length; i++) {
|
|
61
|
+
var a = accounts.result[i];
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
` ${kleur.bold(`[${i + 1}]`)} ${a.name} ${fmt.dim(`(${a.id})`)}\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
process.stderr.write("\n");
|
|
67
|
+
|
|
68
|
+
var choice = await prompt(
|
|
69
|
+
rl,
|
|
70
|
+
`Select account [1-${accounts.result.length}]: `
|
|
71
|
+
);
|
|
72
|
+
var idx = parseInt(choice, 10) - 1;
|
|
73
|
+
|
|
74
|
+
if (isNaN(idx) || idx < 0 || idx >= accounts.result.length) {
|
|
75
|
+
rl.close();
|
|
76
|
+
fatal("Invalid selection.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
account = accounts.result[idx];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
rl.close();
|
|
83
|
+
|
|
84
|
+
saveConfig({ accountId: account.id, apiToken });
|
|
85
|
+
|
|
86
|
+
success("Authenticated!");
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
` Account: ${fmt.bold(account.name)} ${fmt.dim(`(${account.id})`)}\n`
|
|
89
|
+
);
|
|
90
|
+
hint("Next", "flarepilot deploy <name> ./path");
|
|
91
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { getConfig, getAppConfig, pushAppConfig } from "../lib/cf.js";
|
|
3
|
+
import { status, success, fatal, fmt } from "../lib/output.js";
|
|
4
|
+
import { resolveAppName } from "../lib/link.js";
|
|
5
|
+
|
|
6
|
+
export async function configShow(name, options) {
|
|
7
|
+
name = resolveAppName(name);
|
|
8
|
+
var config = getConfig();
|
|
9
|
+
var appConfig = await getAppConfig(config, name);
|
|
10
|
+
|
|
11
|
+
if (!appConfig) {
|
|
12
|
+
fatal(
|
|
13
|
+
`App ${fmt.app(name)} not found.`,
|
|
14
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var env = appConfig.env || {};
|
|
19
|
+
var keys = Object.keys(env);
|
|
20
|
+
|
|
21
|
+
if (keys.length === 0) {
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log("{}");
|
|
24
|
+
} else {
|
|
25
|
+
process.stderr.write(`No config vars set for ${fmt.app(name)}.\n`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options.json) {
|
|
31
|
+
console.log(JSON.stringify(env, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (var key of keys) {
|
|
36
|
+
console.log(`${fmt.key(key)}=${env[key]}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function configSet(args) {
|
|
41
|
+
// Smart detection: if first arg contains '=', all args are vars.
|
|
42
|
+
// Otherwise first arg is the app name.
|
|
43
|
+
var name, vars;
|
|
44
|
+
if (args.length === 0) {
|
|
45
|
+
fatal("No env vars provided.", "Usage: flarepilot config set [name] KEY=VALUE ...");
|
|
46
|
+
}
|
|
47
|
+
if (args[0].includes("=")) {
|
|
48
|
+
name = resolveAppName(null);
|
|
49
|
+
vars = args;
|
|
50
|
+
} else {
|
|
51
|
+
name = args[0];
|
|
52
|
+
vars = args.slice(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (vars.length === 0) {
|
|
56
|
+
fatal("No env vars provided.", "Usage: flarepilot config set [name] KEY=VALUE ...");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var config = getConfig();
|
|
60
|
+
var appConfig = await getAppConfig(config, name);
|
|
61
|
+
|
|
62
|
+
if (!appConfig) {
|
|
63
|
+
fatal(
|
|
64
|
+
`App ${fmt.app(name)} not found.`,
|
|
65
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!appConfig.env) appConfig.env = {};
|
|
70
|
+
|
|
71
|
+
for (var v of vars) {
|
|
72
|
+
var eq = v.indexOf("=");
|
|
73
|
+
if (eq === -1) {
|
|
74
|
+
fatal(`Invalid format: ${v}`, "Use KEY=VALUE format.");
|
|
75
|
+
}
|
|
76
|
+
var key = v.substring(0, eq);
|
|
77
|
+
var value = v.substring(eq + 1);
|
|
78
|
+
appConfig.env[key] = value;
|
|
79
|
+
status(`${fmt.key(key)} set`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await pushAppConfig(config, name, appConfig);
|
|
83
|
+
success("Config updated (live).");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function configGet(args) {
|
|
87
|
+
// 1 arg = key (resolve name from link). 2 args = name + key.
|
|
88
|
+
var name, key;
|
|
89
|
+
if (args.length === 2) {
|
|
90
|
+
name = args[0];
|
|
91
|
+
key = args[1];
|
|
92
|
+
} else if (args.length === 1) {
|
|
93
|
+
name = resolveAppName(null);
|
|
94
|
+
key = args[0];
|
|
95
|
+
} else {
|
|
96
|
+
fatal("Usage: flarepilot config get [name] <key>");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var config = getConfig();
|
|
100
|
+
var appConfig = await getAppConfig(config, name);
|
|
101
|
+
|
|
102
|
+
if (!appConfig) {
|
|
103
|
+
fatal(
|
|
104
|
+
`App ${fmt.app(name)} not found.`,
|
|
105
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var env = appConfig.env || {};
|
|
110
|
+
|
|
111
|
+
if (!(key in env)) {
|
|
112
|
+
fatal(`Key ${fmt.key(key)} is not set on ${fmt.app(name)}.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(env[key]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function configUnset(args) {
|
|
119
|
+
// 1 arg = key (resolve name from link). 2+ args: if first looks like
|
|
120
|
+
// an env key (UPPER_SNAKE), all are keys. Otherwise first is name.
|
|
121
|
+
var name, keys;
|
|
122
|
+
if (args.length === 0) {
|
|
123
|
+
fatal("No keys provided.", "Usage: flarepilot config unset [name] KEY ...");
|
|
124
|
+
}
|
|
125
|
+
if (args.length === 1) {
|
|
126
|
+
name = resolveAppName(null);
|
|
127
|
+
keys = args;
|
|
128
|
+
} else if (/^[A-Z_][A-Z0-9_]*$/.test(args[0])) {
|
|
129
|
+
name = resolveAppName(null);
|
|
130
|
+
keys = args;
|
|
131
|
+
} else {
|
|
132
|
+
name = args[0];
|
|
133
|
+
keys = args.slice(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (keys.length === 0) {
|
|
137
|
+
fatal("No keys provided.", "Usage: flarepilot config unset [name] KEY ...");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var config = getConfig();
|
|
141
|
+
var appConfig = await getAppConfig(config, name);
|
|
142
|
+
|
|
143
|
+
if (!appConfig) {
|
|
144
|
+
fatal(
|
|
145
|
+
`App ${fmt.app(name)} not found.`,
|
|
146
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!appConfig.env) appConfig.env = {};
|
|
151
|
+
|
|
152
|
+
for (var key of keys) {
|
|
153
|
+
if (!(key in appConfig.env)) {
|
|
154
|
+
status(`${fmt.key(key)} ${fmt.dim("(not set, skipping)")}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
delete appConfig.env[key];
|
|
158
|
+
status(`${fmt.key(key)} removed`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await pushAppConfig(config, name, appConfig);
|
|
162
|
+
success("Config updated (live).");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function configImport(name, options) {
|
|
166
|
+
name = resolveAppName(name);
|
|
167
|
+
var config = getConfig();
|
|
168
|
+
var appConfig = await getAppConfig(config, name);
|
|
169
|
+
|
|
170
|
+
if (!appConfig) {
|
|
171
|
+
fatal(
|
|
172
|
+
`App ${fmt.app(name)} not found.`,
|
|
173
|
+
`Run ${fmt.cmd(`flarepilot deploy ${name} .`)} first.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!appConfig.env) appConfig.env = {};
|
|
178
|
+
|
|
179
|
+
var input;
|
|
180
|
+
if (options.file) {
|
|
181
|
+
try {
|
|
182
|
+
input = readFileSync(options.file, "utf-8");
|
|
183
|
+
} catch (e) {
|
|
184
|
+
fatal(`Could not read file: ${options.file}`, e.message);
|
|
185
|
+
}
|
|
186
|
+
} else if (process.stdin.isTTY) {
|
|
187
|
+
fatal(
|
|
188
|
+
"No input provided.",
|
|
189
|
+
"Pipe a .env file: cat .env | flarepilot config import\n Or use: flarepilot config import --file .env"
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
var chunks = [];
|
|
193
|
+
for await (var chunk of process.stdin) {
|
|
194
|
+
chunks.push(chunk);
|
|
195
|
+
}
|
|
196
|
+
input = Buffer.concat(chunks).toString("utf-8");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
var count = 0;
|
|
200
|
+
for (var line of input.split("\n")) {
|
|
201
|
+
line = line.trim();
|
|
202
|
+
if (!line || line.startsWith("#")) continue;
|
|
203
|
+
var eq = line.indexOf("=");
|
|
204
|
+
if (eq === -1) continue;
|
|
205
|
+
var key = line.substring(0, eq).trim();
|
|
206
|
+
var value = line.substring(eq + 1).trim();
|
|
207
|
+
// Strip surrounding quotes
|
|
208
|
+
if (
|
|
209
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
210
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
211
|
+
) {
|
|
212
|
+
value = value.slice(1, -1);
|
|
213
|
+
}
|
|
214
|
+
appConfig.env[key] = value;
|
|
215
|
+
status(`${fmt.key(key)} set`);
|
|
216
|
+
count++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (count === 0) {
|
|
220
|
+
fatal("No variables found in input.", "Use KEY=VALUE format, one per line.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await pushAppConfig(config, name, appConfig);
|
|
224
|
+
success(`${count} variable${count !== 1 ? "s" : ""} imported (live).`);
|
|
225
|
+
}
|