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 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
+ }