claude-aws-mfa 1.2.0 → 1.2.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.
- package/.github/workflows/release.yml +9 -3
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/config.ts +9 -5
- package/test/config.test.ts +7 -3
- package/test/gui-screenshot.test.ts +28 -11
|
@@ -110,9 +110,17 @@ jobs:
|
|
|
110
110
|
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
|
111
111
|
echo "Detected version=$VERSION prerelease=$PRERELEASE"
|
|
112
112
|
|
|
113
|
-
release
|
|
113
|
+
# v1.2.0 shipped while the Test workflow was red: the release ran its own
|
|
114
|
+
# `bun test` on ubuntu only, so a Windows-only failure never reached it.
|
|
115
|
+
# Gate on the whole matrix instead, and only when actually releasing.
|
|
116
|
+
test:
|
|
114
117
|
needs: detect-release
|
|
115
118
|
if: needs.detect-release.outputs.version != ''
|
|
119
|
+
uses: ./.github/workflows/test.yml
|
|
120
|
+
|
|
121
|
+
release:
|
|
122
|
+
needs: [detect-release, test]
|
|
123
|
+
if: needs.detect-release.outputs.version != ''
|
|
116
124
|
runs-on: ubuntu-latest
|
|
117
125
|
permissions:
|
|
118
126
|
contents: read
|
|
@@ -128,8 +136,6 @@ jobs:
|
|
|
128
136
|
|
|
129
137
|
- run: bun install --frozen-lockfile
|
|
130
138
|
|
|
131
|
-
- run: bun test
|
|
132
|
-
|
|
133
139
|
- name: Set package version
|
|
134
140
|
run: |
|
|
135
141
|
VERSION="${{ needs.detect-release.outputs.version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,13 @@ Releases are automated via CI. To create a new release:
|
|
|
22
22
|
Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
|
|
23
23
|
(e.g. `v1.2.0-rc.1`) publish to npm `@next` and create a GitHub pre-release.
|
|
24
24
|
|
|
25
|
+
## [v1.2.1] - 2026-08-10
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Profile scoping works on Windows.** The per-profile config filename encoded only `/` and `.`, so a Windows `CLAUDE_CONFIG_DIR` kept its backslashes and drive colon — `~/.config/claude-aws-mfa<encoded>.json` then named an NTFS alternate data stream instead of a per-profile file. Separators are normalized and the colon encoded, so a Windows profile gets its own config and lock like everywhere else.
|
|
29
|
+
- **A release can no longer publish over a red build.** The release workflow ran its own `bun test` on Linux only, so v1.2.0 shipped while the Test workflow's Windows leg was failing. It now calls that workflow directly and waits for the full matrix — every OS, GUI tests included — before publishing.
|
|
30
|
+
- Dialog titles on Windows showed the full `C:\Users\you\.claude-profiles\work` instead of `~/.claude-profiles/work`: the home-prefix shortening only recognized `/` as a separator.
|
|
31
|
+
|
|
25
32
|
## [v1.2.0] - 2026-08-10
|
|
26
33
|
|
|
27
34
|
### Added
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -57,15 +57,18 @@ export interface Config {
|
|
|
57
57
|
export function profileSuffix(dir = process.env.CLAUDE_CONFIG_DIR): string {
|
|
58
58
|
if (!dir) return "";
|
|
59
59
|
const canonical = (p: string) => {
|
|
60
|
+
let real: string;
|
|
60
61
|
try {
|
|
61
|
-
|
|
62
|
+
real = realpathSync(p);
|
|
62
63
|
} catch {
|
|
63
|
-
|
|
64
|
+
real = resolve(p);
|
|
64
65
|
}
|
|
66
|
+
return real.replace(/\\/g, "/");
|
|
65
67
|
};
|
|
66
68
|
const real = canonical(dir);
|
|
67
69
|
if (real === canonical(join(homedir(), ".claude"))) return "";
|
|
68
|
-
|
|
70
|
+
// `:` goes too, or a Windows drive letter would make the filename an NTFS stream.
|
|
71
|
+
return real.replace(/[/.:]/g, "-");
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface ActiveProfile {
|
|
@@ -87,11 +90,12 @@ export const DEFAULT_PROFILE: ActiveProfile = { name: "default", path: "~/.claud
|
|
|
87
90
|
*/
|
|
88
91
|
export function activeProfile(dir = process.env.CLAUDE_CONFIG_DIR): ActiveProfile {
|
|
89
92
|
if (!dir || !profileSuffix(dir)) return DEFAULT_PROFILE;
|
|
90
|
-
const clean = dir.replace(
|
|
93
|
+
const clean = dir.replace(/[/\\]+$/, "");
|
|
91
94
|
const home = homedir();
|
|
95
|
+
const underHome = clean === home || (clean.startsWith(home) && /^[/\\]/.test(clean.slice(home.length)));
|
|
92
96
|
return {
|
|
93
97
|
name: basename(clean),
|
|
94
|
-
path:
|
|
98
|
+
path: underHome ? ("~" + clean.slice(home.length)).replace(/\\/g, "/") : clean,
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
|
package/test/config.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync, statSync, openSync, closeSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { activeProfile, missingConfigFields, profileSuffix } from "../src/config";
|
|
@@ -13,10 +13,14 @@ describe("profileSuffix", () => {
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
test("encodes the full path, not the basename, with / and . both as -", () => {
|
|
16
|
-
|
|
16
|
+
// resolve() puts a drive letter in front on Windows: "-" on POSIX, "D--" there.
|
|
17
|
+
const root = resolve("/").replace(/[/\\:]/g, "-");
|
|
18
|
+
expect(profileSuffix("/opt/profiles/work")).toBe(root + "opt-profiles-work");
|
|
17
19
|
// basename alone would collide; the full path keeps them distinct
|
|
18
20
|
expect(profileSuffix("/a/work")).not.toBe(profileSuffix("/b/work"));
|
|
19
|
-
expect(profileSuffix("/home/me/.claude-profiles/work")).toBe("
|
|
21
|
+
expect(profileSuffix("/home/me/.claude-profiles/work")).toBe(root + "home-me--claude-profiles-work");
|
|
22
|
+
// whatever the platform, the result has to be usable as a filename component
|
|
23
|
+
expect(profileSuffix("/opt/profiles/work")).not.toMatch(/[/\\:]/);
|
|
20
24
|
});
|
|
21
25
|
});
|
|
22
26
|
|
|
@@ -41,7 +41,10 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
41
41
|
typeof import("node-screenshots").Window
|
|
42
42
|
> | null = null;
|
|
43
43
|
|
|
44
|
+
let titles: string[] = [];
|
|
45
|
+
|
|
44
46
|
beforeAll(async () => {
|
|
47
|
+
const start = Date.now();
|
|
45
48
|
windowModule = await import("node-screenshots");
|
|
46
49
|
|
|
47
50
|
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
@@ -52,28 +55,42 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
52
55
|
stderr: "inherit",
|
|
53
56
|
});
|
|
54
57
|
|
|
55
|
-
// Poll for the window to appear
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
58
|
+
// Poll for the window to appear. The budget is 90s rather than a snug
|
|
59
|
+
// multiple of the ~8s a warm runner takes: on Windows the first WebView2
|
|
60
|
+
// launch on a fresh image bootstraps its runtime and user-data folder
|
|
61
|
+
// before any window exists, which is what made this test flaky in CI.
|
|
62
|
+
// Waiting longer costs nothing when the window shows up on schedule.
|
|
63
|
+
//
|
|
64
|
+
// Window.all() may throw if the window manager hasn't registered EWMH
|
|
65
|
+
// atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so we catch and
|
|
66
|
+
// retry.
|
|
67
|
+
const deadline = Date.now() + 90_000;
|
|
60
68
|
while (Date.now() < deadline) {
|
|
61
69
|
try {
|
|
62
70
|
const windows = windowModule.Window.all();
|
|
63
|
-
|
|
64
|
-
try { return w.title()
|
|
65
|
-
catch { return false; }
|
|
71
|
+
titles = windows.map((w) => {
|
|
72
|
+
try { return w.title() ?? ""; } catch { return ""; }
|
|
66
73
|
});
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const i = titles.findIndex((t) => t.includes("Claude AWS MFA"));
|
|
75
|
+
if (i >= 0) {
|
|
76
|
+
foundWindow = windows[i]!;
|
|
69
77
|
break;
|
|
70
78
|
}
|
|
71
79
|
} catch {
|
|
72
80
|
// Window.all() not yet available — WM still initializing
|
|
73
81
|
}
|
|
82
|
+
// A dead launcher will never produce a window; stop and let the
|
|
83
|
+
// "app launches without error" assertion report the exit code.
|
|
84
|
+
if (proc.exitCode !== null) break;
|
|
74
85
|
await Bun.sleep(500);
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
if (!foundWindow) {
|
|
88
|
+
// Without this a failure is a bare `null` — no way to tell "window never
|
|
89
|
+
// opened" from "opened under a title we didn't match".
|
|
90
|
+
console.log(`No matching window after ${Math.round((Date.now() - start) / 1000)}s.`);
|
|
91
|
+
console.log(`Launcher exitCode: ${proc.exitCode}. Visible titles: ${JSON.stringify(titles)}`);
|
|
92
|
+
}
|
|
93
|
+
}, 120_000);
|
|
77
94
|
|
|
78
95
|
afterAll(() => {
|
|
79
96
|
proc?.kill();
|