feat(profiles): 3rd-party API endpoints via per-profile env#5
Conversation
Shahinyanm
left a comment
There was a problem hiding this comment.
Really nice work — this is basically a superset of #3 and you already folded in the three notes I left there. The interactive flow, the chmod 600 default and the oauth/api split in status all read clean, and the parseDotenv tests cover the cases I'd have worried about.
There's one thing I'd like fixed before merging, plus a few small notes you can take or leave.
Blocking — chmod 600 isn't actually enforced on rewrite.
writeProfileDotEnv passes { mode: 0o600 } to writeFileSync, but Node only applies that mode when it creates the file. If the .env already exists, the mode argument is ignored and the old permissions stick. So the path that bites: someone ends up with a 0644 .env (created by hand, or the #3 way), then runs aimux profile update -e KEY=VAL → mergeProfileDotEnv rewrites it → it stays 0644. The docstring says "Re-writes with chmod 600", which is the behaviour I'd want, just not what happens today. Your perm test passes because it writes into a fresh tmp dir where the file doesn't exist yet.
One extra line fixes it — chmodSync(target, 0o600) right after the write. A merge-over-an-existing-0644-file test would lock it in.
Smaller stuff, non-blocking:
checkDotenvPermissionsonly runs onaimux run. Since env is injected intoauth logintoo, the same warning there would be consistent.- Name collision in
profile add --api:addProfilethrows aftercollectApiCredentials, so if the name's already taken the user types the token blind and then hits the error. Checking the name before prompting would be kinder. Minor. seedApiClaudeJsonwrites just{ hasCompletedOnboarding: true }— worth a quick sanity check against a current Claude Code build that it's enough to actually skip onboarding (newer versions sometimes want a couple more fields). If it works for you, it's fine.
Fix the chmod one and I'm happy to merge. Thanks for the thorough tests.
| for (const [key, value] of Object.entries(vars)) { | ||
| lines.push(`${key}=${serializeDotenvValue(value)}`); | ||
| } | ||
| writeFileSync(join(profilePath, '.env'), lines.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600 }); |
There was a problem hiding this comment.
This mode: 0o600 only takes effect when the file is created. On an overwrite (e.g. via mergeProfileDotEnv) Node ignores it and keeps the existing permissions. A chmodSync(join(profilePath, '.env'), 0o600) right after the write makes the 600 guarantee hold every time.
Adds a way to point a profile at a self-hosted or 3rd-party
Claude-compatible API endpoint instead of an Anthropic subscription.
Core (env injection):
- parseDotenv + loadProfileEnv in run.ts: merge a profile's <path>/.env
dotenv file with an optional config.yaml `env:` block (YAML wins on
conflict); injected into both `aimux run` and `aimux auth login`
- ProfileConfig.env type + config validation; `.env` added to
DEFAULT_PRIVATE_ELEMENTS so it is never symlinked to the shared source
- dotenv parser strips inline comments AFTER quoted values too
(fixes the `FOO="bar" # x` case); no ${VAR}/multi-line by design
UX (apiProfile.ts — additive, self-contained):
- `aimux profile add <name> --api`: interactive prompt (Base URL, hidden
auth token, per-tier models). Credentials collected before any disk
write so Ctrl+C leaves no orphan; .env written chmod 600
- Single-consumer StdinLineReader for the prompt flow — avoids the
lost-input / unsettled-promise quirks of repeated readline interfaces;
masks the secret in TTY, handles piped input for CI
- `aimux profile update -e KEY=VALUE / --unset-env KEY` edits .env in place
- `aimux run` warns when a profile .env is group/other-readable
- StatusView distinguishes oauth vs api (N vars)
Tests: parseDotenv (incl. quoted-comment bugfix), loadProfileEnv override
order, writeProfileDotEnv 0600 round-trip, mergeProfileDotEnv, perms check.
README + design doc updated.
40d9dce to
fee2335
Compare
|
Thanks for the careful review — all addressed in Blocking — chmod 600 on rewrite: fixed. Non-blocking, also done:
seedApiClaudeJson: verified against the current Claude Code build (v2.1.156) — 131 tests green. Ready for another look 🙏 |
What
Lets a profile point at a self-hosted / 3rd-party Claude-compatible API endpoint instead of an Anthropic subscription.
How
Core (env injection)
parseDotenv+loadProfileEnvinrun.ts: merge a profile's<path>/.envwith an optionalconfig.yamlenv:block (YAML wins on conflict); injected into bothaimux runandaimux auth login.ProfileConfig.envtype + validation;.envadded toDEFAULT_PRIVATE_ELEMENTS(never symlinked to the shared source).UX (
apiProfile.ts, self-contained)aimux profile add --api: interactive prompt; credentials collected before any disk write (Ctrl+C leaves no orphan);.envwrittenchmod 600; seeds a minimal.claude.jsonso Claude Code skips onboarding/OAuth.aimux profile update -e KEY=VALUE / --unset-env KEYedits.envin place.aimux runwarns when a profile.envis group/other-readable.StatusViewdistinguishesoauthvsapi (N vars).StdinLineReaderfor the prompt flow — avoids readline's lost-input / unsettled-promise quirks and CRLF double-line on Node 22+; masks the secret in TTY, handles piped input for CI.Relationship to #3
This overlaps with #3 (per-profile env via
.env) and adopts the same core API surface (parseDotenv/loadProfileEnv/env:block) to keep the merge trivial. It additionally:FOO="bar" # xcase), documents no${VAR}/multi-line, and adds thechmod 600+ loose-permission runtime warning;--apiUX,.claude.jsonseeding, and the oauth/api status distinction on top.If #3 lands first, the core helpers dedupe trivially and this becomes the UX layer; if not, it stands alone.
Tests
130 passing. New:
parseDotenv(incl. quoted-comment + CRLF round-trip),loadProfileEnvoverride order,writeProfileDotEnv0600 round-trip,mergeProfileDotEnv, permission check.Closes #2