@meshxdata/fops 0.0.1

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.
Files changed (57) hide show
  1. package/README.md +98 -0
  2. package/STRUCTURE.md +43 -0
  3. package/foundation.mjs +16 -0
  4. package/package.json +52 -0
  5. package/src/agent/agent.js +367 -0
  6. package/src/agent/agent.test.js +233 -0
  7. package/src/agent/context.js +143 -0
  8. package/src/agent/context.test.js +81 -0
  9. package/src/agent/index.js +2 -0
  10. package/src/agent/llm.js +127 -0
  11. package/src/agent/llm.test.js +139 -0
  12. package/src/auth/index.js +4 -0
  13. package/src/auth/keychain.js +58 -0
  14. package/src/auth/keychain.test.js +185 -0
  15. package/src/auth/login.js +421 -0
  16. package/src/auth/login.test.js +192 -0
  17. package/src/auth/oauth.js +203 -0
  18. package/src/auth/oauth.test.js +118 -0
  19. package/src/auth/resolve.js +78 -0
  20. package/src/auth/resolve.test.js +153 -0
  21. package/src/commands/index.js +268 -0
  22. package/src/config.js +24 -0
  23. package/src/config.test.js +70 -0
  24. package/src/doctor.js +487 -0
  25. package/src/doctor.test.js +134 -0
  26. package/src/plugins/api.js +37 -0
  27. package/src/plugins/api.test.js +95 -0
  28. package/src/plugins/discovery.js +78 -0
  29. package/src/plugins/discovery.test.js +92 -0
  30. package/src/plugins/hooks.js +13 -0
  31. package/src/plugins/hooks.test.js +118 -0
  32. package/src/plugins/index.js +3 -0
  33. package/src/plugins/loader.js +110 -0
  34. package/src/plugins/manifest.js +26 -0
  35. package/src/plugins/manifest.test.js +106 -0
  36. package/src/plugins/registry.js +14 -0
  37. package/src/plugins/registry.test.js +43 -0
  38. package/src/plugins/skills.js +126 -0
  39. package/src/plugins/skills.test.js +173 -0
  40. package/src/project.js +61 -0
  41. package/src/project.test.js +196 -0
  42. package/src/setup/aws.js +369 -0
  43. package/src/setup/aws.test.js +280 -0
  44. package/src/setup/index.js +3 -0
  45. package/src/setup/setup.js +161 -0
  46. package/src/setup/wizard.js +119 -0
  47. package/src/shell.js +9 -0
  48. package/src/shell.test.js +72 -0
  49. package/src/skills/foundation/SKILL.md +107 -0
  50. package/src/ui/banner.js +56 -0
  51. package/src/ui/banner.test.js +97 -0
  52. package/src/ui/confirm.js +97 -0
  53. package/src/ui/index.js +5 -0
  54. package/src/ui/input.js +199 -0
  55. package/src/ui/spinner.js +170 -0
  56. package/src/ui/spinner.test.js +29 -0
  57. package/src/ui/streaming.js +106 -0
