Skip to content

feat(appstash): add createConfigStore() for context and credential management#63

Merged
pyramation merged 1 commit intomainfrom
devin/1771313494-appstash-config-store
Feb 17, 2026
Merged

feat(appstash): add createConfigStore() for context and credential management#63
pyramation merged 1 commit intomainfrom
devin/1771313494-appstash-config-store

Conversation

@pyramation
Copy link
Contributor

@pyramation pyramation commented Feb 17, 2026

feat(appstash): add createConfigStore() for context and credential management

Summary

Adds a higher-level createConfigStore(toolName) API to appstash that provides kubectl-style context and credential management. This is a direct extraction and parameterization of the existing config-manager from the cnc CLI (packages/cli/src/config/config-manager.ts in the constructive repo), generalized so any tool can use it.

New API:

  • createConfigStore(toolName, options?)ConfigStore
  • Context CRUD: createContext, loadContext, listContexts, deleteContext, getCurrentContext, setCurrentContext
  • Credential CRUD: setCredentials, getCredentials, removeCredentials, hasValidCredentials
  • Settings: loadSettings, saveSettings

Storage layout uses the existing appstash directory structure: ~/.{toolName}/config/settings.json, ~/.{toolName}/config/contexts/{name}.json, ~/.{toolName}/config/credentials.json (mode 0o600).

Files changed:

  • packages/appstash/src/config-store.ts — new module (~220 lines)
  • packages/appstash/src/index.ts — re-exports createConfigStore + types
  • packages/appstash/__tests__/config-store.test.ts — 30 test cases covering settings, context CRUD, credentials, expiry validation, tool isolation, and a full end-to-end workflow

Review & Testing Checklist for Human

  • Path traversal on context names: contextPath() uses the name parameter directly in ${name}.json with no sanitization. A name like ../../foo would write outside the contexts/ directory. Consider whether input validation is needed here or if it's the caller's responsibility.
  • Mutable DEFAULT_SETTINGS reference: loadSettings() returns the DEFAULT_SETTINGS object directly when no file exists. If the caller mutates the returned object (e.g., settings.currentContext = 'x'), it would mutate the module-level default. Verify this is acceptable or if a spread/clone is needed.
  • Credentials file permission on overwrite: writeJson passes mode: 0o600 to writeFileSync, but verify that Node.js actually applies the mode when overwriting an existing file (vs. only on creation).
  • contextPath side-effect: contextPath() creates the contexts/ directory on every call, including reads. This is harmless but worth being aware of.

Recommended test plan: Run pnpm test in packages/appstash/ — all 46 tests should pass (16 original + 30 new). Also run pnpm build to verify the TypeScript compiles cleanly.

Notes

  • Builds and tests pass locally. Lint fails due to a pre-existing ESLint 9 migration issue (no eslint.config.js found) — not introduced by this PR.
  • This PR is part of a larger effort to support codegen-based GraphQL CLI generation (see plan in the Devin session).
  • Link to Devin run: https://app.devin.ai/sessions/e9c668834d394cdc8ebcb371b6ebf430
  • Requested by: @pyramation

Open with Devin

@devin-ai-integration
Copy link

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@pyramation pyramation merged commit 4a75244 into main Feb 17, 2026
36 checks passed
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

}

function loadSettings(): GlobalSettings {
return readJson(settingsPath(), DEFAULT_SETTINGS);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Shared mutable DEFAULT_SETTINGS object is mutated via loadSettings(), polluting state across store instances

When the settings file doesn't exist, loadSettings() returns the module-level DEFAULT_SETTINGS object reference directly (via readJson at line 89). Callers like setCurrentContext() and deleteContext() then mutate this returned object (e.g., settings.currentContext = name at line 158), permanently polluting the shared default.

Root Cause and Impact

The readJson function at packages/appstash/src/config-store.ts:49-58 returns the fallback parameter by reference when the file doesn't exist. Since loadSettings() passes the module-level DEFAULT_SETTINGS constant (packages/appstash/src/config-store.ts:47), any mutation to the returned object mutates DEFAULT_SETTINGS itself.

The mutation happens in two places:

  • setCurrentContext() at line 158: settings.currentContext = name;
  • deleteContext() at line 136: settings.currentContext = undefined;

Scenario: In a single process, if store A calls setCurrentContext('production') before its settings file exists, DEFAULT_SETTINGS becomes { currentContext: 'production' }. Later, if store B (for a different tool) calls loadSettings() before its own settings file exists, it receives { currentContext: 'production' } instead of {}, causing it to incorrectly think it has an active context.

While saveSettings does write to disk (so subsequent reads from the same store will read from the file), the in-memory DEFAULT_SETTINGS remains polluted for any other store instance or any call path where the file doesn't yet exist.

Suggested change
return readJson(settingsPath(), DEFAULT_SETTINGS);
return readJson(settingsPath(), { ...DEFAULT_SETTINGS });
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant