@meshxdata/fops 0.0.6 → 0.0.8
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 +2 -3
- package/src/auth/coda.js +4 -4
- package/src/auth/login.js +9 -6
- package/src/commands/index.js +74 -9
- package/src/doctor.js +333 -71
- package/src/feature-flags.js +3 -3
- package/src/lazy.js +12 -0
- package/src/plugins/bundled/coda/auth.js +8 -2
- package/src/plugins/bundled/cursor/index.js +3 -2
- package/src/plugins/bundled/fops-plugin-1password/index.js +6 -4
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +10 -2
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +3 -3
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +7 -2
- package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +5 -6
- package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +5 -5
- package/src/plugins/loader.js +3 -3
- package/src/plugins/skills.js +1 -1
- package/src/setup/aws.js +74 -46
- package/src/setup/setup.js +2 -2
- package/src/setup/wizard.js +16 -32
- package/src/shell.js +7 -1
- package/src/ui/confirm.js +10 -1
- package/src/wsl.js +82 -0
|
@@ -144,9 +144,7 @@ export function register(api) {
|
|
|
144
144
|
} else if (whoami.hasAccount) {
|
|
145
145
|
ok(`1Password configured`, `${whoami.email} — session will unlock on next use`);
|
|
146
146
|
} else {
|
|
147
|
-
warn("1Password not signed in", "
|
|
148
|
-
await opSignin();
|
|
149
|
-
});
|
|
147
|
+
warn("1Password not signed in", "optional — run: op signin");
|
|
150
148
|
}
|
|
151
149
|
},
|
|
152
150
|
});
|
|
@@ -163,7 +161,11 @@ export function register(api) {
|
|
|
163
161
|
|
|
164
162
|
const whoami = await opWhoami();
|
|
165
163
|
if (!whoami.authenticated) {
|
|
166
|
-
|
|
164
|
+
if (whoami.hasAccount) {
|
|
165
|
+
console.log(chalk.dim(" 1Password: session locked — unlock the app to auto-sync secrets"));
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.yellow(" ⚠ 1Password: not signed in — skipping secret sync"));
|
|
168
|
+
}
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -90,7 +90,7 @@ export async function opSignin() {
|
|
|
90
90
|
if (check.exitCode === 0) return;
|
|
91
91
|
} catch {}
|
|
92
92
|
|
|
93
|
-
// Desktop app integration not enabled —
|
|
93
|
+
// Desktop app integration not enabled — platform-specific fallback
|
|
94
94
|
if (process.platform === "darwin") {
|
|
95
95
|
console.log(" Opening 1Password Developer settings...");
|
|
96
96
|
console.log(" Enable \"Integrate with 1Password CLI\", then come back.\n");
|
|
@@ -105,7 +105,15 @@ export async function opSignin() {
|
|
|
105
105
|
if (exitCode === 0) return;
|
|
106
106
|
} catch {}
|
|
107
107
|
}
|
|
108
|
+
} else if (process.platform === "linux") {
|
|
109
|
+
// On Linux/WSL there's no desktop app — use interactive signin
|
|
110
|
+
console.log(" No 1Password desktop app on Linux. Signing in interactively...\n");
|
|
111
|
+
try {
|
|
112
|
+
await execa("op", ["signin"], { stdio: "inherit", timeout: 120_000 });
|
|
113
|
+
const check = await execa("op", ["whoami"], { reject: false, timeout: 5000 });
|
|
114
|
+
if (check.exitCode === 0) return;
|
|
115
|
+
} catch {}
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
throw new Error("1Password CLI integration not enabled");
|
|
118
|
+
throw new Error("1Password CLI integration not enabled — run: op signin");
|
|
111
119
|
}
|
|
@@ -2,9 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import inquirer from "inquirer";
|
|
6
5
|
import { opVersion, opWhoami, opSignin, opListVaults } from "./op.js";
|
|
7
6
|
import { discoverTemplates } from "./env.js";
|
|
7
|
+
import { getInquirer } from "../../../../lazy.js";
|
|
8
8
|
|
|
9
9
|
// Patterns that indicate a key is likely a secret
|
|
10
10
|
const SECRET_PATTERNS = [
|
|
@@ -50,14 +50,14 @@ function saveFopsConfig(updates) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async function ask(message, defaultValue = true) {
|
|
53
|
-
const { answer } = await
|
|
53
|
+
const { answer } = await (await getInquirer()).prompt([{
|
|
54
54
|
type: "confirm", name: "answer", message, default: defaultValue,
|
|
55
55
|
}]);
|
|
56
56
|
return answer;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async function choose(message, choices) {
|
|
60
|
-
const { answer } = await
|
|
60
|
+
const { answer } = await (await getInquirer()).prompt([{
|
|
61
61
|
type: "list", name: "answer", message, choices,
|
|
62
62
|
}]);
|
|
63
63
|
return answer;
|
|
@@ -51,9 +51,14 @@ export async function syncSecrets(root) {
|
|
|
51
51
|
console.log(chalk.green(` ✓ ${relDir}/.env — ${secrets.size} secret(s) synced`));
|
|
52
52
|
synced++;
|
|
53
53
|
} catch (err) {
|
|
54
|
-
|
|
54
|
+
// Extract clean error: prefer op's stderr over the generic execa message
|
|
55
|
+
const stderr = err.stderr?.trim();
|
|
56
|
+
const clean = stderr
|
|
57
|
+
? stderr.replace(/^\[ERROR\]\s*\S+\s*/gm, "").trim()
|
|
58
|
+
: err.message.split("\n")[0];
|
|
59
|
+
const msg = `${relDir}: ${clean}`;
|
|
55
60
|
errors.push(msg);
|
|
56
|
-
console.log(chalk.red(` ✗ ${relDir}/.env.1password — ${
|
|
61
|
+
console.log(chalk.red(` ✗ ${relDir}/.env.1password — ${clean}`));
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -44,12 +44,11 @@ export async function ssoLogin(profile) {
|
|
|
44
44
|
const args = ["sso", "login"];
|
|
45
45
|
if (profile) args.push("--profile", profile);
|
|
46
46
|
|
|
47
|
-
// Open /dev/tty so SSO login gets a real terminal even under piped stdio
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ttyFd = null;
|
|
47
|
+
// Open /dev/tty so SSO login gets a real terminal even under piped stdio.
|
|
48
|
+
// On Windows /dev/tty doesn't exist — fall back to inherited stdio.
|
|
49
|
+
let ttyFd = null;
|
|
50
|
+
if (process.platform !== "win32") {
|
|
51
|
+
try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
await execa("aws", args, {
|
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import inquirer from "inquirer";
|
|
6
5
|
import {
|
|
7
6
|
awsVersion,
|
|
8
7
|
detectEcrRegistry,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
ssoLogin,
|
|
12
11
|
ecrLogin,
|
|
13
12
|
} from "./aws.js";
|
|
13
|
+
import { getInquirer } from "../../../../lazy.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Read ~/.fops.json config (full file).
|
|
@@ -53,7 +53,7 @@ export async function runSetupWizard(root) {
|
|
|
53
53
|
let version = await awsVersion();
|
|
54
54
|
if (!version) {
|
|
55
55
|
console.log(chalk.red(" ✗ AWS CLI not found."));
|
|
56
|
-
const { install } = await
|
|
56
|
+
const { install } = await (await getInquirer()).prompt([{
|
|
57
57
|
type: "confirm", name: "install", message: "Install via Homebrew?", default: true,
|
|
58
58
|
}]);
|
|
59
59
|
if (install) {
|
|
@@ -85,7 +85,7 @@ export async function runSetupWizard(root) {
|
|
|
85
85
|
console.log(chalk.dim(` Using profile: ${selectedProfile}`));
|
|
86
86
|
} else {
|
|
87
87
|
const choices = profiles.map((p) => ({ name: p.name, value: p.name }));
|
|
88
|
-
const { profile } = await
|
|
88
|
+
const { profile } = await (await getInquirer()).prompt([{
|
|
89
89
|
type: "list", name: "profile", message: "Select AWS profile:", choices,
|
|
90
90
|
}]);
|
|
91
91
|
selectedProfile = profile;
|
|
@@ -94,7 +94,7 @@ export async function runSetupWizard(root) {
|
|
|
94
94
|
// No profiles — prompt for SSO config
|
|
95
95
|
console.log(chalk.yellow(" No SSO profiles found. Let's configure one."));
|
|
96
96
|
|
|
97
|
-
const answers = await
|
|
97
|
+
const answers = await (await getInquirer()).prompt([
|
|
98
98
|
{ type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
|
|
99
99
|
{ type: "input", name: "ssoStartUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
|
|
100
100
|
{ type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
|
|
@@ -154,7 +154,7 @@ output = json
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Step 5: Auto-login preference
|
|
157
|
-
const { autoLogin } = await
|
|
157
|
+
const { autoLogin } = await (await getInquirer()).prompt([{
|
|
158
158
|
type: "confirm", name: "autoLogin", message: "Auto-login to ECR before `fops up`?", default: true,
|
|
159
159
|
}]);
|
|
160
160
|
|
package/src/plugins/loader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createRegistry } from "./registry.js";
|
|
6
6
|
import { validateManifest } from "./manifest.js";
|
|
7
7
|
import { discoverPlugins } from "./discovery.js";
|
|
@@ -127,7 +127,7 @@ async function loadBuiltinPlugins(registry) {
|
|
|
127
127
|
const entries = fs.readdirSync(builtinsDir).filter((f) => f.endsWith(".js"));
|
|
128
128
|
for (const file of entries) {
|
|
129
129
|
try {
|
|
130
|
-
const mod = await import(path.join(builtinsDir, file));
|
|
130
|
+
const mod = await import(pathToFileURL(path.join(builtinsDir, file)).href);
|
|
131
131
|
const plugin = mod.default || mod;
|
|
132
132
|
if (typeof plugin.register === "function") {
|
|
133
133
|
const pluginId = `builtin:${path.basename(file, ".js")}`;
|
|
@@ -192,7 +192,7 @@ export async function loadPlugins() {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
try {
|
|
195
|
-
const mod = await import(entryPoint);
|
|
195
|
+
const mod = await import(pathToFileURL(entryPoint).href);
|
|
196
196
|
const plugin = mod.default || mod;
|
|
197
197
|
|
|
198
198
|
if (typeof plugin.register === "function") {
|
package/src/plugins/skills.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { execa } from "execa";
|
|
6
5
|
|
|
7
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
7
|
|
|
@@ -27,6 +26,7 @@ function parseFrontmatter(content) {
|
|
|
27
26
|
*/
|
|
28
27
|
async function hasBin(name) {
|
|
29
28
|
try {
|
|
29
|
+
const { execa } = await import("execa");
|
|
30
30
|
await execa("which", [name], { reject: false, timeout: 2000 });
|
|
31
31
|
return true;
|
|
32
32
|
} catch {
|
package/src/setup/aws.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
-
import
|
|
6
|
+
import { getInquirer } from "../lazy.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Read saved FOPS config (~/.fops.json) for AWS SSO settings etc.
|
|
@@ -34,7 +34,7 @@ export async function promptAwsSsoConfig() {
|
|
|
34
34
|
console.log(chalk.dim(" We'll set up an AWS CLI profile for ECR image pulls."));
|
|
35
35
|
console.log(chalk.dim(" You can find these values in your AWS SSO portal.\n"));
|
|
36
36
|
|
|
37
|
-
const answers = await
|
|
37
|
+
const answers = await (await getInquirer()).prompt([
|
|
38
38
|
{
|
|
39
39
|
type: "input",
|
|
40
40
|
name: "profileName",
|
|
@@ -100,10 +100,13 @@ export function detectEcrRegistry(dir) {
|
|
|
100
100
|
/**
|
|
101
101
|
* Parse ~/.aws/config and find SSO profiles with their sso_session names.
|
|
102
102
|
*/
|
|
103
|
-
export function detectAwsSsoProfiles() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
export function detectAwsSsoProfiles(configContent = null) {
|
|
104
|
+
if (configContent === null) {
|
|
105
|
+
const configPath = path.join(os.homedir(), ".aws", "config");
|
|
106
|
+
if (!fs.existsSync(configPath)) return [];
|
|
107
|
+
configContent = fs.readFileSync(configPath, "utf8");
|
|
108
|
+
}
|
|
109
|
+
const content = configContent;
|
|
107
110
|
const profiles = [];
|
|
108
111
|
let currentProfile = null;
|
|
109
112
|
let currentAttrs = {};
|
|
@@ -151,7 +154,7 @@ export async function ensureSsoConfig() {
|
|
|
151
154
|
console.log(chalk.cyan("\n AWS SSO is not configured. Let's set it up.\n"));
|
|
152
155
|
console.log(chalk.dim(" You can find these values in your AWS SSO portal.\n"));
|
|
153
156
|
|
|
154
|
-
const answers = await
|
|
157
|
+
const answers = await (await getInquirer()).prompt([
|
|
155
158
|
{ type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
|
|
156
159
|
{ type: "input", name: "startUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
|
|
157
160
|
{ type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
|
|
@@ -183,86 +186,111 @@ output = json
|
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
/**
|
|
186
|
-
*
|
|
189
|
+
* Run `aws sso login` for a profile, handling TTY on all platforms.
|
|
190
|
+
* Returns the execa result ({ exitCode, timedOut }).
|
|
187
191
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (profiles.length === 0) {
|
|
193
|
-
throw new Error("No SSO profiles found after config — check ~/.aws/config");
|
|
192
|
+
async function runSsoLogin(profileName) {
|
|
193
|
+
let ttyFd = null;
|
|
194
|
+
if (process.platform !== "win32") {
|
|
195
|
+
try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
const
|
|
197
|
-
console.log(chalk.dim(` Using AWS profile: ${profile.name}`));
|
|
198
|
-
console.log(chalk.cyan(` ▶ aws sso login --profile ${profile.name}`));
|
|
199
|
-
|
|
200
|
-
// Open /dev/tty directly so SSO login gets a real terminal even when
|
|
201
|
-
// the parent process has piped stdio (e.g. agent → runShellCommand → fops doctor)
|
|
202
|
-
let ttyFd;
|
|
203
|
-
try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
|
|
204
|
-
|
|
205
|
-
const { exitCode } = await execa("aws", ["sso", "login", "--profile", profile.name], {
|
|
198
|
+
const result = await execa("aws", ["sso", "login", "--profile", profileName], {
|
|
206
199
|
stdio: [ttyFd ?? "inherit", "inherit", "inherit"],
|
|
207
200
|
reject: false,
|
|
208
201
|
timeout: 120_000,
|
|
209
202
|
});
|
|
210
203
|
|
|
211
204
|
if (ttyFd !== null) fs.closeSync(ttyFd);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Fix AWS SSO: ensure config exists, detect profile, then login.
|
|
210
|
+
* Distinguishes timeout/incomplete browser auth from bad config so
|
|
211
|
+
* the user isn't forced to re-enter values when they just ran out of time.
|
|
212
|
+
*/
|
|
213
|
+
export async function fixAwsSso(ctx = {}) {
|
|
214
|
+
const exec = ctx.exec || execa;
|
|
215
|
+
const home = ctx.home || os.homedir();
|
|
216
|
+
|
|
217
|
+
await ensureSsoConfig();
|
|
219
218
|
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
const MAX_ATTEMPTS = 3;
|
|
220
|
+
|
|
221
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
222
|
+
const profiles = detectAwsSsoProfiles();
|
|
223
|
+
if (profiles.length === 0) {
|
|
222
224
|
throw new Error("No SSO profiles found after config — check ~/.aws/config");
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
const
|
|
226
|
-
console.log(chalk.
|
|
227
|
-
|
|
228
|
-
let retryTtyFd;
|
|
229
|
-
try { retryTtyFd = fs.openSync("/dev/tty", "r"); } catch { retryTtyFd = null; }
|
|
227
|
+
const profile = profiles[0];
|
|
228
|
+
console.log(chalk.dim(` Using AWS profile: ${profile.name}`));
|
|
229
|
+
console.log(chalk.cyan(` ▶ aws sso login --profile ${profile.name}`));
|
|
230
230
|
|
|
231
|
-
const
|
|
232
|
-
stdio:
|
|
231
|
+
const result = await exec("aws", ["sso", "login", "--profile", profile.name], {
|
|
232
|
+
stdio: "inherit",
|
|
233
233
|
reject: false,
|
|
234
234
|
timeout: 120_000,
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
if (
|
|
237
|
+
if (result.exitCode === 0) return;
|
|
238
|
+
|
|
239
|
+
if (result.timedOut) {
|
|
240
|
+
console.log(chalk.yellow("\n SSO login timed out — browser auth was not completed in time."));
|
|
241
|
+
} else {
|
|
242
|
+
console.log(chalk.yellow("\n SSO login did not complete."));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { action } = await (await getInquirer()).prompt([{
|
|
246
|
+
type: "list",
|
|
247
|
+
name: "action",
|
|
248
|
+
message: "What would you like to do?",
|
|
249
|
+
choices: [
|
|
250
|
+
{ name: "Retry login (same settings)", value: "retry" },
|
|
251
|
+
{ name: "Reconfigure SSO (re-enter values)", value: "reconfig" },
|
|
252
|
+
{ name: "Abort", value: "abort" },
|
|
253
|
+
],
|
|
254
|
+
}]);
|
|
255
|
+
|
|
256
|
+
if (action === "abort") {
|
|
257
|
+
throw new Error("SSO login aborted by user");
|
|
258
|
+
}
|
|
238
259
|
|
|
239
|
-
if (
|
|
240
|
-
|
|
260
|
+
if (action === "reconfig") {
|
|
261
|
+
const configPath = path.join(home, ".aws", "config");
|
|
262
|
+
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
263
|
+
await ensureSsoConfig();
|
|
241
264
|
}
|
|
265
|
+
// "retry" loops again with the same config
|
|
242
266
|
}
|
|
267
|
+
|
|
268
|
+
throw new Error("SSO login failed after multiple attempts. Check your SSO start URL and region in ~/.aws/config");
|
|
243
269
|
}
|
|
244
270
|
|
|
245
271
|
/**
|
|
246
272
|
* Fix ECR: ensure SSO session is valid, then docker login to ECR.
|
|
247
273
|
*/
|
|
248
|
-
export async function fixEcr(ecrInfo) {
|
|
274
|
+
export async function fixEcr(ecrInfo, ctx = {}) {
|
|
275
|
+
const exec = ctx.exec || execa;
|
|
276
|
+
|
|
249
277
|
const profiles = detectAwsSsoProfiles();
|
|
250
278
|
// Pick the profile whose region matches ECR, or fall back to first
|
|
251
279
|
const profile = profiles.find((p) => p.region === ecrInfo.region) || profiles[0];
|
|
252
280
|
const profileArgs = profile ? ["--profile", profile.name] : [];
|
|
253
281
|
|
|
254
282
|
// First make sure SSO works
|
|
255
|
-
const { stdout } = await
|
|
283
|
+
const { stdout } = await exec("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
|
|
256
284
|
reject: false, timeout: 10000,
|
|
257
285
|
});
|
|
258
286
|
if (!stdout || !stdout.includes("Account")) {
|
|
259
|
-
await fixAwsSso();
|
|
287
|
+
await fixAwsSso(ctx);
|
|
260
288
|
}
|
|
261
289
|
|
|
262
290
|
// Now do ECR docker login
|
|
263
291
|
const ecrUrl = `${ecrInfo.accountId}.dkr.ecr.${ecrInfo.region}.amazonaws.com`;
|
|
264
292
|
console.log(chalk.cyan(` ▶ ECR docker login → ${ecrUrl}`));
|
|
265
|
-
const { stdout: password } = await
|
|
293
|
+
const { stdout: password } = await exec("aws", [
|
|
266
294
|
"ecr", "get-login-password", "--region", ecrInfo.region, ...profileArgs,
|
|
267
295
|
], { reject: false, timeout: 15000 });
|
|
268
296
|
|
package/src/setup/setup.js
CHANGED
|
@@ -3,9 +3,9 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
-
import inquirer from "inquirer";
|
|
7
6
|
import { make } from "../shell.js";
|
|
8
7
|
import { readFopsConfig, saveFopsConfig, promptAwsSsoConfig, detectEcrRegistry, checkEcrRepos } from "./aws.js";
|
|
8
|
+
import { getInquirer } from "../lazy.js";
|
|
9
9
|
|
|
10
10
|
// TODO: change back to "main" once stack/api is merged
|
|
11
11
|
export const CLONE_BRANCH = "stack/api";
|
|
@@ -70,7 +70,7 @@ export function runSetup(dir, opts = {}) {
|
|
|
70
70
|
// Check if docker-compose references ECR to auto-detect some values
|
|
71
71
|
const ecrInfo = detectEcrRegistry(dir);
|
|
72
72
|
|
|
73
|
-
const { setupAws } = await
|
|
73
|
+
const { setupAws } = await (await getInquirer()).prompt([{
|
|
74
74
|
type: "confirm",
|
|
75
75
|
name: "setupAws",
|
|
76
76
|
message: ecrInfo
|
package/src/setup/wizard.js
CHANGED
|
@@ -3,12 +3,12 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
-
import inquirer from "inquirer";
|
|
7
6
|
import { isFoundationRoot, findComposeRootUp } from "../project.js";
|
|
8
7
|
import { discoverPlugins } from "../plugins/discovery.js";
|
|
9
8
|
import { validateManifest } from "../plugins/manifest.js";
|
|
10
9
|
import { runSetup, CLONE_BRANCH } from "./setup.js";
|
|
11
|
-
import { confirm } from "../ui/index.js";
|
|
10
|
+
import { confirm, selectOption } from "../ui/index.js";
|
|
11
|
+
import { getInquirer } from "../lazy.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Ensure Homebrew is available (macOS). Installs if missing.
|
|
@@ -76,7 +76,7 @@ export async function runInitWizard() {
|
|
|
76
76
|
} else {
|
|
77
77
|
const foundUp = findComposeRootUp(cwd);
|
|
78
78
|
if (foundUp && foundUp !== cwd) {
|
|
79
|
-
const { useFound } = await
|
|
79
|
+
const { useFound } = await (await getInquirer()).prompt([
|
|
80
80
|
{ type: "confirm", name: "useFound", message: `Found Foundation project at:\n ${foundUp}\n Use it instead of the current directory?`, default: false },
|
|
81
81
|
]);
|
|
82
82
|
if (useFound) projectRoot = foundUp;
|
|
@@ -130,24 +130,9 @@ export async function runInitWizard() {
|
|
|
130
130
|
hasAws = await installTool("AWS CLI", { brew: "awscli", winget: "Amazon.AWSCLI" });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// 1Password
|
|
134
|
-
const has1PwdApp = process.platform === "darwin"
|
|
135
|
-
? fs.existsSync("/Applications/1Password.app")
|
|
136
|
-
: process.platform === "win32"
|
|
137
|
-
? fs.existsSync(path.join(process.env.LOCALAPPDATA || "", "1Password", "app", "8", "1Password.exe"))
|
|
138
|
-
: false;
|
|
139
|
-
if (has1PwdApp) console.log(chalk.green(" ✓ 1Password"));
|
|
140
|
-
else {
|
|
141
|
-
console.log(chalk.yellow(" ⚠ 1Password") + chalk.dim(" — desktop app needed for CLI integration"));
|
|
142
|
-
await installTool("1Password", { brewCask: "1password", winget: "AgileBits.1Password" });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 1Password CLI (optional)
|
|
133
|
+
// 1Password (optional — status only, no install prompt)
|
|
146
134
|
if (hasOp) console.log(chalk.green(" ✓ 1Password CLI"));
|
|
147
|
-
else
|
|
148
|
-
console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — needed for secret sync"));
|
|
149
|
-
hasOp = await installTool("1Password CLI", { brewCask: "1password-cli", winget: "AgileBits.1Password.CLI" });
|
|
150
|
-
}
|
|
135
|
+
else console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — optional, run: op signin"));
|
|
151
136
|
|
|
152
137
|
// GitHub credentials
|
|
153
138
|
const netrcPath = path.join(os.homedir(), ".netrc");
|
|
@@ -185,19 +170,18 @@ export async function runInitWizard() {
|
|
|
185
170
|
console.log(chalk.red(" Required tools are still missing. Install them and run fops init again.\n"));
|
|
186
171
|
process.exit(1);
|
|
187
172
|
}
|
|
188
|
-
const
|
|
189
|
-
{
|
|
190
|
-
{
|
|
191
|
-
{
|
|
192
|
-
];
|
|
193
|
-
|
|
194
|
-
if (action === "cancel") process.exit(0);
|
|
173
|
+
const action = await selectOption("No Foundation project found. What do you want to do?", [
|
|
174
|
+
{ label: "Clone foundation-compose into this directory", value: "clone" },
|
|
175
|
+
{ label: "Enter path to an existing foundation-compose directory", value: "path" },
|
|
176
|
+
{ label: "Cancel", value: "cancel" },
|
|
177
|
+
]);
|
|
178
|
+
if (!action || action === "cancel") process.exit(0);
|
|
195
179
|
if (action === "clone") {
|
|
196
|
-
const { repoUrl } = await
|
|
180
|
+
const { repoUrl } = await (await getInquirer()).prompt([
|
|
197
181
|
{ type: "input", name: "repoUrl", message: "Repository URL:", default: "https://github.com/meshxdata/foundation-compose.git", validate: (v) => (v?.trim() ? true : "Repository URL is required.") },
|
|
198
182
|
]);
|
|
199
183
|
const repoName = repoUrl.trim().replace(/\.git$/, "").split("/").pop() || "foundation-compose";
|
|
200
|
-
const { targetDir } = await
|
|
184
|
+
const { targetDir } = await (await getInquirer()).prompt([
|
|
201
185
|
{ type: "input", name: "targetDir", message: "Clone into:", default: path.join(cwd, repoName) },
|
|
202
186
|
]);
|
|
203
187
|
const resolved = path.resolve(targetDir.trim());
|
|
@@ -238,7 +222,7 @@ export async function runInitWizard() {
|
|
|
238
222
|
}
|
|
239
223
|
}
|
|
240
224
|
if (action === "path") {
|
|
241
|
-
const { dir } = await
|
|
225
|
+
const { dir } = await (await getInquirer()).prompt([
|
|
242
226
|
{
|
|
243
227
|
type: "input", name: "dir", message: "Path to foundation-compose directory:",
|
|
244
228
|
validate: (value) => {
|
|
@@ -253,7 +237,7 @@ export async function runInitWizard() {
|
|
|
253
237
|
}
|
|
254
238
|
}
|
|
255
239
|
}
|
|
256
|
-
const { submodules, env, download } = await
|
|
240
|
+
const { submodules, env, download } = await (await getInquirer()).prompt([
|
|
257
241
|
{ type: "confirm", name: "submodules", message: "Initialize and update git submodules?", default: true },
|
|
258
242
|
{ type: "confirm", name: "env", message: "Create .env from .env.example (if missing)?", default: true },
|
|
259
243
|
{ type: "confirm", name: "download", message: "Download container images now (make download)?", default: false },
|
|
@@ -292,7 +276,7 @@ export async function runInitWizard() {
|
|
|
292
276
|
};
|
|
293
277
|
});
|
|
294
278
|
|
|
295
|
-
const { enabledPlugins } = await
|
|
279
|
+
const { enabledPlugins } = await (await getInquirer()).prompt([{
|
|
296
280
|
type: "checkbox",
|
|
297
281
|
name: "enabledPlugins",
|
|
298
282
|
message: "Plugins:",
|
package/src/shell.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
let _execa;
|
|
2
|
+
async function lazyExeca() {
|
|
3
|
+
if (!_execa) _execa = (await import("execa")).execa;
|
|
4
|
+
return _execa;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export async function make(root, target, args = []) {
|
|
8
|
+
const execa = await lazyExeca();
|
|
4
9
|
return execa("make", [target, ...args], { cwd: root, stdio: "inherit", reject: false });
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export async function dockerCompose(root, args) {
|
|
13
|
+
const execa = await lazyExeca();
|
|
8
14
|
return execa("docker", ["compose", ...args], { cwd: root, stdio: "inherit", reject: false });
|
|
9
15
|
}
|
package/src/ui/confirm.js
CHANGED
|
@@ -76,7 +76,16 @@ export async function selectOption(message, options) {
|
|
|
76
76
|
resolved = true;
|
|
77
77
|
clear();
|
|
78
78
|
unmount();
|
|
79
|
-
|
|
79
|
+
// Fix for Node 24+: restore stdin state after ink unmounts
|
|
80
|
+
// ink leaves stdin paused/in raw mode which breaks inquirer
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
if (process.stdin.isTTY) {
|
|
83
|
+
process.stdin.setRawMode(false);
|
|
84
|
+
process.stdin.resume();
|
|
85
|
+
process.stdin.ref();
|
|
86
|
+
}
|
|
87
|
+
resolve(selected ? selected.value : null);
|
|
88
|
+
}, 100);
|
|
80
89
|
};
|
|
81
90
|
const { unmount, clear } = render(
|
|
82
91
|
h(SelectPrompt, { message, options: normalized, onResult })
|
package/src/wsl.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a command inside the default WSL distro.
|
|
5
|
+
*/
|
|
6
|
+
export async function wslExec(cmd, args = [], opts = {}) {
|
|
7
|
+
const { input, timeout, reject, stdio } = opts;
|
|
8
|
+
return execa("wsl", ["-e", cmd, ...args], { input, timeout, reject, stdio });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let _cachedHome = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the WSL user's home directory (cached after first call).
|
|
15
|
+
*/
|
|
16
|
+
export async function wslHomedir() {
|
|
17
|
+
if (_cachedHome) return _cachedHome;
|
|
18
|
+
const { stdout } = await execa("wsl", ["-e", "sh", "-c", "echo $HOME"], {
|
|
19
|
+
timeout: 10000,
|
|
20
|
+
});
|
|
21
|
+
_cachedHome = stdout.trim();
|
|
22
|
+
return _cachedHome;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reset the cached WSL home directory (for testing).
|
|
27
|
+
*/
|
|
28
|
+
export function _resetHomedirCache() {
|
|
29
|
+
_cachedHome = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check whether a file exists inside WSL.
|
|
34
|
+
*/
|
|
35
|
+
export async function wslFileExists(filepath) {
|
|
36
|
+
try {
|
|
37
|
+
const { exitCode } = await execa("wsl", ["-e", "test", "-f", filepath], {
|
|
38
|
+
reject: false,
|
|
39
|
+
timeout: 5000,
|
|
40
|
+
});
|
|
41
|
+
return exitCode === 0;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read file content from inside WSL.
|
|
49
|
+
*/
|
|
50
|
+
export async function wslReadFile(filepath) {
|
|
51
|
+
const { stdout } = await execa("wsl", ["-e", "cat", filepath], {
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
});
|
|
54
|
+
return stdout;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write content to a file inside WSL.
|
|
59
|
+
*/
|
|
60
|
+
export async function wslWriteFile(filepath, content) {
|
|
61
|
+
await execa("wsl", ["-e", "tee", filepath], {
|
|
62
|
+
input: content,
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
stdout: "ignore",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the version string for a command inside WSL.
|
|
70
|
+
* Returns the first line of output, or null if the command fails.
|
|
71
|
+
*/
|
|
72
|
+
export async function wslCmdVersion(cmd, args = ["--version"]) {
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execa("wsl", ["-e", cmd, ...args], {
|
|
75
|
+
reject: false,
|
|
76
|
+
timeout: 10000,
|
|
77
|
+
});
|
|
78
|
+
return stdout?.split("\n")[0]?.trim() || null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|