simple, clean application directory resolution
Application directory resolution for Node.js applications
Simple, clean application directory resolution for Node.js applications.
npm install appstash- Simple API: Just one function to get all your app directories
- Clean structure:
~/.<tool>/{config,cache,data,logs}+/tmp/<tool> - Graceful fallback: XDG directories → tmp if home fails
- No throws: Always returns valid paths, never throws errors
- TypeScript: Full type definitions included
- Zero dependencies: Pure Node.js implementation
import { appstash } from 'appstash';
// Get directories for your tool
const dirs = appstash('pgpm');
console.log(dirs.config); // ~/.pgpm/config
console.log(dirs.cache); // ~/.pgpm/cache
console.log(dirs.data); // ~/.pgpm/data
console.log(dirs.logs); // ~/.pgpm/logs
console.log(dirs.tmp); // /tmp/pgpmimport { appstash } from 'appstash';
// Get directories and create them
const dirs = appstash('pgpm', { ensure: true });
// All directories now exist
// dirs.usedFallback will be true if XDG or tmp fallback was usedimport { appstash, resolve } from 'appstash';
const dirs = appstash('pgpm');
// Resolve paths within directories
const configFile = resolve(dirs, 'config', 'settings.json');
// Returns: ~/.pgpm/config/settings.json
const cacheDir = resolve(dirs, 'cache', 'repos', 'my-repo');
// Returns: ~/.pgpm/cache/repos/my-repoimport { appstash, ensure } from 'appstash';
const dirs = appstash('pgpm');
// Create directories later
const result = ensure(dirs);
console.log(result.created); // ['~/.pgpm', '~/.pgpm/config', ...]
console.log(result.usedFallback); // false (or true if fallback was used)Get application directories for a tool.
Parameters:
tool(string): Tool name (e.g., 'pgpm', 'lql')options(object, optional):baseDir(string): Base directory (defaults toos.homedir())useXdgFallback(boolean): Use XDG fallback if home fails (default:true)ensure(boolean): Automatically create directories (default:false)tmpRoot(string): Root for temp directory (defaults toos.tmpdir())
Returns: AppStashResult
{
root: string; // ~/.<tool>
config: string; // ~/.<tool>/config
cache: string; // ~/.<tool>/cache
data: string; // ~/.<tool>/data
logs: string; // ~/.<tool>/logs
tmp: string; // /tmp/<tool>
usedFallback?: boolean; // true if XDG or tmp fallback was used
}Create directories if they don't exist. Never throws.
Parameters:
dirs(AppStashResult): Directory paths fromappstash()
Returns: EnsureResult
{
created: string[]; // Directories that were created
usedFallback: boolean; // true if XDG or tmp fallback was used
}Resolve a path within a specific directory.
Parameters:
dirs(AppStashResult): Directory paths fromappstash()kind('config' | 'cache' | 'data' | 'logs' | 'tmp'): Directory kindparts(string[]): Path parts to join
Returns: string - Resolved path
~/.<tool>/
├── config/ # Configuration files
├── cache/ # Cached data
├── data/ # Application data
└── logs/ # Log files
/tmp/<tool>/ # Temporary files
If home directory is unavailable or creation fails, falls back to XDG:
~/.config/<tool>/ # Config
~/.cache/<tool>/ # Cache
~/.local/share/<tool>/ # Data
~/.local/state/<tool>/logs/ # Logs
If XDG also fails, falls back to system temp:
/tmp/<tool>/
├── config/
├── cache/
├── data/
└── logs/
import { appstash, resolve } from 'appstash';
import fs from 'fs';
const dirs = appstash('myapp', { ensure: true });
const configPath = resolve(dirs, 'config', 'settings.json');
// Write config
fs.writeFileSync(configPath, JSON.stringify({ theme: 'dark' }));
// Read config
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));import { appstash, resolve } from 'appstash';
import fs from 'fs';
const dirs = appstash('myapp', { ensure: true });
const cacheFile = resolve(dirs, 'cache', 'data.json');
// Check if cached
if (fs.existsSync(cacheFile)) {
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
console.log('Using cached data:', cached);
} else {
// Fetch and cache
const data = await fetchData();
fs.writeFileSync(cacheFile, JSON.stringify(data));
}import { appstash, resolve } from 'appstash';
import fs from 'fs';
const dirs = appstash('myapp', { ensure: true });
const logFile = resolve(dirs, 'logs', 'app.log');
function log(message: string) {
const timestamp = new Date().toISOString();
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
}
log('Application started');import { appstash } from 'appstash';
// Use a custom base directory
const dirs = appstash('myapp', {
baseDir: '/opt/myapp',
ensure: true
});
console.log(dirs.config); // /opt/myapp/.myapp/configcreateConfigStore(tool, options?) layers a context + credential store on top of the
directories above: named contexts (each with an endpoint and optional per-target
endpoints), credentials per context, per-context vars, and getClientConfig(target)
resolution (store → env vars → actionable error).
Every file it writes is atomic (temp file + rename) and mode 0600. A stored file
that exists but does not parse throws with its path — it is never silently replaced
with defaults.
Pass stashName when multiple binaries are really one product. They then share
contexts and credentials, while tool still drives env-var prefixes (CSDK_TOKEN)
and the command names in error messages:
// A generated SDK CLI, an agent CLI and a desktop app, one login:
createConfigStore('csdk', { stashName: 'constructive' });
createConfigStore('agent', { stashName: 'constructive' });
createConfigStore('desktop', { stashName: 'constructive' });Supply a SecretCodec to transform the secret-bearing fields (token,
refreshToken, apiKey) on the way to disk; everything else stays readable. The
codec name is recorded in credentials.json, so a file written by a different codec
is reported instead of decoded into garbage:
import { safeStorage } from 'electron';
createConfigStore('desktop', {
stashName: 'constructive',
codec: {
name: 'electron-safeStorage',
encode: (s) => safeStorage.encryptString(s).toString('base64'),
decode: (s) => safeStorage.decryptString(Buffer.from(s, 'base64'))
}
});- Simple: One function, clear structure
- Clean: No pollution of exports, minimal API surface
- Graceful: Never throws, always returns valid paths
- Fallback: XDG only as absolute fallback, not primary
- Focused: Just directory resolution, no state management