@@ -0,0 +1,369 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import chalk from "chalk";
6
+ import { execa } from "execa";
7
+ import inquirer from "inquirer";
8
+
9
+ /**
10
+ * Read saved FOPS config (~/.fops.json) for AWS SSO settings etc.
11
+ */
12
+ export function readFopsConfig() {
13
+ const configPath = path.join(os.homedir(), ".fops.json");
14
+ try {
15
+ if (fs.existsSync(configPath)) {
16
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
17
+ }
18
+ } catch {}
19
+ return {};
20
+ }
21
+
22
+ /**
23
+ * Save FOPS config (~/.fops.json)
24
+ */
25
+ export function saveFopsConfig(config) {
26
+ const configPath = path.join(os.homedir(), ".fops.json");
27
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
28
+ }
29
+
30
+ /**
31
+ * Prompt user for AWS SSO configuration interactively
32
+ */
33
+ export async function promptAwsSsoConfig() {
34
+ console.log(chalk.cyan("\n AWS SSO Configuration\n"));
35
+ console.log(chalk.gray(" We'll set up an AWS CLI profile for ECR image pulls."));
36
+ console.log(chalk.gray(" You can find these values in your AWS SSO portal.\n"));
37
+
38
+ const answers = await inquirer.prompt([
39
+ {
40
+ type: "input",
41
+ name: "profileName",
42
+ message: "AWS profile name:",
43
+ default: "dev",
44
+ },
45
+ {
46
+ type: "input",
47
+ name: "ssoStartUrl",
48
+ message: "SSO start URL:",
49
+ validate: (v) => v?.trim() ? true : "SSO start URL is required.",
50
+ },
51
+ {
52
+ type: "input",
53
+ name: "ssoRegion",
54
+ message: "SSO region:",
55
+ default: "us-east-1",
56
+ },
57
+ {
58
+ type: "input",
59
+ name: "accountId",
60
+ message: "AWS account ID:",
61
+ validate: (v) => /^\d{12}$/.test(v?.trim()) ? true : "Account ID must be 12 digits.",
62
+ },
63
+ {
64
+ type: "input",
65
+ name: "roleName",
66
+ message: "SSO role name:",
67
+ default: "AdministratorAccess",
68
+ },
69
+ {
70
+ type: "input",
71
+ name: "region",
72
+ message: "Default region:",
73
+ default: (answers) => answers.ssoRegion,
74
+ },
75
+ ]);
76
+
77
+ return {
78
+ profileName: answers.profileName.trim(),
79
+ ssoStartUrl: answers.ssoStartUrl.trim(),
80
+ ssoRegion: answers.ssoRegion.trim(),
81
+ accountId: answers.accountId.trim(),
82
+ roleName: answers.roleName.trim(),
83
+ region: answers.region.trim(),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Detect ECR registry URL from docker-compose.yaml
89
+ */
90
+ export function detectEcrRegistry(dir) {
91
+ try {
92
+ const composePath = path.join(dir, "docker-compose.yaml");
93
+ if (!fs.existsSync(composePath)) return null;
94
+ const content = fs.readFileSync(composePath, "utf8");
95
+ const match = content.match(/(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com/);
96
+ if (match) return { accountId: match[1], region: match[2] };
97
+ } catch {}
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Parse ~/.aws/config and find SSO profiles with their sso_session names.
103
+ */
104
+ export function detectAwsSsoProfiles() {
105
+ const configPath = path.join(os.homedir(), ".aws", "config");
106
+ if (!fs.existsSync(configPath)) return [];
107
+ const content = fs.readFileSync(configPath, "utf8");
108
+ const profiles = [];
109
+ let currentProfile = null;
110
+ let currentAttrs = {};
111
+
112
+ for (const line of content.split("\n")) {
113
+ const profileMatch = line.match(/^\[profile\s+(.+?)\]/);
114
+ if (profileMatch) {
115
+ if (currentProfile && currentAttrs.sso_session) {
116
+ profiles.push({ name: currentProfile, ...currentAttrs });
117
+ }
118
+ currentProfile = profileMatch[1];
119
+ currentAttrs = {};
120
+ continue;
121
+ }
122
+ if (line.startsWith("[")) {
123
+ if (currentProfile && currentAttrs.sso_session) {
124
+ profiles.push({ name: currentProfile, ...currentAttrs });
125
+ }
126
+ currentProfile = null;
127
+ currentAttrs = {};
128
+ continue;
129
+ }
130
+ const kv = line.match(/^\s*(\S+)\s*=\s*(.+)/);
131
+ if (kv && currentProfile) {
132
+ currentAttrs[kv[1]] = kv[2].trim();
133
+ }
134
+ }
135
+ if (currentProfile && currentAttrs.sso_session) {
136
+ profiles.push({ name: currentProfile, ...currentAttrs });
137
+ }
138
+ return profiles;
139
+ }
140
+
141
+ /**
142
+ * Simple readline prompt helper.
143
+ */
144
+ function ask(question, defaultVal) {
145
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
146
+ const suffix = defaultVal ? chalk.gray(` (${defaultVal})`) : "";
147
+ return new Promise((resolve) => {
148
+ rl.question(` ${question}${suffix}: `, (answer) => {
149
+ rl.close();
150
+ resolve(answer.trim() || defaultVal || "");
151
+ });
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Check if ~/.aws/config has an sso-session block with sso_start_url.
157
+ * If not, prompt user for the values and write them.
158
+ */
159
+ export async function ensureSsoConfig() {
160
+ const configPath = path.join(os.homedir(), ".aws", "config");
161
+ const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
162
+
163
+ // Check if any sso-session block has sso_start_url
164
+ if (/sso_start_url\s*=/.test(content)) return; // already configured
165
+
166
+ console.log(chalk.cyan("\n AWS SSO is not configured. Let's set it up.\n"));
167
+ console.log(chalk.gray(" You can find these values in your AWS SSO portal.\n"));
168
+
169
+ const sessionName = await ask("SSO session name", "meshx");
170
+ const startUrl = await ask("SSO start URL");
171
+ const ssoRegion = await ask("SSO region", "us-east-1");
172
+ const accountId = await ask("AWS account ID");
173
+ const roleName = await ask("SSO role name", "AdministratorAccess");
174
+ const region = await ask("Default region", ssoRegion);
175
+ const profileName = await ask("Profile name", "dev");
176
+
177
+ if (!startUrl || !accountId) {
178
+ throw new Error("SSO start URL and account ID are required");
179
+ }
180
+
181
+ // Ensure ~/.aws directory exists
182
+ const awsDir = path.join(os.homedir(), ".aws");
183
+ if (!fs.existsSync(awsDir)) fs.mkdirSync(awsDir, { mode: 0o700 });
184
+
185
+ const block = `
186
+ [sso-session ${sessionName}]
187
+ sso_start_url = ${startUrl}
188
+ sso_region = ${ssoRegion}
189
+ sso_registration_scopes = sso:account:access
190
+
191
+ [profile ${profileName}]
192
+ sso_session = ${sessionName}
193
+ sso_account_id = ${accountId}
194
+ sso_role_name = ${roleName}
195
+ region = ${region}
196
+ output = json
197
+ `;
198
+
199
+ fs.appendFileSync(configPath, block);
200
+ console.log(chalk.green(`\n ✓ Written to ~/.aws/config (profile: ${profileName})`));
201
+ }
202
+
203
+ /**
204
+ * Fix AWS SSO: ensure config exists, detect profile, then login.
205
+ */
206
+ export async function fixAwsSso() {
207
+ await ensureSsoConfig();
208
+
209
+ const profiles = detectAwsSsoProfiles();
210
+ if (profiles.length === 0) {
211
+ throw new Error("No SSO profiles found after config — check ~/.aws/config");
212
+ }
213
+
214
+ const profile = profiles[0];
215
+ console.log(chalk.gray(` Using AWS profile: ${profile.name}`));
216
+ console.log(chalk.cyan(` ▶ aws sso login --profile ${profile.name}`));
217
+
218
+ // Open /dev/tty directly so SSO login gets a real terminal even when
219
+ // the parent process has piped stdio (e.g. agent → runShellCommand → fops doctor)
220
+ let ttyFd;
221
+ try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
222
+
223
+ await execa("aws", ["sso", "login", "--profile", profile.name], {
224
+ stdio: [ttyFd ?? "inherit", "inherit", "inherit"],
225
+ reject: false,
226
+ timeout: 120_000,
227
+ });
228
+
229
+ if (ttyFd !== null) fs.closeSync(ttyFd);
230
+ }
231
+
232
+ /**
233
+ * Fix ECR: ensure SSO session is valid, then docker login to ECR.
234
+ */
235
+ export async function fixEcr(ecrInfo) {
236
+ const profiles = detectAwsSsoProfiles();
237
+ // Pick the profile whose region matches ECR, or fall back to first
238
+ const profile = profiles.find((p) => p.region === ecrInfo.region) || profiles[0];
239
+ const profileArgs = profile ? ["--profile", profile.name] : [];
240
+
241
+ // First make sure SSO works
242
+ const { stdout } = await execa("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
243
+ reject: false, timeout: 10000,
244
+ });
245
+ if (!stdout || !stdout.includes("Account")) {
246
+ await fixAwsSso();
247
+ }
248
+
249
+ // Now do ECR docker login
250
+ const ecrUrl = `${ecrInfo.accountId}.dkr.ecr.${ecrInfo.region}.amazonaws.com`;
251
+ console.log(chalk.cyan(` ▶ ECR docker login → ${ecrUrl}`));
252
+ const { stdout: password } = await execa("aws", [
253
+ "ecr", "get-login-password", "--region", ecrInfo.region, ...profileArgs,
254
+ ], { reject: false, timeout: 15000 });
255
+
256
+ if (password?.trim()) {
257
+ const { exitCode } = await execa("docker", ["login", "--username", "AWS", "--password-stdin", ecrUrl], {
258
+ input: password.trim(), reject: false, timeout: 15000,
259
+ });
260
+ if (exitCode === 0) console.log(chalk.green(` ✓ Logged in to ECR`));
261
+ else throw new Error("docker login failed");
262
+ } else {
263
+ throw new Error("Could not get ECR login password — check AWS session");
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Ensure ECR auth is valid before starting services.
269
+ * Detects ECR images in compose file, checks/refreshes credentials.
270
+ * Silently skips if no ECR images are found.
271
+ */
272
+ export async function ensureEcrAuth(dir) {
273
+ const ecrInfo = detectEcrRegistry(dir);
274
+ if (!ecrInfo) return; // no ECR images — nothing to do
275
+
276
+ const ecrUrl = `${ecrInfo.accountId}.dkr.ecr.${ecrInfo.region}.amazonaws.com`;
277
+
278
+ // Check if AWS CLI is available
279
+ try {
280
+ await execa("aws", ["--version"], { timeout: 5000 });
281
+ } catch {
282
+ console.log(chalk.yellow(" ⚠ AWS CLI not found — skipping ECR auth (image pulls may fail)"));
283
+ return;
284
+ }
285
+
286
+ const profiles = detectAwsSsoProfiles();
287
+ const profile = profiles.find((p) => p.region === ecrInfo.region) || profiles[0];
288
+ const profileArgs = profile ? ["--profile", profile.name] : [];
289
+
290
+ // Check if STS session is valid
291
+ const { stdout: stsOut } = await execa("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
292
+ reject: false, timeout: 10000,
293
+ });
294
+
295
+ if (!stsOut || !stsOut.includes("Account")) {
296
+ console.log(chalk.yellow(" ⚠ AWS session expired — logging in via SSO..."));
297
+ try {
298
+ await fixAwsSso();
299
+ } catch (err) {
300
+ console.log(chalk.red(` ✗ AWS SSO login failed: ${err.message}`));
301
+ return;
302
+ }
303
+ }
304
+
305
+ // Docker login to ECR
306
+ try {
307
+ const { stdout: password } = await execa("aws", [
308
+ "ecr", "get-login-password", "--region", ecrInfo.region, ...profileArgs,
309
+ ], { reject: false, timeout: 15000 });
310
+
311
+ if (!password?.trim()) {
312
+ console.log(chalk.red(" ✗ Could not get ECR login password — image pulls may fail"));
313
+ return;
314
+ }
315
+
316
+ const { exitCode } = await execa("docker", ["login", "--username", "AWS", "--password-stdin", ecrUrl], {
317
+ input: password.trim(), reject: false, timeout: 15000,
318
+ });
319
+
320
+ if (exitCode === 0) {
321
+ console.log(chalk.green(` ✓ ECR authenticated (${ecrUrl})`));
322
+ } else {
323
+ console.log(chalk.red(` ✗ ECR docker login failed — image pulls may fail`));
324
+ }
325
+ } catch (err) {
326
+ console.log(chalk.red(` ✗ ECR auth error: ${err.message}`));
327
+ }
328
+ }
329
+
330
+ export async function checkEcrRepos(dir, awsConfig) {
331
+ const profile = awsConfig?.profileName || "dev";
332
+ const ecrInfo = detectEcrRegistry(dir);
333
+ if (!ecrInfo) return;
334
+
335
+ const ecrPrefix = `${ecrInfo.accountId}.dkr.ecr.${ecrInfo.region}.amazonaws.com/`;
336
+ try {
337
+ const composePath = path.join(dir, "docker-compose.yaml");
338
+ if (!fs.existsSync(composePath)) return;
339
+ const content = fs.readFileSync(composePath, "utf8");
340
+ const imageRefs = [...content.matchAll(/image:\s*(.+)/g)]
341
+ .map((m) => m[1].trim())
342
+ .filter((img) => img.includes(".dkr.ecr.") && img.includes(".amazonaws.com"));
343
+ const neededRepos = [...new Set(
344
+ imageRefs.map((img) => {
345
+ const noTag = img.replace(/\$\{[^}]+\}/, "compose").split(":")[0];
346
+ return noTag.replace(ecrPrefix, "").replace(/^\//, "");
347
+ })
348
+ )];
349
+ const { stdout } = await execa("aws", [
350
+ "ecr", "describe-repositories",
351
+ "--registry-id", ecrInfo.accountId,
352
+ "--region", ecrInfo.region,
353
+ "--profile", profile,
354
+ "--query", "repositories[].repositoryName",
355
+ "--output", "json",
356
+ ], { timeout: 10000 });
357
+ const existingRepos = JSON.parse(stdout);
358
+ const missing = neededRepos.filter((r) => !existingRepos.includes(r));
359
+ if (missing.length > 0) {
360
+ console.log(chalk.yellow("\n⚠ These ECR repos are referenced but don't exist (will need local build):"));
361
+ for (const r of missing) console.log(chalk.gray(` ✗ ${r}`));
362
+ console.log(chalk.gray(" These services will be built from source instead.\n"));
363
+ } else {
364
+ console.log(chalk.green("All required ECR repos exist."));
365
+ }
366
+ } catch {
367
+ // Non-critical
368
+ }
369
+ }
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ describe("setup/aws", () => {
7
+ let tmpDir;
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fops-aws-"));
11
+ });
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true });
15
+ });
16
+
17
+ describe("readFopsConfig / saveFopsConfig", () => {
18
+ it("readFopsConfig returns empty object when file does not exist", () => {
19
+ const configPath = path.join(tmpDir, ".fops.json");
20
+ expect(fs.existsSync(configPath)).toBe(false);
21
+ let config = {};
22
+ try {
23
+ if (fs.existsSync(configPath)) {
24
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
25
+ }
26
+ } catch {}
27
+ expect(config).toEqual({});
28
+ });
29
+
30
+ it("saveFopsConfig writes valid JSON", () => {
31
+ const configPath = path.join(tmpDir, ".fops.json");
32
+ const data = { aws: { profile: "dev" } };
33
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
34
+ const read = JSON.parse(fs.readFileSync(configPath, "utf8"));
35
+ expect(read).toEqual(data);
36
+ });
37
+
38
+ it("readFopsConfig handles corrupted JSON gracefully", () => {
39
+ const configPath = path.join(tmpDir, ".fops.json");
40
+ fs.writeFileSync(configPath, "not valid json{");
41
+ let config = {};
42
+ try {
43
+ if (fs.existsSync(configPath)) {
44
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
45
+ }
46
+ } catch {}
47
+ expect(config).toEqual({});
48
+ });
49
+
50
+ it("round-trips nested config", () => {
51
+ const configPath = path.join(tmpDir, ".fops.json");
52
+ const data = {
53
+ aws: { profile: "dev", region: "us-east-1" },
54
+ plugins: { entries: { "my-plugin": { enabled: true, config: { key: "val" } } } },
55
+ };
56
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n");
57
+ const read = JSON.parse(fs.readFileSync(configPath, "utf8"));
58
+ expect(read).toEqual(data);
59
+ });
60
+ });
61
+
62
+ describe("detectEcrRegistry", () => {
63
+ let detectEcrRegistry;
64
+
65
+ beforeEach(async () => {
66
+ ({ detectEcrRegistry } = await import("./aws.js"));
67
+ });
68
+
69
+ it("returns null when no docker-compose.yaml", () => {
70
+ expect(detectEcrRegistry(tmpDir)).toBe(null);
71
+ });
72
+
73
+ it("returns null when no ECR URL in compose", () => {
74
+ fs.writeFileSync(path.join(tmpDir, "docker-compose.yaml"), "version: '3'\nservices:\n web:\n image: nginx\n");
75
+ expect(detectEcrRegistry(tmpDir)).toBe(null);
76
+ });
77
+
78
+ it("extracts ECR info from docker-compose.yaml", () => {
79
+ fs.writeFileSync(
80
+ path.join(tmpDir, "docker-compose.yaml"),
81
+ `services:\n app:\n image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest\n`
82
+ );
83
+ const result = detectEcrRegistry(tmpDir);
84
+ expect(result).toEqual({ accountId: "123456789012", region: "us-east-1" });
85
+ });
86
+
87
+ it("extracts different regions", () => {
88
+ fs.writeFileSync(
89
+ path.join(tmpDir, "docker-compose.yaml"),
90
+ `services:\n app:\n image: 987654321098.dkr.ecr.eu-west-1.amazonaws.com/app:v1\n`
91
+ );
92
+ const result = detectEcrRegistry(tmpDir);
93
+ expect(result).toEqual({ accountId: "987654321098", region: "eu-west-1" });
94
+ });
95
+
96
+ it("handles multiple ECR references (returns first match)", () => {
97
+ fs.writeFileSync(
98
+ path.join(tmpDir, "docker-compose.yaml"),
99
+ [
100
+ "services:",
101
+ " app1:",
102
+ " image: 111111111111.dkr.ecr.us-east-1.amazonaws.com/app1:latest",
103
+ " app2:",
104
+ " image: 222222222222.dkr.ecr.eu-west-1.amazonaws.com/app2:latest",
105
+ ].join("\n")
106
+ );
107
+ const result = detectEcrRegistry(tmpDir);
108
+ expect(result.accountId).toBe("111111111111");
109
+ });
110
+
111
+ it("returns null for compose with no image directives", () => {
112
+ fs.writeFileSync(
113
+ path.join(tmpDir, "docker-compose.yaml"),
114
+ "services:\n app:\n build: .\n"
115
+ );
116
+ expect(detectEcrRegistry(tmpDir)).toBe(null);
117
+ });
118
+ });
119
+
120
+ describe("detectAwsSsoProfiles", () => {
121
+ let detectAwsSsoProfiles;
122
+
123
+ beforeEach(async () => {
124
+ ({ detectAwsSsoProfiles } = await import("./aws.js"));
125
+ });
126
+
127
+ it("returns an array", () => {
128
+ const result = detectAwsSsoProfiles();
129
+ expect(Array.isArray(result)).toBe(true);
130
+ });
131
+ });
132
+
133
+ describe("detectAwsSsoProfiles parsing logic", () => {
134
+ it("parses SSO profiles from aws config content", () => {
135
+ const configContent = `[profile dev]
136
+ sso_session = meshx
137
+ sso_account_id = 123456789012
138
+ sso_role_name = AdministratorAccess
139
+ region = us-east-1
140
+
141
+ [sso-session meshx]
142
+ sso_start_url = https://meshx.awsapps.com/start
143
+ sso_region = us-east-1
144
+
145
+ [profile staging]
146
+ sso_session = meshx
147
+ sso_account_id = 987654321098
148
+ sso_role_name = ReadOnly
149
+ region = us-west-2
150
+ `;
151
+ // Inline the parsing logic (same as detectAwsSsoProfiles)
152
+ const content = configContent;
153
+ const profiles = [];
154
+ let currentProfile = null;
155
+ let currentAttrs = {};
156
+ for (const line of content.split("\n")) {
157
+ const profileMatch = line.match(/^\[profile\s+(.+?)\]/);
158
+ if (profileMatch) {
159
+ if (currentProfile && currentAttrs.sso_session) {
160
+ profiles.push({ name: currentProfile, ...currentAttrs });
161
+ }
162
+ currentProfile = profileMatch[1];
163
+ currentAttrs = {};
164
+ continue;
165
+ }
166
+ if (line.startsWith("[")) {
167
+ if (currentProfile && currentAttrs.sso_session) {
168
+ profiles.push({ name: currentProfile, ...currentAttrs });
169
+ }
170
+ currentProfile = null;
171
+ currentAttrs = {};
172
+ continue;
173
+ }
174
+ const kv = line.match(/^\s*(\S+)\s*=\s*(.+)/);
175
+ if (kv && currentProfile) {
176
+ currentAttrs[kv[1]] = kv[2].trim();
177
+ }
178
+ }
179
+ if (currentProfile && currentAttrs.sso_session) {
180
+ profiles.push({ name: currentProfile, ...currentAttrs });
181
+ }
182
+
183
+ expect(profiles).toHaveLength(2);
184
+ expect(profiles[0].name).toBe("dev");
185
+ expect(profiles[0].sso_session).toBe("meshx");
186
+ expect(profiles[0].sso_account_id).toBe("123456789012");
187
+ expect(profiles[0].region).toBe("us-east-1");
188
+ expect(profiles[1].name).toBe("staging");
189
+ expect(profiles[1].sso_account_id).toBe("987654321098");
190
+ expect(profiles[1].region).toBe("us-west-2");
191
+ });
192
+
193
+ it("skips profiles without sso_session", () => {
194
+ const content = `[profile default]
195
+ region = us-east-1
196
+ output = json
197
+
198
+ [profile sso-dev]
199
+ sso_session = meshx
200
+ region = us-east-1
201
+ `;
202
+ const profiles = [];
203
+ let currentProfile = null;
204
+ let currentAttrs = {};
205
+ for (const line of content.split("\n")) {
206
+ const profileMatch = line.match(/^\[profile\s+(.+?)\]/);
207
+ if (profileMatch) {
208
+ if (currentProfile && currentAttrs.sso_session) {
209
+ profiles.push({ name: currentProfile, ...currentAttrs });
210
+ }
211
+ currentProfile = profileMatch[1];
212
+ currentAttrs = {};
213
+ continue;
214
+ }
215
+ if (line.startsWith("[")) {
216
+ if (currentProfile && currentAttrs.sso_session) {
217
+ profiles.push({ name: currentProfile, ...currentAttrs });
218
+ }
219
+ currentProfile = null;
220
+ currentAttrs = {};
221
+ continue;
222
+ }
223
+ const kv = line.match(/^\s*(\S+)\s*=\s*(.+)/);
224
+ if (kv && currentProfile) {
225
+ currentAttrs[kv[1]] = kv[2].trim();
226
+ }
227
+ }
228
+ if (currentProfile && currentAttrs.sso_session) {
229
+ profiles.push({ name: currentProfile, ...currentAttrs });
230
+ }
231
+
232
+ expect(profiles).toHaveLength(1);
233
+ expect(profiles[0].name).toBe("sso-dev");
234
+ });
235
+
236
+ it("handles empty config", () => {
237
+ const profiles = [];
238
+ // No profiles to parse
239
+ expect(profiles).toHaveLength(0);
240
+ });
241
+
242
+ it("handles profile at end of file (no trailing section)", () => {
243
+ const content = `[profile last]
244
+ sso_session = test
245
+ region = us-east-1`;
246
+ const profiles = [];
247
+ let currentProfile = null;
248
+ let currentAttrs = {};
249
+ for (const line of content.split("\n")) {
250
+ const profileMatch = line.match(/^\[profile\s+(.+?)\]/);
251
+ if (profileMatch) {
252
+ if (currentProfile && currentAttrs.sso_session) {
253
+ profiles.push({ name: currentProfile, ...currentAttrs });
254
+ }
255
+ currentProfile = profileMatch[1];
256
+ currentAttrs = {};
257
+ continue;
258
+ }
259
+ if (line.startsWith("[")) {
260
+ if (currentProfile && currentAttrs.sso_session) {
261
+ profiles.push({ name: currentProfile, ...currentAttrs });
262
+ }
263
+ currentProfile = null;
264
+ currentAttrs = {};
265
+ continue;
266
+ }
267
+ const kv = line.match(/^\s*(\S+)\s*=\s*(.+)/);
268
+ if (kv && currentProfile) {
269
+ currentAttrs[kv[1]] = kv[2].trim();
270
+ }
271
+ }
272
+ if (currentProfile && currentAttrs.sso_session) {
273
+ profiles.push({ name: currentProfile, ...currentAttrs });
274
+ }
275
+
276
+ expect(profiles).toHaveLength(1);
277
+ expect(profiles[0].name).toBe("last");
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,3 @@
1
+ export { runSetup } from "./setup.js";
2
+ export { runInitWizard } from "./wizard.js";
3
+ export { checkEcrRepos } from "./aws.js";