You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a hulak migrate env command that converts existing env/*.env files into the encrypted .hulak/store.age format.
Background
After #127, hulak detects which storage backend to use:
.hulak/store.age exists → vault mode (primary)
only env/ exists → classic mode (legacy)
So users do not need to delete env/ to start using the vault — once store.age exists, it takes priority automatically. Migration is purely a one-time copy operation. Users can verify the migration, then delete env/ whenever they're comfortable.
Command
hulak migrate env
# ✓ Migrated global.env → store.age[global] (3 keys)# ✓ Migrated staging.env → store.age[staging] (5 keys)# ✓ Migrated prod.env → store.age[prod] (4 keys)## No keypair found. Generating one...# ✓ Identity stored at ~/.config/hulak/identity.txt# ✓ Recipients file written to .hulak/recipients.txt## ⚠ Save this recovery key somewhere safe (you won't see it again):# AGE-SECRET-KEY-1QF...# Anyone with this key can decrypt store.age.## Note: env/ is untouched. The encrypted store now takes priority,# but you can keep env/ around until you're confident in the migration.# Delete it manually when ready: rm -rf env/
Parse with existing envparser.LoadEnvVars() (preserves type inference)
Derive environment name by stripping .env suffix
Add as section in store
If store.age already exists, merge with existing-store-wins precedence (don't overwrite secrets the user already set in the vault)
Encrypt and write store.age via vault.WriteStore()
Print summary + reminder about env/ cleanup
Print recovery key warning only on first-run keypair generation
Edge cases
env/ doesn't exist → error: "No env/ directory found. Nothing to migrate."
store.age already has some environments → merge, existing store wins on conflicts (so re-running migrate is safe and won't clobber post-migration edits)
Empty .env files → create empty section in store
Files that aren't .env (e.g., .env.bak, .env.example) → skip with warning
Migration does NOT delete env/ — user does that manually after verification
Files to Create/Change
File
Change
pkg/userFlags/subcommands.go
Wire up migrate env subcommand
pkg/migration/envmigration.go
NEW — migration logic
Relation to existing hulak migrate
Today hulak migrate handles Postman collection/environment import. This adds an env subcommand:
$SYSTEM_VAR pass-through values: envparser supports $VAR references in .env values that are resolved against the host env at runtime. Migration must store the literal $VAR string in the encrypted store, not the resolved value, so the runtime template engine keeps doing the same pass-through after migration.
Partial pre-existing setup: identity file exists but .hulak/recipients.txt doesn't (e.g. project predates vault: multi-recipient encryption for team sharing #144). Migrate must bootstrap recipients.txt from key.pub before encrypting, not assume a clean slate.
Symlinked env/: follow the symlink and migrate the target. If the symlink is broken, error clearly rather than panicking.
env/ is a regular file, not a directory: graceful "expected env/ to be a directory" error.
stderr discipline: per project conventions, all migration progress (✓ Migrated..., recovery key warning, etc.) goes to stderr. stdout stays clean for piped consumers.
.env files containing only comments / blank lines: still create a section in the store (empty), don't skip silently — the user explicitly wants that environment to exist.
Duplicate keys within one .env file: last-wins (matches LoadEnvVars today), no warning needed.
Run when store.age already has a recipient set: do not regenerate the keypair; reuse the existing identity. The "Save this recovery key" warning prints only when EnsureKeypair actually generated new keys — gate on IdentityExists() snapshot taken before the call.
Summary
Add a
hulak migrate envcommand that converts existingenv/*.envfiles into the encrypted.hulak/store.ageformat.Background
After #127, hulak detects which storage backend to use:
.hulak/store.ageexists → vault mode (primary)env/exists → classic mode (legacy)So users do not need to delete
env/to start using the vault — oncestore.ageexists, it takes priority automatically. Migration is purely a one-time copy operation. Users can verify the migration, then deleteenv/whenever they're comfortable.Command
Flow
env/directory existsvault.EnsureKeypair()— creates.hulak/, identity file, andrecipients.txtif needed (see vault: multi-recipient encryption for team sharing #144)env/for*.envfilesenvparser.LoadEnvVars()(preserves type inference).envsuffixstore.agealready exists, merge with existing-store-wins precedence (don't overwrite secrets the user already set in the vault)store.ageviavault.WriteStore()env/cleanupEdge cases
env/doesn't exist → error: "No env/ directory found. Nothing to migrate."store.agealready has some environments → merge, existing store wins on conflicts (so re-running migrate is safe and won't clobber post-migration edits).envfiles → create empty section in store.env(e.g.,.env.bak,.env.example) → skip with warningenv/— user does that manually after verificationFiles to Create/Change
pkg/userFlags/subcommands.gomigrate envsubcommandpkg/migration/envmigration.goRelation to existing
hulak migrateToday
hulak migratehandles Postman collection/environment import. This adds anenvsubcommand:hulak migrate <env.json> <collection.json>— existing Postman migration (unchanged)hulak migrate env— NEW:env/*.env→store.agemigrationTests
.envfile.envfilesstore.age(existing values win).envfiles (e.g.,.env.bak)env/directory → still creates empty store + identityenv/directory → errorDepends on
Blocks
Nothing — utility command.
Additional cases to cover (review pass)
$SYSTEM_VARpass-through values:envparsersupports$VARreferences in.envvalues that are resolved against the host env at runtime. Migration must store the literal$VARstring in the encrypted store, not the resolved value, so the runtime template engine keeps doing the same pass-through after migration..hulak/recipients.txtdoesn't (e.g. project predates vault: multi-recipient encryption for team sharing #144). Migrate must bootstraprecipients.txtfromkey.pubbefore encrypting, not assume a clean slate.env/: follow the symlink and migrate the target. If the symlink is broken, error clearly rather than panicking.env/is a regular file, not a directory: graceful "expected env/ to be a directory" error.✓ Migrated..., recovery key warning, etc.) goes to stderr. stdout stays clean for piped consumers..envfiles containing only comments / blank lines: still create a section in the store (empty), don't skip silently — the user explicitly wants that environment to exist..envfile: last-wins (matchesLoadEnvVarstoday), no warning needed.store.agealready has a recipient set: do not regenerate the keypair; reuse the existing identity. The "Save this recovery key" warning prints only whenEnsureKeypairactually generated new keys — gate onIdentityExists()snapshot taken before the call.