Skip to content

Commit e7507e6

Browse files
committed
feat: Add appstash package for simple application directory resolution
Create new appstash package with clean, minimal architecture: Features: - Simple API: appstash(tool, options) returns all 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 - Zero dependencies: Pure Node.js implementation - Full TypeScript support API: - appstash(tool, options): Get all directories for a tool - ensure(dirs): Create directories if they don't exist - resolve(dirs, kind, ...parts): Resolve paths within directories Directory structure: - Primary: ~/.<tool>/{config,cache,data,logs} - Fallback: XDG directories (only if home fails) - Final fallback: /tmp/<tool> Tests: 17 tests passing Build: Successful This is a relocation of @launchql/appdirs from launchql/launchql to hyperweb-io/dev-utils as a more generic utility package. Co-Authored-By: Dan Lynch <[email protected]>
1 parent 4b091f1 commit e7507e6

File tree

9 files changed

+3529
-5028
lines changed

9 files changed

+3529
-5028
lines changed

packages/appstash/README.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# appstash
2+
3+
Simple, clean application directory resolution for Node.js applications.
4+
5+
## Installation
6+
7+
```bash
8+
npm install appstash
9+
# or
10+
pnpm add appstash
11+
```
12+
13+
## Features
14+
15+
- **Simple API**: Just one function to get all your app directories
16+
- **Clean structure**: `~/.<tool>/{config,cache,data,logs}` + `/tmp/<tool>`
17+
- **Graceful fallback**: XDG directories → tmp if home fails
18+
- **No throws**: Always returns valid paths, never throws errors
19+
- **TypeScript**: Full type definitions included
20+
- **Zero dependencies**: Pure Node.js implementation
21+
22+
## Usage
23+
24+
### Basic Usage
25+
26+
```typescript
27+
import { appstash } from 'appstash';
28+
29+
// Get directories for your tool
30+
const dirs = appstash('pgpm');
31+
32+
console.log(dirs.config); // ~/.pgpm/config
33+
console.log(dirs.cache); // ~/.pgpm/cache
34+
console.log(dirs.data); // ~/.pgpm/data
35+
console.log(dirs.logs); // ~/.pgpm/logs
36+
console.log(dirs.tmp); // /tmp/pgpm
37+
```
38+
39+
### Create Directories
40+
41+
```typescript
42+
import { appstash } from 'appstash';
43+
44+
// Get directories and create them
45+
const dirs = appstash('pgpm', { ensure: true });
46+
47+
// All directories now exist
48+
// dirs.usedFallback will be true if XDG or tmp fallback was used
49+
```
50+
51+
### Resolve Paths
52+
53+
```typescript
54+
import { appstash, resolve } from 'appstash';
55+
56+
const dirs = appstash('pgpm');
57+
58+
// Resolve paths within directories
59+
const configFile = resolve(dirs, 'config', 'settings.json');
60+
// Returns: ~/.pgpm/config/settings.json
61+
62+
const cacheDir = resolve(dirs, 'cache', 'repos', 'my-repo');
63+
// Returns: ~/.pgpm/cache/repos/my-repo
64+
```
65+
66+
### Manual Ensure
67+
68+
```typescript
69+
import { appstash, ensure } from 'appstash';
70+
71+
const dirs = appstash('pgpm');
72+
73+
// Create directories later
74+
const result = ensure(dirs);
75+
76+
console.log(result.created); // ['~/.pgpm', '~/.pgpm/config', ...]
77+
console.log(result.usedFallback); // false (or true if fallback was used)
78+
```
79+
80+
## API
81+
82+
### `appstash(tool, options?)`
83+
84+
Get application directories for a tool.
85+
86+
**Parameters:**
87+
- `tool` (string): Tool name (e.g., 'pgpm', 'lql')
88+
- `options` (object, optional):
89+
- `baseDir` (string): Base directory (defaults to `os.homedir()`)
90+
- `useXdgFallback` (boolean): Use XDG fallback if home fails (default: `true`)
91+
- `ensure` (boolean): Automatically create directories (default: `false`)
92+
- `tmpRoot` (string): Root for temp directory (defaults to `os.tmpdir()`)
93+
94+
**Returns:** `AppStashResult`
95+
```typescript
96+
{
97+
root: string; // ~/.<tool>
98+
config: string; // ~/.<tool>/config
99+
cache: string; // ~/.<tool>/cache
100+
data: string; // ~/.<tool>/data
101+
logs: string; // ~/.<tool>/logs
102+
tmp: string; // /tmp/<tool>
103+
usedFallback?: boolean; // true if XDG or tmp fallback was used
104+
}
105+
```
106+
107+
### `ensure(dirs)`
108+
109+
Create directories if they don't exist. Never throws.
110+
111+
**Parameters:**
112+
- `dirs` (AppStashResult): Directory paths from `appstash()`
113+
114+
**Returns:** `EnsureResult`
115+
```typescript
116+
{
117+
created: string[]; // Directories that were created
118+
usedFallback: boolean; // true if XDG or tmp fallback was used
119+
}
120+
```
121+
122+
### `resolve(dirs, kind, ...parts)`
123+
124+
Resolve a path within a specific directory.
125+
126+
**Parameters:**
127+
- `dirs` (AppStashResult): Directory paths from `appstash()`
128+
- `kind` ('config' | 'cache' | 'data' | 'logs' | 'tmp'): Directory kind
129+
- `parts` (string[]): Path parts to join
130+
131+
**Returns:** `string` - Resolved path
132+
133+
## Directory Structure
134+
135+
### Primary (POSIX-style)
136+
137+
```
138+
~/.<tool>/
139+
├── config/ # Configuration files
140+
├── cache/ # Cached data
141+
├── data/ # Application data
142+
└── logs/ # Log files
143+
144+
/tmp/<tool>/ # Temporary files
145+
```
146+
147+
### Fallback (XDG)
148+
149+
If home directory is unavailable or creation fails, falls back to XDG:
150+
151+
```
152+
~/.config/<tool>/ # Config
153+
~/.cache/<tool>/ # Cache
154+
~/.local/share/<tool>/ # Data
155+
~/.local/state/<tool>/logs/ # Logs
156+
```
157+
158+
### Final Fallback (tmp)
159+
160+
If XDG also fails, falls back to system temp:
161+
162+
```
163+
/tmp/<tool>/
164+
├── config/
165+
├── cache/
166+
├── data/
167+
└── logs/
168+
```
169+
170+
## Examples
171+
172+
### Configuration File
173+
174+
```typescript
175+
import { appstash, resolve } from 'appstash';
176+
import fs from 'fs';
177+
178+
const dirs = appstash('myapp', { ensure: true });
179+
const configPath = resolve(dirs, 'config', 'settings.json');
180+
181+
// Write config
182+
fs.writeFileSync(configPath, JSON.stringify({ theme: 'dark' }));
183+
184+
// Read config
185+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
186+
```
187+
188+
### Cache Management
189+
190+
```typescript
191+
import { appstash, resolve } from 'appstash';
192+
import fs from 'fs';
193+
194+
const dirs = appstash('myapp', { ensure: true });
195+
const cacheFile = resolve(dirs, 'cache', 'data.json');
196+
197+
// Check if cached
198+
if (fs.existsSync(cacheFile)) {
199+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
200+
console.log('Using cached data:', cached);
201+
} else {
202+
// Fetch and cache
203+
const data = await fetchData();
204+
fs.writeFileSync(cacheFile, JSON.stringify(data));
205+
}
206+
```
207+
208+
### Logging
209+
210+
```typescript
211+
import { appstash, resolve } from 'appstash';
212+
import fs from 'fs';
213+
214+
const dirs = appstash('myapp', { ensure: true });
215+
const logFile = resolve(dirs, 'logs', 'app.log');
216+
217+
function log(message: string) {
218+
const timestamp = new Date().toISOString();
219+
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
220+
}
221+
222+
log('Application started');
223+
```
224+
225+
### Custom Base Directory
226+
227+
```typescript
228+
import { appstash } from 'appstash';
229+
230+
// Use a custom base directory
231+
const dirs = appstash('myapp', {
232+
baseDir: '/opt/myapp',
233+
ensure: true
234+
});
235+
236+
console.log(dirs.config); // /opt/myapp/.myapp/config
237+
```
238+
239+
## Design Philosophy
240+
241+
- **Simple**: One function, clear structure
242+
- **Clean**: No pollution of exports, minimal API surface
243+
- **Graceful**: Never throws, always returns valid paths
244+
- **Fallback**: XDG only as absolute fallback, not primary
245+
- **Focused**: Just directory resolution, no state management
246+
247+
## License
248+
249+
MIT
250+
251+
## Contributing
252+
253+
See the main [hyperweb-io/dev-utils repository](https://github.com/hyperweb-io/dev-utils) for contribution guidelines.

0 commit comments

Comments
 (0)